diff --git a/cmd/server/server.go b/cmd/server/server.go index cc7aedd..3d27a11 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -39,11 +39,11 @@ func StartServer(staticFS embed.FS) error { songRepo := db.NewBaseDB[models.Song](dbConn) playlistRepo := db.NewBaseDB[models.Playlist](dbConn) playlistOwnersRepo := db.NewBaseDB[models.PlaylistOwner](dbConn) - playlistSongssRepo := db.NewBaseDB[models.PlaylistSong](dbConn) + playlistSongsRepo := db.NewBaseDB[models.PlaylistSong](dbConn) downloadService := download.New(songRepo) - playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongssRepo, downloadService) - songsService := songs.New(playlistSongssRepo, songRepo, playlistRepo, downloadService) + playlistsService := playlists.New(playlistRepo, playlistOwnersRepo, playlistSongsRepo, downloadService) + songsService := songs.New(playlistSongsRepo, playlistOwnersRepo, songRepo, playlistRepo, downloadService) jwtUtil := jwt.NewJWTImpl() diff --git a/handlers/apis/email_login.go b/handlers/apis/email_login.go index eb02712..24cec19 100644 --- a/handlers/apis/email_login.go +++ b/handlers/apis/email_login.go @@ -119,7 +119,18 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt } sessionToken, err := e.service.VerifyOtp(verificationToken.Value, reqBody) - // TODO: specify errors further suka + if errors.Is(err, login.ErrExpiredVerificationCode) { + status. + GenericError("Expired verification code!"). + Render(context.Background(), w) + return + } + if errors.Is(err, login.ErrInvalidVerificationCode) { + status. + GenericError("Invalid verification code!"). + Render(context.Background(), w) + return + } if err != nil { log.Error(err) // w.WriteHeader(http.StatusInternalServerError) diff --git a/handlers/apis/playlist.go b/handlers/apis/playlist.go index b945944..397d6a4 100644 --- a/handlers/apis/playlist.go +++ b/handlers/apis/playlist.go @@ -53,7 +53,7 @@ func (p *playlistApi) HandleCreatePlaylist(w http.ResponseWriter, r *http.Reques } func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http.Request) { - _, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷♂️")) return @@ -78,9 +78,9 @@ func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http. var err error switch removeSongFromPlaylist { case "false": - err = p.songService.AddSongToPlaylist(songId, playlistId) + err = p.songService.AddSongToPlaylist(songId, playlistId, profileId) case "true": - err = p.songService.RemoveSongFromPlaylist(songId, playlistId) + err = p.songService.RemoveSongFromPlaylist(songId, playlistId, profileId) } if err != nil { diff --git a/handlers/apis/songs.go b/handlers/apis/songs.go index 1e228a6..7978812 100644 --- a/handlers/apis/songs.go +++ b/handlers/apis/songs.go @@ -2,6 +2,7 @@ package apis import ( "dankmuzikk/entities" + "dankmuzikk/handlers" "dankmuzikk/log" "dankmuzikk/services/playlists/songs" "dankmuzikk/services/youtube/download" @@ -20,6 +21,11 @@ func NewDownloadHandler(service *download.Service, songsService *songs.Service) } func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { + profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + if !profileIdCorrect { + w.Write([]byte("🤷♂️")) + return + } songId := r.URL.Query().Get("song-id") if songId == "" { w.WriteHeader(http.StatusBadRequest) @@ -31,7 +37,7 @@ func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.Response return } - err := s.songsService.IncrementSongPlays(songId, playlistId) + err := s.songsService.IncrementSongPlays(songId, playlistId, profileId) if err != nil { log.Errorln(err) w.WriteHeader(http.StatusInternalServerError) diff --git a/handlers/handler.go b/handlers/handler.go index 7c93598..fb337db 100644 --- a/handlers/handler.go +++ b/handlers/handler.go @@ -17,7 +17,7 @@ var noAuthPaths = []string{"/login", "/signup"} // Handler is handler for pages and APIs, where it wraps the common stuff in one place. type Handler struct { profileRepo db.GORMDBGetter - jwtUtil jwt.Decoder[any] + jwtUtil jwt.Decoder[jwt.Json] } // NewHandler returns a new AuthHandler instance. @@ -25,7 +25,7 @@ type Handler struct { // Where BaseDB doesn't provide column selection yet :( func NewHandler( accountRepo db.GORMDBGetter, - jwtUtil jwt.Decoder[any], + jwtUtil jwt.Decoder[jwt.Json], ) *Handler { return &Handler{accountRepo, jwtUtil} } @@ -107,11 +107,7 @@ func (a *Handler) authenticate(r *http.Request) (entities.Profile, error) { if err != nil { return entities.Profile{}, err } - payload, valid := theThing.Payload.(map[string]any) - if !valid || payload == nil { - return entities.Profile{}, err - } - username, validUsername := theThing.Payload.(map[string]any)["username"].(string) + username, validUsername := theThing.Payload["username"].(string) if !validUsername || username == "" { return entities.Profile{}, err } diff --git a/handlers/pages/pages.go b/handlers/pages/pages.go index 1167da4..a899521 100644 --- a/handlers/pages/pages.go +++ b/handlers/pages/pages.go @@ -24,14 +24,14 @@ const ( type pagesHandler struct { profileRepo db.GetterRepo[models.Profile] playlistsService *playlists.Service - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] ytSearch search.Service } func NewPagesHandler( profileRepo db.GetterRepo[models.Profile], playlistsService *playlists.Service, - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ytSearch search.Service, ) *pagesHandler { return &pagesHandler{ diff --git a/models/verification_code.go b/models/verification_code.go index f7640c6..1c672fa 100644 --- a/models/verification_code.go +++ b/models/verification_code.go @@ -8,7 +8,6 @@ type EmailVerificationCode struct { Account Account Code string `gorm:"not null"` CreatedAt time.Time - UpdatedAt time.Time } func (e EmailVerificationCode) GetId() uint { diff --git a/services/jwt/jwt_impl.go b/services/jwt/jwt_impl.go index b55b369..f384723 100644 --- a/services/jwt/jwt_impl.go +++ b/services/jwt/jwt_impl.go @@ -14,8 +14,8 @@ type JWTImpl[T any] struct{} // NewJWTImpl returns a new JWTImpl instance, // and since session tokens are to validate users the working type is models.User -func NewJWTImpl() Manager[any] { - return &JWTImpl[any]{} +func NewJWTImpl() Manager[Json] { + return &JWTImpl[Json]{} } // Sign returns a JWT string(which will be the session token) based on the set JWT secret, diff --git a/services/login/email.go b/services/login/email.go index 9a56a63..50dff64 100644 --- a/services/login/email.go +++ b/services/login/email.go @@ -18,14 +18,14 @@ type EmailLoginService struct { accountRepo db.CRUDRepo[models.Account] profileRepo db.CRUDRepo[models.Profile] otpRepo db.CRUDRepo[models.EmailVerificationCode] - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] } func NewEmailLoginService( accountRepo db.CRUDRepo[models.Account], profileRepo db.CRUDRepo[models.Profile], otpRepo db.CRUDRepo[models.EmailVerificationCode], - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ) *EmailLoginService { return &EmailLoginService{ accountRepo: accountRepo, @@ -48,7 +48,7 @@ func (e *EmailLoginService) Login(user entities.LoginRequest) (string, error) { profile[0].Account = account[0] profile[0].AccountId = account[0].Id - verificationToken, err := e.jwtUtil.Sign(map[string]string{ + verificationToken, err := e.jwtUtil.Sign(jwt.Json{ "name": profile[0].Name, "email": profile[0].Account.Email, "username": profile[0].Username, @@ -78,7 +78,7 @@ func (e *EmailLoginService) Signup(user entities.SignupRequest) (string, error) return "", err } - verificationToken, err := e.jwtUtil.Sign(map[string]string{ + verificationToken, err := e.jwtUtil.Sign(jwt.Json{ "name": profile.Name, "email": profile.Account.Email, "username": profile.Username, @@ -91,23 +91,22 @@ func (e *EmailLoginService) Signup(user entities.SignupRequest) (string, error) } func (e *EmailLoginService) VerifyOtp(token string, otp entities.OtpRequest) (string, error) { - user, err := e.jwtUtil.Decode(token, jwt.VerificationToken) + tokeeeen, err := e.jwtUtil.Decode(token, jwt.VerificationToken) if err != nil { return "", err } - mappedUser := user.Payload.(map[string]any) - email, emailExists := mappedUser["email"].(string) + email, emailExists := tokeeeen.Payload["email"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !emailExists { return "", errors.New("missing email") } - name, nameExists := mappedUser["name"].(string) + name, nameExists := tokeeeen.Payload["name"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !nameExists { return "", errors.New("missing name") } - username, usernameExists := mappedUser["username"].(string) + username, usernameExists := tokeeeen.Payload["username"].(string) // TODO: ADD THE FUCKING ERRORS SUKA if !usernameExists { return "", errors.New("missing username") @@ -124,15 +123,19 @@ func (e *EmailLoginService) VerifyOtp(token string, otp entities.OtpRequest) (st } verCode := verCodes[len(verCodes)-1] defer func() { - _ = e.otpRepo.Delete("id = ?", verCode.Id) + _ = e.otpRepo.Delete("account_id = ?", account[0].Id) }() + if verCode.CreatedAt.Add(time.Hour / 2).Before(time.Now()) { + return "", ErrExpiredVerificationCode + } + err = bcrypt.CompareHashAndPassword([]byte(verCode.Code), []byte(otp.Code)) if err != nil { - return "", err + return "", ErrInvalidVerificationCode } - sessionToken, err := e.jwtUtil.Sign(map[string]string{ + sessionToken, err := e.jwtUtil.Sign(jwt.Json{ "email": email, "name": name, "username": username, diff --git a/services/login/errors.go b/services/login/errors.go index 7773636..4ecc231 100644 --- a/services/login/errors.go +++ b/services/login/errors.go @@ -3,7 +3,9 @@ package login import "errors" var ( - ErrAccountNotFound = errors.New("account was not found") - ErrProfileNotFound = errors.New("profile was not found") - ErrAccountExists = errors.New("an account with the associated email already exists") + ErrAccountNotFound = errors.New("account was not found") + ErrProfileNotFound = errors.New("profile was not found") + ErrAccountExists = errors.New("an account with the associated email already exists") + ErrExpiredVerificationCode = errors.New("expired verification code") + ErrInvalidVerificationCode = errors.New("invalid verification code") ) diff --git a/services/login/google.go b/services/login/google.go index 713dfab..f828df4 100644 --- a/services/login/google.go +++ b/services/login/google.go @@ -41,14 +41,14 @@ type GoogleLoginService struct { accountRepo db.CRUDRepo[models.Account] profileRepo db.CRUDRepo[models.Profile] otpRepo db.CRUDRepo[models.EmailVerificationCode] - jwtUtil jwt.Manager[any] + jwtUtil jwt.Manager[jwt.Json] } func NewGoogleLoginService( accountRepo db.CRUDRepo[models.Account], profileRepo db.CRUDRepo[models.Profile], otpRepo db.CRUDRepo[models.EmailVerificationCode], - jwtUtil jwt.Manager[any], + jwtUtil jwt.Manager[jwt.Json], ) *GoogleLoginService { return &GoogleLoginService{ accountRepo: accountRepo, @@ -79,7 +79,7 @@ func (g *GoogleLoginService) Login(state, code string) (string, error) { profile[0].Account = account[0] profile[0].AccountId = account[0].Id - verificationToken, err := g.jwtUtil.Sign(map[string]string{ + verificationToken, err := g.jwtUtil.Sign(jwt.Json{ "name": profile[0].Name, "email": profile[0].Account.Email, "username": profile[0].Username, @@ -107,7 +107,7 @@ func (g *GoogleLoginService) Signup(googleUser oauthUserInfo) (string, error) { return "", err } - verificationToken, err := g.jwtUtil.Sign(map[string]string{ + verificationToken, err := g.jwtUtil.Sign(jwt.Json{ "name": profile.Name, "email": profile.Account.Email, "username": profile.Username, diff --git a/services/playlists/playlists.go b/services/playlists/playlists.go index 7c94a51..b15f029 100644 --- a/services/playlists/playlists.go +++ b/services/playlists/playlists.go @@ -138,7 +138,6 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er Id: ownerId, }). Where("public_id = ?", playlistPubId). - Preload("Songs"). Association("Playlist"). Find(&dbPlaylists) if err != nil { @@ -148,46 +147,33 @@ func (p *Service) Get(playlistPubId string, ownerId uint) (entities.Playlist, er return entities.Playlist{}, ErrUnauthorizedToSeePlaylist } - var playlistSongs []models.PlaylistSong - err = p. - playlistSongsRepo. + gigaQuery := `SELECT yt_id, title, artist, thumbnail_url, duration, ps.created_at, ps.play_times + FROM + playlist_owners po JOIN playlist_songs ps ON po.playlist_id = ps.playlist_id + JOIN songs + ON ps.song_id = songs.id + WHERE ps.playlist_id = ? AND po.profile_id = ? + ORDER BY ps.created_at;` + + rows, err := p.repo. GetDB(). - Model(new(models.PlaylistSong)). - Where("playlist_id = ?", dbPlaylists[0].Id). - Select("song_id", "play_times", "votes", "created_at"). - Find(&playlistSongs). - Error + Raw(gigaQuery, dbPlaylists[0].Id, ownerId). + Rows() if err != nil { - return entities.Playlist{ - PublicId: dbPlaylists[0].PublicId, - Title: dbPlaylists[0].Title, - }, err - } - if len(playlistSongs) == 0 { - return entities.Playlist{ - PublicId: dbPlaylists[0].PublicId, - Title: dbPlaylists[0].Title, - }, ErrEmptyPlaylist - } - - mappedPlaylistSongsToPlaysSuka := make(map[uint]int) - mappedPlaylistSongsToCreatedAtSuka := make(map[uint]time.Time) - for _, playlistSong := range playlistSongs { - mappedPlaylistSongsToPlaysSuka[playlistSong.SongId] = playlistSong.PlayTimes - mappedPlaylistSongsToCreatedAtSuka[playlistSong.SongId] = playlistSong.CreatedAt + return entities.Playlist{}, err } + defer rows.Close() - songs := make([]entities.Song, len(dbPlaylists[0].Songs)) - for i, song := range dbPlaylists[0].Songs { - songs[i] = entities.Song{ - YtId: song.YtId, - Title: song.Title, - Artist: song.Artist, - ThumbnailUrl: song.ThumbnailUrl, - Duration: song.Duration, - PlayTimes: mappedPlaylistSongsToPlaysSuka[song.Id], - AddedAt: mappedPlaylistSongsToCreatedAtSuka[song.Id].Format("2, January, 2006"), + songs := make([]entities.Song, 0) + for rows.Next() { + var song entities.Song + var addedAt time.Time + err = rows.Scan(&song.YtId, &song.Title, &song.Artist, &song.ThumbnailUrl, &song.Duration, &addedAt, &song.PlayTimes) + if err != nil { + continue } + song.AddedAt = addedAt.Format("2, January, 2006") + songs = append(songs, song) } return entities.Playlist{ diff --git a/services/playlists/songs/songs.go b/services/playlists/songs/songs.go index df90500..5ea0ac8 100644 --- a/services/playlists/songs/songs.go +++ b/services/playlists/songs/songs.go @@ -9,24 +9,27 @@ import ( // Service represents songs in platlists management service, // where it adds and deletes songs to and from playlists type Service struct { - playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong] - songRepo db.UnsafeCRUDRepo[models.Song] - playlistRepo db.UnsafeCRUDRepo[models.Playlist] - downloadService *download.Service + playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong] + playlistOwnerRepo db.CRUDRepo[models.PlaylistOwner] + songRepo db.UnsafeCRUDRepo[models.Song] + playlistRepo db.UnsafeCRUDRepo[models.Playlist] + downloadService *download.Service } // New accepts repos lol, and returns a new instance to the songs playlists service. func New( playlistSongRepo db.UnsafeCRUDRepo[models.PlaylistSong], + playlistOwnerRepo db.CRUDRepo[models.PlaylistOwner], songRepo db.UnsafeCRUDRepo[models.Song], playlistRepo db.UnsafeCRUDRepo[models.Playlist], downloadService *download.Service, ) *Service { return &Service{ - playlistSongRepo: playlistSongRepo, - songRepo: songRepo, - playlistRepo: playlistRepo, - downloadService: downloadService, + playlistSongRepo: playlistSongRepo, + playlistOwnerRepo: playlistOwnerRepo, + songRepo: songRepo, + playlistRepo: playlistRepo, + downloadService: downloadService, } } @@ -34,15 +37,20 @@ func New( // checks if the actual song and playlist exist then adds the song to the given playlist, // and returns an occurring error. // TODO: check playlist's owner :) -func (s *Service) AddSongToPlaylist(songId, playlistPubId string) error { - song, err := s.songRepo.GetByConds("yt_id = ?", songId) +func (s *Service) AddSongToPlaylist(songId, playlistPubId string, ownerId uint) error { + playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) if err != nil { return err } - playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist[0].Id) if err != nil { return err } + song, err := s.songRepo.GetByConds("yt_id = ?", songId) + if err != nil { + return err + } + err = s.playlistSongRepo.Add(&models.PlaylistSong{ PlaylistId: playlist[0].Id, SongId: song[0].Id, @@ -57,28 +65,33 @@ func (s *Service) AddSongToPlaylist(songId, playlistPubId string) error { // IncrementSongPlays increases the song's play times in the given playlist. // Checks for the song and playlist first, yada yada... // TODO: check playlist's owner :) -func (s *Service) IncrementSongPlays(songId, playlistPubId string) error { - var song models.Song +func (s *Service) IncrementSongPlays(songId, playlistPubId string, ownerId uint) error { + var playlist models.Playlist err := s. songRepo. GetDB(). - Model(new(models.Song)). + Model(new(models.Playlist)). Select("id"). - Where("yt_id = ?", songId). - First(&song). + Where("public_id = ?", playlistPubId). + First(&playlist). Error if err != nil { return err } - var playlist models.Playlist + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist.Id) + if err != nil { + return err + } + + var song models.Song err = s. songRepo. GetDB(). - Model(new(models.Playlist)). + Model(new(models.Song)). Select("id"). - Where("public_id = ?", playlistPubId). - First(&playlist). + Where("yt_id = ?", songId). + First(&song). Error if err != nil { return err @@ -110,12 +123,16 @@ func (s *Service) IncrementSongPlays(songId, playlistPubId string) error { // checks if the actual song and playlist exist then removes the song to the given playlist, // and returns an occurring error. // TODO: check playlist's owner :) -func (s *Service) RemoveSongFromPlaylist(songId, playlistPubId string) error { - song, err := s.songRepo.GetByConds("yt_id = ?", songId) +func (s *Service) RemoveSongFromPlaylist(songId, playlistPubId string, ownerId uint) error { + playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) if err != nil { return err } - playlist, err := s.playlistRepo.GetByConds("public_id = ?", playlistPubId) + _, err = s.playlistOwnerRepo.GetByConds("profile_id = ? AND playlist_id = ?", ownerId, playlist[0].Id) + if err != nil { + return err + } + song, err := s.songRepo.GetByConds("yt_id = ?", songId) if err != nil { return err } diff --git a/services/youtube/download/download.go b/services/youtube/download/download.go index 0cfd273..1775277 100644 --- a/services/youtube/download/download.go +++ b/services/youtube/download/download.go @@ -36,6 +36,10 @@ func (d *Service) DownloadYoutubeSong(req entities.Song) error { return nil } + err = d.DownloadYoutubeSongQueue(req.YtId) + if err != nil { + return err + } resp, err := http.Get(fmt.Sprintf("%s/download/%s", config.Env().YouTube.DownloaderUrl, req.YtId)) if err != nil { return err diff --git a/static/js/player.js b/static/js/player.js index 7914537..f9fb5cd 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -73,6 +73,7 @@ class PlaylistPlayer { * @param {string} songYtIt */ play(songYtIt = "") { + this.setSongNotPlayingStyle(); this.#currentSongIndex = this.#currentPlaylist.songs.findIndex( (song) => song.yt_id === songYtIt, ); @@ -82,9 +83,11 @@ class PlaylistPlayer { const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); } next(shuffle = false, loop = false) { + this.setSongNotPlayingStyle(); if ( !loop && !shuffle && @@ -93,19 +96,19 @@ class PlaylistPlayer { stopMuzikk(); return; } - this.#currentSongIndex = shuffle ? Math.floor(Math.random() * this.#currentPlaylist.songs.length) : loop && this.#currentSongIndex + 1 >= this.#currentPlaylist.songs.length ? 0 : this.#currentSongIndex + 1; - const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); } previous(shuffle = false, loop = false) { + this.setSongNotPlayingStyle(); if (!loop && !shuffle && this.#currentSongIndex - 1 < 0) { stopMuzikk(); return; @@ -115,9 +118,34 @@ class PlaylistPlayer { : loop && this.#currentSongIndex - 1 < 0 ? this.#currentPlaylist.songs.length - 1 : this.#currentSongIndex - 1; + this.setSongNotPlayingStyle(); const songToPlay = this.#currentPlaylist.songs[this.#currentSongIndex]; playNewSong(songToPlay); this.#updateSongPlays(); + this.setSongPlayingStyle(); + } + + removeSong(songYtId) { + const songIndex = this.#currentPlaylist.songs.findIndex( + (song) => song.yt_id === songYtId, + ); + if (songIndex < 0) { + return; + } + this.#currentPlaylist.songs.splice(songIndex, 1); + } + + setSongPlayingStyle() { + document.getElementById( + "song-" + this.#currentPlaylist.songs[this.#currentSongIndex].yt_id, + ).style.backgroundColor = "var(--accent-color-30)"; + } + + setSongNotPlayingStyle() { + for (const song of this.#currentPlaylist.songs) { + document.getElementById("song-" + song.yt_id).style.backgroundColor = + "var(--secondary-color-20)"; + } } /** @@ -157,6 +185,16 @@ class PlaylistPlayer { } } +/** + * @param {string} songYtId + */ +function removeSongFromPlaylist(songYtId) { + if (!currentPlaylistPlayer) { + return; + } + currentPlaylistPlayer.removeSong(songYtId); +} + /** * @param {Playlist} playlist */ @@ -445,4 +483,5 @@ window.Player = { playPlaylist, playSongFromPlaylist, playNewSong, + removeSongFromPlaylist, }; diff --git a/views/components/playlist/new_playlis_popover.templ b/views/components/playlist/new_playlis_popover.templ index 0c1aa28..0f9ad5c 100644 --- a/views/components/playlist/new_playlis_popover.templ +++ b/views/components/playlist/new_playlis_popover.templ @@ -8,7 +8,7 @@ templ NewPlaylistPopover() { templ newPlaylistPopover() {