commit aa35aaa8cebf6f0160087dd9b274f33f4e6ea30f Author: arkonsadter Date: Tue Feb 24 21:24:16 2026 +0600 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2f17fb --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Music Platform + +Музыкальная платформа с функционалом, похожим на Яндекс.Музыку/Spotify. + +## Возможности + +- Регистрация и авторизация пользователей +- Загрузка своих песен с обложками +- Скачивание песен +- Создание и управление плейлистами +- Публичные и приватные плейлисты +- Комнаты для совместного прослушивания с чатом +- Dynamic Island плеер (как на iPhone) + +## Технологии + +- Frontend: React + Vite +- Backend: Python FastAPI +- База данных: SQLite +- WebSocket для real-time функций + +## Установка и запуск + +### Backend + +```bash +cd backend +pip install -r requirements.txt +python init_db.py +uvicorn app.main:app --reload +``` + +Backend будет доступен на http://localhost:8000 + +### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Frontend будет доступен на http://localhost:5173 + +## Использование + +1. Зарегистрируйтесь на сайте +2. Загрузите свои песни через раздел "Загрузить" +3. Создавайте плейлисты и добавляйте в них песни +4. Создавайте комнаты для совместного прослушивания с друзьями +5. Используйте Dynamic Island плеер для управления воспроизведением + +## API Endpoints + +- `POST /api/auth/register` - Регистрация +- `POST /api/auth/login` - Вход +- `POST /api/music/upload` - Загрузка песни +- `GET /api/music/songs` - Получить все песни +- `GET /api/music/download/{song_id}` - Скачать песню +- `POST /api/playlists/create` - Создать плейлист +- `GET /api/playlists/my-playlists` - Мои плейлисты +- `POST /api/rooms/create` - Создать комнату +- `WS /api/rooms/ws/{room_code}` - WebSocket для комнаты diff --git a/backend/add_owner_role.py b/backend/add_owner_role.py new file mode 100644 index 0000000..79bbeae --- /dev/null +++ b/backend/add_owner_role.py @@ -0,0 +1,41 @@ +""" +Скрипт для добавления роли создателя (owner) +""" +import sqlite3 + +def migrate(): + conn = sqlite3.connect('music_platform.db') + cursor = conn.cursor() + + try: + # Добавляем поле is_owner + cursor.execute("ALTER TABLE users ADD COLUMN is_owner BOOLEAN DEFAULT 0") + print("✓ Добавлено поле is_owner") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e): + print("✓ Поле is_owner уже существует") + else: + print(f"✗ Ошибка при добавлении is_owner: {e}") + + conn.commit() + + # Показываем пользователя Leuteg + cursor.execute("SELECT id, username, is_admin, is_owner FROM users WHERE username = 'Leuteg'") + user = cursor.fetchone() + + if user: + print(f"\nПользователь найден:") + print(f"ID: {user[0]}, Username: {user[1]}, Admin: {user[2]}, Owner: {user[3]}") + + # Делаем Leuteg создателем + cursor.execute("UPDATE users SET is_owner = 1, is_admin = 1 WHERE username = 'Leuteg'") + conn.commit() + print(f"\n✓ Пользователь 'Leuteg' теперь СОЗДАТЕЛЬ с полными правами!") + else: + print("\n✗ Пользователь 'Leuteg' не найден") + + conn.close() + print("\n✓ Миграция завершена!") + +if __name__ == "__main__": + migrate() diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/auth.py b/backend/app/auth.py new file mode 100644 index 0000000..b0cfe38 --- /dev/null +++ b/backend/app/auth.py @@ -0,0 +1,55 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session +from app.database import get_db +from app.models.models import User + +SECRET_KEY = "your-secret-key-change-in-production-make-it-very-long-and-secure" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict): + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError as e: + print(f"JWT Error: {e}") + raise credentials_exception + + user = db.query(User).filter(User.username == username).first() + if user is None: + raise credentials_exception + + if user.is_banned: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Ваш аккаунт заблокирован" + ) + + return user diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..258a3bb --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./music_platform.db" + +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7ba6f73 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +import os + +app = FastAPI(title="Music Platform API") + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +os.makedirs("uploads/music", exist_ok=True) +os.makedirs("uploads/covers", exist_ok=True) + +app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") + +from app.routes import auth, music, playlists, rooms, admin + +app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) +app.include_router(music.router, prefix="/api/music", tags=["music"]) +app.include_router(playlists.router, prefix="/api/playlists", tags=["playlists"]) +app.include_router(rooms.router, prefix="/api/rooms", tags=["rooms"]) +app.include_router(admin.router) + +@app.get("/") +def root(): + return {"message": "Music Platform API"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/models.py b/backend/app/models/models.py new file mode 100644 index 0000000..9e23d36 --- /dev/null +++ b/backend/app/models/models.py @@ -0,0 +1,62 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, Table, DateTime, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +from app.database import Base + +playlist_songs = Table('playlist_songs', Base.metadata, + Column('playlist_id', Integer, ForeignKey('playlists.id')), + Column('song_id', Integer, ForeignKey('songs.id')) +) + +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_admin = Column(Boolean, default=False) + is_owner = Column(Boolean, default=False) + is_banned = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + + songs = relationship("Song", back_populates="owner") + playlists = relationship("Playlist", back_populates="owner") + rooms = relationship("Room", back_populates="creator") + +class Song(Base): + __tablename__ = "songs" + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + artist = Column(String) + file_path = Column(String) + cover_path = Column(String, nullable=True) + duration = Column(Integer) + owner_id = Column(Integer, ForeignKey("users.id")) + created_at = Column(DateTime, default=datetime.utcnow) + + owner = relationship("User", back_populates="songs") + playlists = relationship("Playlist", secondary=playlist_songs, back_populates="songs") + +class Playlist(Base): + __tablename__ = "playlists" + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + description = Column(String, nullable=True) + is_public = Column(Boolean, default=False) + owner_id = Column(Integer, ForeignKey("users.id")) + created_at = Column(DateTime, default=datetime.utcnow) + + owner = relationship("User", back_populates="playlists") + songs = relationship("Song", secondary=playlist_songs, back_populates="playlists") + +class Room(Base): + __tablename__ = "rooms" + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + code = Column(String, unique=True, index=True) + creator_id = Column(Integer, ForeignKey("users.id")) + current_song_id = Column(Integer, ForeignKey("songs.id"), nullable=True) + is_playing = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + + creator = relationship("User", back_populates="rooms") diff --git a/backend/app/routes/__init__.py b/backend/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py new file mode 100644 index 0000000..bc3f355 --- /dev/null +++ b/backend/app/routes/admin.py @@ -0,0 +1,174 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.database import get_db +from app.models.models import User, Song +from app.auth import get_current_user +from pydantic import BaseModel +import os + +router = APIRouter(prefix="/api/admin", tags=["admin"]) + +def get_admin_user(current_user: User = Depends(get_current_user)): + if not current_user.is_admin and not current_user.is_owner: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещен. Требуются права администратора" + ) + return current_user + +def get_owner_user(current_user: User = Depends(get_current_user)): + if not current_user.is_owner: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Доступ запрещен. Требуются права создателя" + ) + return current_user + +class UpdateSongRequest(BaseModel): + title: str + artist: str + +class BanUserRequest(BaseModel): + user_id: int + is_banned: bool + +class PromoteUserRequest(BaseModel): + user_id: int + is_admin: bool + +@router.get("/users") +def get_all_users( + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + users = db.query(User).all() + return [{ + "id": user.id, + "username": user.username, + "email": user.email, + "is_admin": user.is_admin, + "is_owner": user.is_owner, + "is_banned": user.is_banned, + "created_at": user.created_at + } for user in users] + +@router.get("/songs") +def get_all_songs( + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + songs = db.query(Song).all() + return [{ + "id": song.id, + "title": song.title, + "artist": song.artist, + "file_path": song.file_path, + "cover_path": song.cover_path, + "owner_id": song.owner_id, + "owner_username": song.owner.username if song.owner else None, + "created_at": song.created_at + } for song in songs] + +@router.put("/songs/{song_id}") +def update_song( + song_id: int, + request: UpdateSongRequest, + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + song = db.query(Song).filter(Song.id == song_id).first() + if not song: + raise HTTPException(status_code=404, detail="Песня не найдена") + + song.title = request.title + song.artist = request.artist + db.commit() + db.refresh(song) + + return {"message": "Песня обновлена", "song": { + "id": song.id, + "title": song.title, + "artist": song.artist + }} + +@router.delete("/songs/{song_id}") +def delete_song( + song_id: int, + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + song = db.query(Song).filter(Song.id == song_id).first() + if not song: + raise HTTPException(status_code=404, detail="Песня не найдена") + + # Удаляем файлы + if song.file_path and os.path.exists(song.file_path): + os.remove(song.file_path) + if song.cover_path and os.path.exists(song.cover_path): + os.remove(song.cover_path) + + db.delete(song) + db.commit() + + return {"message": "Песня удалена"} + +@router.post("/users/ban") +def ban_user( + request: BanUserRequest, + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + user = db.query(User).filter(User.id == request.user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + if user.is_admin or user.is_owner: + raise HTTPException(status_code=400, detail="Нельзя забанить администратора или создателя") + + user.is_banned = request.is_banned + db.commit() + + return {"message": f"Пользователь {'забанен' if request.is_banned else 'разбанен'}"} + +@router.post("/users/promote") +def promote_user( + request: PromoteUserRequest, + db: Session = Depends(get_db), + owner: User = Depends(get_owner_user) +): + user = db.query(User).filter(User.id == request.user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + if user.is_owner: + raise HTTPException(status_code=400, detail="Нельзя изменить права создателя") + + user.is_admin = request.is_admin + db.commit() + + return {"message": f"Пользователь {'повышен до администратора' if request.is_admin else 'понижен до пользователя'}"} + +@router.delete("/users/{user_id}") +def delete_user( + user_id: int, + db: Session = Depends(get_db), + admin: User = Depends(get_admin_user) +): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + if user.is_admin or user.is_owner: + raise HTTPException(status_code=400, detail="Нельзя удалить администратора или создателя") + + # Удаляем все песни пользователя + for song in user.songs: + if song.file_path and os.path.exists(song.file_path): + os.remove(song.file_path) + if song.cover_path and os.path.exists(song.cover_path): + os.remove(song.cover_path) + + db.delete(user) + db.commit() + + return {"message": "Пользователь удален"} diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py new file mode 100644 index 0000000..e6dd7bb --- /dev/null +++ b/backend/app/routes/auth.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from pydantic import BaseModel +from app.database import get_db +from app.models.models import User +from app.auth import get_password_hash, verify_password, create_access_token, get_current_user + +router = APIRouter() + +class UserCreate(BaseModel): + username: str + email: str + password: str + +class Token(BaseModel): + access_token: str + token_type: str + +@router.post("/register") +def register(user: UserCreate, db: Session = Depends(get_db)): + if db.query(User).filter(User.username == user.username).first(): + raise HTTPException(status_code=400, detail="Username already exists") + if db.query(User).filter(User.email == user.email).first(): + raise HTTPException(status_code=400, detail="Email already exists") + + db_user = User( + username=user.username, + email=user.email, + hashed_password=get_password_hash(user.password) + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return {"message": "User created successfully"} + +@router.post("/login", response_model=Token) +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == form_data.username).first() + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException(status_code=401, detail="Incorrect username or password") + + if user.is_banned: + raise HTTPException(status_code=403, detail="Ваш аккаунт заблокирован") + + access_token = create_access_token(data={"sub": user.username}) + return {"access_token": access_token, "token_type": "bearer"} + +@router.get("/me") +def get_current_user_info(current_user: User = Depends(get_current_user)): + return { + "id": current_user.id, + "username": current_user.username, + "email": current_user.email, + "is_admin": current_user.is_admin, + "is_owner": current_user.is_owner + } diff --git a/backend/app/routes/music.py b/backend/app/routes/music.py new file mode 100644 index 0000000..19f384b --- /dev/null +++ b/backend/app/routes/music.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from typing import List +from pydantic import BaseModel +import shutil +import os +from app.database import get_db +from app.models.models import Song, User +from app.auth import get_current_user + +router = APIRouter() + +class SongResponse(BaseModel): + id: int + title: str + artist: str + file_path: str + cover_path: str | None + duration: int + owner_id: int + + class Config: + from_attributes = True + +@router.post("/upload") +async def upload_song( + title: str = Form(...), + artist: str = Form(...), + duration: int = Form(...), + file: UploadFile = File(...), + cover: UploadFile = File(None), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + file_path = f"uploads/music/{current_user.id}_{file.filename}" + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + cover_path = None + if cover: + cover_path = f"uploads/covers/{current_user.id}_{cover.filename}" + with open(cover_path, "wb") as buffer: + shutil.copyfileobj(cover.file, buffer) + + song = Song( + title=title, + artist=artist, + file_path=file_path, + cover_path=cover_path, + duration=duration, + owner_id=current_user.id + ) + db.add(song) + db.commit() + db.refresh(song) + return song + +@router.get("/songs", response_model=List[SongResponse]) +def get_songs(db: Session = Depends(get_db)): + return db.query(Song).all() + +@router.get("/my-songs", response_model=List[SongResponse]) +def get_my_songs(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return db.query(Song).filter(Song.owner_id == current_user.id).all() + +@router.get("/download/{song_id}") +def download_song(song_id: int, db: Session = Depends(get_db)): + song = db.query(Song).filter(Song.id == song_id).first() + if not song: + raise HTTPException(status_code=404, detail="Song not found") + return FileResponse(song.file_path, filename=f"{song.title}.mp3") diff --git a/backend/app/routes/playlists.py b/backend/app/routes/playlists.py new file mode 100644 index 0000000..79b04b8 --- /dev/null +++ b/backend/app/routes/playlists.py @@ -0,0 +1,112 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from pydantic import BaseModel +from app.database import get_db +from app.models.models import Playlist, Song, User +from app.auth import get_current_user + +router = APIRouter() + +class PlaylistCreate(BaseModel): + name: str + description: str | None = None + is_public: bool = False + +class PlaylistResponse(BaseModel): + id: int + name: str + description: str | None + is_public: bool + owner_id: int + songs: List[dict] + + class Config: + from_attributes = True + +@router.post("/create") +def create_playlist( + playlist: PlaylistCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + db_playlist = Playlist( + name=playlist.name, + description=playlist.description, + is_public=playlist.is_public, + owner_id=current_user.id + ) + db.add(db_playlist) + db.commit() + db.refresh(db_playlist) + return db_playlist + +@router.get("/my-playlists") +def get_my_playlists(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return db.query(Playlist).filter(Playlist.owner_id == current_user.id).all() + +@router.get("/public") +def get_public_playlists(db: Session = Depends(get_db)): + return db.query(Playlist).filter(Playlist.is_public == True).all() + +@router.post("/{playlist_id}/add-song/{song_id}") +def add_song_to_playlist( + playlist_id: int, + song_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first() + if not playlist or playlist.owner_id != current_user.id: + raise HTTPException(status_code=403, detail="Not authorized") + + song = db.query(Song).filter(Song.id == song_id).first() + if not song: + raise HTTPException(status_code=404, detail="Song not found") + + playlist.songs.append(song) + db.commit() + return {"message": "Song added to playlist"} + +@router.get("/{playlist_id}") +def get_playlist(playlist_id: int, db: Session = Depends(get_db)): + playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first() + if not playlist: + raise HTTPException(status_code=404, detail="Playlist not found") + + songs_data = [{ + "id": song.id, + "title": song.title, + "artist": song.artist, + "file_path": song.file_path, + "cover_path": song.cover_path, + "duration": song.duration, + "owner_id": song.owner_id + } for song in playlist.songs] + + return { + "id": playlist.id, + "name": playlist.name, + "description": playlist.description, + "is_public": playlist.is_public, + "owner_id": playlist.owner_id, + "songs": songs_data + } + +@router.delete("/{playlist_id}/remove-song/{song_id}") +def remove_song_from_playlist( + playlist_id: int, + song_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first() + if not playlist or playlist.owner_id != current_user.id: + raise HTTPException(status_code=403, detail="Not authorized") + + song = db.query(Song).filter(Song.id == song_id).first() + if song in playlist.songs: + playlist.songs.remove(song) + db.commit() + + return {"message": "Song removed from playlist"} diff --git a/backend/app/routes/rooms.py b/backend/app/routes/rooms.py new file mode 100644 index 0000000..506f8bd --- /dev/null +++ b/backend/app/routes/rooms.py @@ -0,0 +1,128 @@ +from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, HTTPException +from sqlalchemy.orm import Session +from typing import Dict, List +from pydantic import BaseModel +import random +import string +from app.database import get_db +from app.models.models import Room, User, Song, Playlist +from app.auth import get_current_user + +router = APIRouter() + +class RoomCreate(BaseModel): + name: str + +class ConnectionManager: + def __init__(self): + self.active_connections: Dict[str, List[dict]] = {} + + async def connect(self, websocket: WebSocket, room_code: str, username: str): + await websocket.accept() + if room_code not in self.active_connections: + self.active_connections[room_code] = [] + + # Проверяем, не подключен ли уже этот пользователь + existing = [conn for conn in self.active_connections[room_code] if conn["username"] == username] + if not existing: + self.active_connections[room_code].append({"ws": websocket, "username": username}) + + await self.broadcast({"type": "user_count", "count": len(self.active_connections[room_code])}, room_code) + + def disconnect(self, websocket: WebSocket, room_code: str): + if room_code in self.active_connections: + self.active_connections[room_code] = [ + conn for conn in self.active_connections[room_code] if conn["ws"] != websocket + ] + return len(self.active_connections[room_code]) + return 0 + + async def broadcast(self, message: dict, room_code: str): + if room_code in self.active_connections: + disconnected = [] + for connection in self.active_connections[room_code]: + try: + await connection["ws"].send_json(message) + except: + disconnected.append(connection) + + # Удаляем отключенные соединения + for conn in disconnected: + if conn in self.active_connections[room_code]: + self.active_connections[room_code].remove(conn) + + def get_user_count(self, room_code: str): + if room_code in self.active_connections: + return len(self.active_connections[room_code]) + return 0 + +manager = ConnectionManager() + +def generate_room_code(): + return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + +@router.post("/create") +def create_room( + room: RoomCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + code = generate_room_code() + db_room = Room(name=room.name, code=code, creator_id=current_user.id) + db.add(db_room) + db.commit() + db.refresh(db_room) + return db_room + +@router.get("/{room_code}") +def get_room(room_code: str, db: Session = Depends(get_db)): + room = db.query(Room).filter(Room.code == room_code).first() + if not room: + raise HTTPException(status_code=404, detail="Room not found") + user_count = manager.get_user_count(room_code) + return {**room.__dict__, "user_count": user_count} + +@router.post("/{room_code}/add-song/{song_id}") +def add_song_to_room( + room_code: str, + song_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + room = db.query(Room).filter(Room.code == room_code).first() + if not room: + raise HTTPException(status_code=404, detail="Room not found") + + song = db.query(Song).filter(Song.id == song_id).first() + if not song: + raise HTTPException(status_code=404, detail="Song not found") + + return {"message": "Song added to queue"} + +@router.post("/{room_code}/add-playlist/{playlist_id}") +def add_playlist_to_room( + room_code: str, + playlist_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + room = db.query(Room).filter(Room.code == room_code).first() + if not room: + raise HTTPException(status_code=404, detail="Room not found") + + playlist = db.query(Playlist).filter(Playlist.id == playlist_id).first() + if not playlist: + raise HTTPException(status_code=404, detail="Playlist not found") + + return {"message": "Playlist added to queue", "songs": [{"id": s.id, "title": s.title, "artist": s.artist} for s in playlist.songs]} + +@router.websocket("/ws/{room_code}") +async def websocket_endpoint(websocket: WebSocket, room_code: str, username: str = "User"): + await manager.connect(websocket, room_code, username) + try: + while True: + data = await websocket.receive_json() + await manager.broadcast(data, room_code) + except WebSocketDisconnect: + count = manager.disconnect(websocket, room_code) + await manager.broadcast({"type": "user_count", "count": count}, room_code) diff --git a/backend/init_db.py b/backend/init_db.py new file mode 100644 index 0000000..d83ea9d --- /dev/null +++ b/backend/init_db.py @@ -0,0 +1,5 @@ +from app.database import engine, Base +from app.models.models import User, Song, Playlist, Room + +Base.metadata.create_all(bind=engine) +print("Database initialized successfully!") diff --git a/backend/migrate_db.py b/backend/migrate_db.py new file mode 100644 index 0000000..9217acf --- /dev/null +++ b/backend/migrate_db.py @@ -0,0 +1,52 @@ +""" +Скрипт для миграции базы данных - добавляет поля is_admin и is_banned +""" +import sqlite3 + +def migrate(): + conn = sqlite3.connect('music_platform.db') + cursor = conn.cursor() + + try: + # Добавляем поле is_admin + cursor.execute("ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT 0") + print("✓ Добавлено поле is_admin") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e): + print("✓ Поле is_admin уже существует") + else: + print(f"✗ Ошибка при добавлении is_admin: {e}") + + try: + # Добавляем поле is_banned + cursor.execute("ALTER TABLE users ADD COLUMN is_banned BOOLEAN DEFAULT 0") + print("✓ Добавлено поле is_banned") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e): + print("✓ Поле is_banned уже существует") + else: + print(f"✗ Ошибка при добавлении is_banned: {e}") + + conn.commit() + + # Показываем всех пользователей + cursor.execute("SELECT id, username, is_admin, is_banned FROM users") + users = cursor.fetchall() + + print("\nТекущие пользователи:") + for user in users: + print(f"ID: {user[0]}, Username: {user[1]}, Admin: {user[2]}, Banned: {user[3]}") + + # Предлагаем сделать первого пользователя админом + if users: + make_admin = input(f"\nСделать пользователя '{users[0][1]}' администратором? (y/n): ") + if make_admin.lower() == 'y': + cursor.execute("UPDATE users SET is_admin = 1 WHERE id = ?", (users[0][0],)) + conn.commit() + print(f"✓ Пользователь '{users[0][1]}' теперь администратор") + + conn.close() + print("\n✓ Миграция завершена!") + +if __name__ == "__main__": + migrate() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..b8df126 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.109.0 +uvicorn==0.27.0 +sqlalchemy==2.0.25 +python-multipart==0.0.6 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +pydantic==2.5.3 +pydantic-settings==2.1.0 +websockets==12.0 +aiofiles==23.2.1 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9c17ef2 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Music Platform + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6b0bd1f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2052 @@ +{ + "name": "music-platform-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "music-platform-frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.6.5", + "lucide-react": "^0.309.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.309.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.309.0.tgz", + "integrity": "sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..12c6522 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "music-platform-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1", + "axios": "^1.6.5", + "lucide-react": "^0.309.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.11" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..bd34edb --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,61 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { useState, useEffect, createContext } from 'react' +import Login from './pages/Login' +import Register from './pages/Register' +import Home from './pages/Home' +import Upload from './pages/Upload' +import Playlists from './pages/Playlists' +import Rooms from './pages/Rooms' +import Room from './pages/Room' +import Admin from './pages/Admin' +import DynamicPlayer from './components/DynamicPlayer' +import Layout from './components/Layout' + +// Создаем контекст для управления аудио +export const AudioContext = createContext() + +function App() { + const [token, setToken] = useState(localStorage.getItem('token')) + const [currentSong, setCurrentSong] = useState(null) + const [isPlaying, setIsPlaying] = useState(false) + const [isInRoom, setIsInRoom] = useState(false) + const [playlist, setPlaylist] = useState([]) + + useEffect(() => { + if (token) { + localStorage.setItem('token', token) + } else { + localStorage.removeItem('token') + } + }, [token]) + + // Функция для остановки глобального плеера + const stopGlobalPlayer = () => { + setIsPlaying(false) + } + + const handleSongChange = (song) => { + setCurrentSong(song) + setIsPlaying(true) + } + + return ( + + + + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + + {currentSong && !isInRoom && } + + + ) +} + +export default App diff --git a/frontend/src/components/DynamicPlayer.css b/frontend/src/components/DynamicPlayer.css new file mode 100644 index 0000000..06de975 --- /dev/null +++ b/frontend/src/components/DynamicPlayer.css @@ -0,0 +1,452 @@ +.dynamic-player { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.9); + backdrop-filter: blur(20px); + border-radius: 40px; + padding: 12px 24px; + display: flex; + align-items: center; + gap: 15px; + z-index: 1000; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + animation: slideDown 0.5s ease; + max-width: 500px; + width: 90%; + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dynamic-player.expanded { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 450px; + width: 90%; + height: 600px; + border-radius: 30px; + padding: 40px 30px; + flex-direction: column; + justify-content: flex-start; + animation: expandPlayer 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideDown { + from { + transform: translateX(-50%) translateY(-100px); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +@keyframes expandPlayer { + from { + top: 20px; + transform: translateX(-50%); + height: 74px; + } + to { + top: 50%; + transform: translate(-50%, -50%); + height: 600px; + } +} + +.player-mini-cover { + width: 50px; + height: 50px; + border-radius: 12px; + overflow: hidden; + background: rgba(255, 255, 255, 0.1); + flex-shrink: 0; +} + +.player-mini-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.mini-default-cover { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: rgba(255, 255, 255, 0.5); +} + +.player-info-mini { + flex: 1; + min-width: 0; +} + +.player-info-mini h4 { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.player-info-mini p { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.player-controls-mini { + display: flex; + gap: 10px; + align-items: center; +} + +.control-btn { + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + transition: all 0.3s; +} + +.control-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +.control-btn.play-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.control-btn.play-btn:hover { + transform: scale(1.15); +} + +.player-progress { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(255, 255, 255, 0.2); + border-radius: 0 0 40px 40px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.1s linear; +} + +@media (max-width: 600px) { + .dynamic-player { + padding: 10px 16px; + gap: 10px; + } + + .player-mini-cover { + width: 40px; + height: 40px; + } + + .control-btn { + width: 35px; + height: 35px; + } +} + +.player-expanded-content { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.collapse-btn { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + transition: all 0.3s; +} + +.collapse-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.expanded-cover { + width: 280px; + height: 280px; + border-radius: 20px; + overflow: hidden; + margin: 60px 0 30px; + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.expanded-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.expanded-default-cover { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 120px; + color: rgba(255, 255, 255, 0.3); +} + +.expanded-info { + text-align: center; + margin-bottom: 40px; +} + +.expanded-info h2 { + font-size: 24px; + margin-bottom: 8px; + font-weight: 600; +} + +.expanded-info p { + font-size: 16px; + color: rgba(255, 255, 255, 0.7); +} + +.expanded-progress-container { + width: 100%; + margin-bottom: 10px; +} + +.expanded-progress-track { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + cursor: pointer; + position: relative; + overflow: visible; + user-select: none; +} + +.expanded-progress-track:hover .expanded-progress-bar::after { + opacity: 1; + transform: translateY(-50%) scale(1.2); +} + +.expanded-progress-bar { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 3px; + transition: width 0.1s linear; + position: relative; +} + +.expanded-progress-bar::after { + content: ''; + position: absolute; + right: -7px; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 14px; + background: #fff; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: all 0.2s; +} + +.expanded-progress-track:active .expanded-progress-bar::after { + opacity: 1; + transform: translateY(-50%) scale(1.3); +} + +.expanded-progress-time { + display: flex; + justify-content: space-between; + margin-top: 8px; + font-size: 12px; + color: rgba(255, 255, 255, 0.6); +} + +.expanded-controls { + display: flex; + gap: 30px; + align-items: center; + margin-top: auto; + margin-bottom: 20px; +} + +.control-btn-large { + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + transition: all 0.3s; +} + +.control-btn-large:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +.play-btn-large { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + width: 80px; + height: 80px; +} + +.play-btn-large:hover { + transform: scale(1.15); +} + +.show-player-btn { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.9); + backdrop-filter: blur(20px); + border-radius: 30px; + padding: 12px 24px; + display: flex; + align-items: center; + gap: 10px; + z-index: 1000; + color: #fff; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + animation: slideDown 0.5s ease; + transition: all 0.3s; +} + +.show-player-btn:hover { + background: rgba(0, 0, 0, 1); + transform: translateX(-50%) translateY(-2px); +} + +.show-player-btn span { + font-size: 14px; + font-weight: 500; +} + +.volume-control { + width: 100%; + display: flex; + align-items: center; + gap: 12px; + padding: 0 5px; + margin-top: 15px; +} + +.volume-btn { + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + transition: all 0.3s; + flex-shrink: 0; +} + +.volume-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +.volume-slider { + flex: 1; + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.2); + outline: none; + -webkit-appearance: none; + appearance: none; + cursor: pointer; +} + +.volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: #fff; + cursor: pointer; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + transition: all 0.2s; +} + +.volume-slider::-webkit-slider-thumb:hover { + transform: scale(1.3); + box-shadow: 0 4px 10px rgba(102, 126, 234, 0.6); +} + +.volume-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #fff; + cursor: pointer; + border: none; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + transition: all 0.2s; +} + +.volume-slider::-moz-range-thumb:hover { + transform: scale(1.3); + box-shadow: 0 4px 10px rgba(102, 126, 234, 0.6); +} + +.volume-slider::-webkit-slider-runnable-track { + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.2); +} + +.volume-slider::-moz-range-track { + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.2); +} + +.volume-percentage { + font-size: 13px; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + min-width: 42px; + text-align: right; +} diff --git a/frontend/src/components/DynamicPlayer.jsx b/frontend/src/components/DynamicPlayer.jsx new file mode 100644 index 0000000..8daebfd --- /dev/null +++ b/frontend/src/components/DynamicPlayer.jsx @@ -0,0 +1,273 @@ +import { useState, useEffect, useRef } from 'react' +import { Play, Pause, SkipForward, SkipBack, ChevronDown, ChevronUp, Volume2, VolumeX } from 'lucide-react' +import './DynamicPlayer.css' + +function DynamicPlayer({ song, isPlaying, setIsPlaying, playlist = [], onSongChange }) { + const [progress, setProgress] = useState(0) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + const [isExpanded, setIsExpanded] = useState(false) + const [isHidden, setIsHidden] = useState(false) + const [isDragging, setIsDragging] = useState(false) + const [volume, setVolume] = useState(1) + const [isMuted, setIsMuted] = useState(false) + const audioRef = useRef(null) + + useEffect(() => { + if (audioRef.current && song) { + audioRef.current.src = `http://localhost:8000/${song.file_path}` + audioRef.current.volume = volume + audioRef.current.load() + if (isPlaying) { + audioRef.current.play().catch(err => console.error('Play error:', err)) + } + } + }, [song]) + + useEffect(() => { + if (audioRef.current) { + audioRef.current.volume = isMuted ? 0 : volume + } + }, [volume, isMuted]) + + useEffect(() => { + if (audioRef.current && audioRef.current.src) { + if (isPlaying) { + audioRef.current.play().catch(err => console.error('Play error:', err)) + } else { + audioRef.current.pause() + } + } + }, [isPlaying]) + + const handleTimeUpdate = () => { + if (audioRef.current && audioRef.current.duration) { + const percent = (audioRef.current.currentTime / audioRef.current.duration) * 100 + setProgress(isNaN(percent) ? 0 : percent) + setCurrentTime(audioRef.current.currentTime) + } + } + + const handleLoadedMetadata = () => { + if (audioRef.current) { + setDuration(audioRef.current.duration) + } + } + + const togglePlay = (e) => { + e.stopPropagation() + setIsPlaying(!isPlaying) + } + + const formatTime = (time) => { + if (isNaN(time) || !isFinite(time)) return '0:00' + const minutes = Math.floor(time / 60) + const seconds = Math.floor(time % 60) + return `${minutes}:${seconds.toString().padStart(2, '0')}` + } + + const handleProgressClick = (e) => { + if (audioRef.current && audioRef.current.duration) { + const rect = e.currentTarget.getBoundingClientRect() + const percent = (e.clientX - rect.left) / rect.width + const newTime = percent * audioRef.current.duration + audioRef.current.currentTime = newTime + setProgress(percent * 100) + setCurrentTime(newTime) + } + } + + const handleProgressMouseDown = (e) => { + setIsDragging(true) + handleProgressClick(e) + } + + const handleProgressMouseMove = (e) => { + if (isDragging && audioRef.current && audioRef.current.duration) { + const rect = e.currentTarget.getBoundingClientRect() + const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) + const newTime = percent * audioRef.current.duration + audioRef.current.currentTime = newTime + setProgress(percent * 100) + setCurrentTime(newTime) + } + } + + const handleProgressMouseUp = () => { + setIsDragging(false) + } + + useEffect(() => { + if (isDragging) { + document.addEventListener('mouseup', handleProgressMouseUp) + return () => { + document.removeEventListener('mouseup', handleProgressMouseUp) + } + } + }, [isDragging]) + + const handleHide = (e) => { + e.stopPropagation() + setIsHidden(true) + // Не останавливаем воспроизведение при скрытии + } + + const handleShow = () => { + setIsHidden(false) + } + + const handleVolumeChange = (e) => { + const newVolume = parseFloat(e.target.value) + setVolume(newVolume) + if (newVolume > 0) { + setIsMuted(false) + } + } + + const toggleMute = () => { + setIsMuted(!isMuted) + } + + const handleNext = () => { + if (playlist.length === 0 || !song) return + const currentIndex = playlist.findIndex(s => s.id === song.id) + const nextIndex = (currentIndex + 1) % playlist.length + if (onSongChange) { + onSongChange(playlist[nextIndex]) + } + } + + const handlePrevious = () => { + if (playlist.length === 0 || !song) return + const currentIndex = playlist.findIndex(s => s.id === song.id) + const prevIndex = currentIndex === 0 ? playlist.length - 1 : currentIndex - 1 + if (onSongChange) { + onSongChange(playlist[prevIndex]) + } + } + + if (!song) return null + + return ( + <> + {/* Аудио элемент всегда рендерится, даже когда плеер скрыт */} +