Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Playlist #52

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a0416cb
feat: playlist module
Itsamirmasoud Oct 29, 2023
c28490a
feat: playlist model
Itsamirmasoud Oct 29, 2023
cc60b8c
feat: playlist model with custom intermediary table
Itsamirmasoud Nov 26, 2023
a3be390
cleanup: removed dead code
Itsamirmasoud Nov 26, 2023
93feadd
feat: add track - remove track - track order on playlist
Itsamirmasoud Nov 26, 2023
4178a12
feat: adjust playlist order when adding/removing track
Itsamirmasoud Nov 26, 2023
bdabb55
feat: Playlist and PlaylistTrack serializers
Itsamirmasoud Nov 26, 2023
e457094
feat: custom validation for Playlist order and name
Itsamirmasoud Nov 26, 2023
30dd36c
feat: added PlaylistViewSet with custom implementation
Itsamirmasoud Nov 26, 2023
f277885
feat: route registration for PlaylistViewSet
Itsamirmasoud Nov 26, 2023
3feae18
misc: playlist migration
Itsamirmasoud Nov 26, 2023
3e4f295
test: initial tests for playlist
Itsamirmasoud Nov 26, 2023
142d252
test: test for Playlist serializer validation rules
Itsamirmasoud Nov 26, 2023
9072657
feat: validation to prevent duplicate playlist name
Itsamirmasoud Nov 27, 2023
318ea97
feat: basic representation of the Playlist - create Playlist
Itsamirmasoud Nov 27, 2023
14357e7
feat: update App.js to display playlists
Itsamirmasoud Nov 27, 2023
82ccdad
feat: destroy action for deleting playlists
Itsamirmasoud Nov 28, 2023
e831078
feat: validation rule to prevent creating playlists with duplicate name
Itsamirmasoud Nov 28, 2023
d0e98f5
feat: Playlist viewset permission
Itsamirmasoud Nov 29, 2023
56e9df0
feat: user-playlist relationship
Itsamirmasoud Dec 1, 2023
9fa9fcf
feat: new playlist migration script with a required user
Itsamirmasoud Dec 1, 2023
8b9e6f5
test: updated playlist tests to support user relation
Itsamirmasoud Dec 1, 2023
acbd3fc
misc: migration script to add a seed test user
Itsamirmasoud Dec 4, 2023
cce03ba
misc: migration script to add seed playlists
Itsamirmasoud Dec 4, 2023
038ce8f
misc: restored db settings to match docker-compose
Itsamirmasoud Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 122 additions & 25 deletions client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,141 @@
// App.js
import React, { useState, useEffect } from "react";
import styles from "./App.module.css";
import logo from "./assets/logo.svg";

import TrackRow from "./components/TrackRow";
import PlaylistRow from "./components/PlaylistRow";
import AudioPlayer from "./components/AudioPlayer";

function App() {
const [activeTab, setActiveTab] = useState("tracks");
const [tracks, setTracks] = useState([]);
const [currentTrack, setCurrentTrack] = useState();
const [playlists, setPlaylists] = useState([]);
const [currentTrack, setCurrentTrack] = useState("");
const [newPlaylistName, setNewPlaylistName] = useState("");

const fetchPlaylists = () => {
fetch("http://localhost:8000/playlists/", { mode: "cors" })
.then((res) => res.json())
.then((data) => setPlaylists(data))
.catch((error) => console.error("Error fetching playlists:", error));
};

useEffect(() => {
fetch("http://0.0.0.0:8000/tracks/", { mode: "cors" })
.then((res) => res.json())
.then((data) => setTracks(data));
// Fetch tracks
fetch("http://localhost:8000/tracks/", { mode: "cors" })
.then((res) => res.json())
.then((data) => setTracks(data));

// Fetch playlists
fetchPlaylists();
}, []);

const handlePlay = (track) => setCurrentTrack(track);

const handleAddPlaylist = () => {
fetch("http://localhost:8000/playlists/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: newPlaylistName }),
})
.then((res) => {
if (res.ok) {
// Clear the input field
setNewPlaylistName("");
// Fetch playlists again to update the state
fetchPlaylists();
} else {
throw new Error("Failed to add playlist");
}
})
.catch((error) => console.error("Error adding playlist:", error));
};

const renderContent = () => {
if (activeTab === "tracks") {
return (
<>
<h2>Tracks</h2>
{tracks.map((track, ix) => (
<TrackRow key={ix} track={track} handlePlay={handlePlay} />
))}
</>
);
} else if (activeTab === "playlists") {
return (
<>
<h2>Playlists</h2>
<div className={styles.addPlaylist}>
<input
type="text"
placeholder="Enter playlist name"
value={newPlaylistName}
onChange={(e) => setNewPlaylistName(e.target.value)}
className={styles.playlistInput}
/>
<button onClick={handleAddPlaylist} className={styles.playlistButton}>
Add Playlist
</button>
</div>
{playlists.map((playlist, ix) => (
<PlaylistRow key={ix} playlist={playlist} handleDeletePlaylist={handleDeletePlaylist} />
))}
</>
);
}
return null;
};

const handleDeletePlaylist = (playlistId) => {
fetch(`http://localhost:8000/playlists/${playlistId}/`, {
method: "DELETE",
})
.then((res) => {
if (res.ok) {
// Fetch playlists again to update the state
fetchPlaylists();
} else {
throw new Error("Failed to delete playlist");
}
})
.catch((error) => console.error("Error deleting playlist:", error));
};

return (
<>
<main className={styles.app}>
<nav>
<img src={logo} className={styles.logo} alt="Logo" />
<ul className={styles.menu}>
<li>
<a href="#" className={styles.active}>
Tracks
</a>
</li>
<li>
<a href="#">Playlists</a>
</li>
</ul>
</nav>
{tracks.map((track, ix) => (
<TrackRow key={ix} track={track} handlePlay={handlePlay} />
))}
</main>
{currentTrack && <AudioPlayer track={currentTrack} />}
</>
<>
<main className={styles.app}>
<nav>
<img src={logo} className={styles.logo} alt="Logo" />
<ul className={styles.menu}>
<li>
<a
href="#"
className={activeTab === "tracks" ? styles.active : ""}
onClick={() => setActiveTab("tracks")}
>
Tracks
</a>
</li>
<li>
<a
href="#"
className={activeTab === "playlists" ? styles.active : ""}
onClick={() => setActiveTab("playlists")}
>
Playlists
</a>
</li>
</ul>
</nav>
<section>
{renderContent()}
</section>
</main>
{currentTrack && <AudioPlayer track={currentTrack} />}
</>
);
}

Expand Down
30 changes: 30 additions & 0 deletions client/src/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,33 @@
color: #fff;
border-bottom: 2px solid #009de0;
}

.addPlaylist {
margin-top: 16px;
margin-bottom: 16px; /* Add margin-bottom */
display: flex;
align-items: center;
}

.addPlaylist input {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 8px;
max-width: 300px; /* Limit the width */
}

.addPlaylist button {
background-color: #4caf50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
max-width: 120px; /* Limit the width */
}

.addPlaylist button:hover {
background-color: #45a049;
}
44 changes: 44 additions & 0 deletions client/src/components/PlaylistRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// PlaylistRow.js
import React, { useState } from "react";
import styles from "./PlaylistRow.module.css";
import TrackRow from "./TrackRow";

const PlaylistRow = ({ playlist, handleDeletePlaylist }) => {
const [expanded, setExpanded] = useState(false);

const handleDelete = () => {
if (window.confirm("Are you sure you want to delete this playlist?")) {
handleDeletePlaylist(playlist.id);
}
};

const toggleExpansion = () => {
setExpanded(!expanded);
};

return (
<div className={styles.playlistRow} onClick={toggleExpansion}>
<div className={styles.playlistInfo}>
<h3 className={styles.playlistTitle}>{playlist.name}</h3>
<p>{`${playlist.tracks.length} tracks`}</p>
{/* You can add more details or styles for the playlistInfo */}
</div>
<div className={styles.playlistActions}>
<button onClick={handleDelete} className={styles.deleteButton}>
Delete
</button>
</div>

{expanded && (
<div className={styles.trackList}>
{/* Display the list of tracks using the TrackRow component */}
{playlist.tracks.map((track, index) => (
<TrackRow key={index} track={track} />
))}
</div>
)}
</div>
);
};

export default PlaylistRow;
38 changes: 38 additions & 0 deletions client/src/components/PlaylistRow.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* PlaylistRow.module.css */
.playlistRow {
display: flex;
align-items: center;
height: 5rem;
border-bottom: 1px solid #333;
}

.playlistRow:last-child {
border-bottom: none;
}

/* Adjusted class names to match trackRow styles */
.playlistTitle {
font-weight: 600;
}

.playlistArtist {
color: #767676;
}

.playlistActions {
margin-left: 1rem; /* Add margin to the left of delete button */
}

.deleteButton {
background-color: #ff5555; /* Make the delete button red */
color: #fff;
padding: 5px 10px;
border: none;
cursor: pointer;
}

.trackList {
margin-top: 10px;
padding-left: 20px;
border-left: 2px solid #333;
}
5 changes: 0 additions & 5 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4627,11 +4627,6 @@
"resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
"version" "1.0.0"

"fsevents@^2.3.2", "fsevents@~2.3.2":
"integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="
"resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
"version" "2.3.2"

"function-bind@^1.1.1":
"integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.1.5 on 2023-11-30 15:32

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0002_fetch_data'),
]

operations = [
migrations.CreateModel(
name='Playlist',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250)),
('created_date', models.DateField(auto_now_add=True)),
('last_updated_date', models.DateField(auto_now=True)),
],
),
migrations.CreateModel(
name='PlaylistTrack',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveSmallIntegerField(default=1)),
('playlist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.playlist')),
('track', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.track')),
],
options={
'db_table': 'api_playlist_tracks',
'unique_together': {('track', 'playlist')},
},
),
migrations.AddField(
model_name='playlist',
name='tracks',
field=models.ManyToManyField(blank=True, related_name='playlists', through='api.PlaylistTrack', to='api.track'),
),
migrations.AddField(
model_name='playlist',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='playlists', to=settings.AUTH_USER_MODEL),
),
]
17 changes: 17 additions & 0 deletions service/api/migrations/0004_test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.5 on 2023-12-04 14:48
from django.db import migrations


def add_test_user(apps, schema_editor):
User = apps.get_model('auth', 'User')
User.objects.create_user(username='test', password='test', is_superuser=True, email='[email protected]', is_active=True)


class Migration(migrations.Migration):
dependencies = [
('api', '0003_playlist_playlisttrack_playlist_tracks_playlist_user'),
]

operations = [
migrations.RunPython(add_test_user),
]
32 changes: 32 additions & 0 deletions service/api/migrations/0005_test_playlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.1.5 on 2023-12-04 14:59

from django.db import migrations


def add_test_playlists(apps, schema_editor):
Playlist = apps.get_model('api', 'Playlist')
PlaylistTrack = apps.get_model('api', 'PlaylistTrack')
User = apps.get_model('auth', 'User')
test_user = User.objects.get(username='test')

playlists_data = [
{'name': 'Playlist 1', 'user': test_user, 'tracks': ["FKYVlOXV8Q", "ZkuGOyOiiE", "em55KruCAt"]},
{'name': 'Playlist 2', 'user': test_user, 'tracks': ["mX542l3F2Q", "IFZpDGwLZD", "b1Oq8Scoei"]},
{'name': 'Playlist 3', 'user': test_user, 'tracks': ["RZQbWVB8XQ", "Ru3QrB13Uf", "Uec7QGEctL"]},
]

for playlist_data in playlists_data:
playlist = Playlist.objects.create(name=playlist_data['name'], user=playlist_data['user'])

for index, track_id in enumerate(playlist_data['tracks']):
PlaylistTrack.objects.create(playlist=playlist, track_id=track_id, order=index + 1)


class Migration(migrations.Migration):
dependencies = [
('api', '0004_test_user'),
]

operations = [
migrations.RunPython(add_test_playlists),
]
Loading