I've been a loyal user of mixkeepr, a fantastic tool that aids in saving my weekly music recommendations. But, like any voracious music lover, I found myself engulfed in a sea of playlists—many of which I no longer used or needed.
Here's the kicker: Spotify doesn't offer an easy way to delete multiple playlists at once, which left me with a somewhat chaotic music library.
Rather than treating the issue of mass-deleting unused playlists as just my problem, I saw it as an opportunity to create a solution that could benefit others as well.
With this goal in mind, I decided to build a web app leveraging the power of AI. I am fond of keeping up-to-date with the latest advancements in software development through the use of AI tools.
Markkop / Multiple-Playlist-deleter
A simple app to delete multiple spotify playlists at once.
🎶 Multiple Playlist Deleter
I've been a long-time user of mixkeepr for saving my weekly music recommendations. However, I ended up with a lot of unused playlists and was unable to delete them all at once using Spotify.
To solve this problem, I created a tool with the help of AI to delete multiple playlists easily.
Demo
https://multiple-playlist-deleter-markkop.vercel.app/
Usage
- Click the "Authorize Multiple Playlist Deleter" link to authenticate with your Spotify account.
- After authorization, you'll be redirected back to the app logged in.
- Click the "Load Playlists" button to fetch your playlists.
- Select the playlists you want to delete by clicking on them.
- Click the "Delete Selected Playlists" button to delete the selected playlists.
Prerequisites
Before you begin, ensure you have met the following requirements:
- You have a Spotify account
To run this project on your local machine, you also must have the following installed:
- You have registered a new…
This blog post is a chronicle of my journey—how I used Genie to create a simple web application that made deleting my Spotify playlists a breeze.
Setup
I wanted to use NextJS with Typescript and Tailwind CSS for this project, so I created a new project using a NextJS starter template and sent the package.json
file to Genie so the AI would know the base project.
{
"name": "nextjs-tailwindcss-typescript-starter",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "13.1.2",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4"
}
}
Then I kept asking for the files and features.
Prompts and Responses.
Luckily for me, Genie has a way of exporting the whole conversation.
Note that I removed some of the prompts that didn't work out and also removed the code that was sent with some of my prompts for readability.
The process was essentially selecting the code I wanted to change (sometimes the whole file), right-clicking on it and selecting Genie: Ad-hoc prompt
.
⌨️ Initial code
Prompt
Give me the code for NextJS app with an UI where the user can connect with his spotify account, load his playlists, select one or more and delete them. Assume that the setup is already made. This is the current package.json file:
{
"name": "nextjs-tailwindcss-typescript-starter",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "13.1.2",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4"
}
}
Response
Here's the code for a Next.js app that connects to a user's Spotify account, loads their playlists, lets them select one or more, and delete them.
First, install axios
, @types/axios
, and spotify-web-api-node
if you haven't already:
npm install axios spotify-web-api-node
npm install --save-dev @types/axios
Create a new file lib/spotifyAuth.ts
for handling Spotify authentication:
// lib/spotifyAuth.ts
import { NextApiRequest, NextApiResponse } from "next";
import SpotifyWebApi from "spotify-web-api-node";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
const spotifyApi = new SpotifyWebApi({
clientId: clientId,
clientSecret: clientSecret,
});
export async function getAccessToken(code: string) {
try {
const { body } = await spotifyApi.authorizationCodeGrant(code);
return body;
} catch (err) {
console.error(`Error getting access token: ${err.message}`);
return null;
}
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "POST") {
if (!req.body || !req.body.code) {
res.status(400).json({ error: "Invalid request" });
return;
}
const accessTokenData = await getAccessToken(req.body.code);
if (accessTokenData) {
res.status(200).json({ accessTokenData });
} else {
res.status(500).json({ error: "Failed to get access token" });
}
} else {
res.status(405).json({ error: "Method not allowed" });
}
}
On pages/index.tsx
or another page component:
// pages/index.tsx
import { useState } from "react";
import axios from "axios";
import SpotifyWebApi from "spotify-web-api-node";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const spotifyApi = new SpotifyWebApi({
clientId: clientId,
});
export default function Home() {
const [code, setCode] = useState("");
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [userId, setUserId] = useState("");
const [playlists, setPlaylists] = useState([]);
const [selectedPlaylists, setSelectedPlaylists] = useState(new Set());
async function loginUser() {
if (!code) return;
const response = await axios.post("/api/auth", { code });
const { access_token, refresh_token } = response.data.accessTokenData;
setAccessToken(access_token);
setRefreshToken(refresh_token);
}
async function loadUserPlaylists() {
if (!accessToken) {
return;
}
spotifyApi.setAccessToken(accessToken);
const userData = await spotifyApi.getMe();
const { id } = userData.body;
setUserId(id);
const { body } = await spotifyApi.getUserPlaylists();
setPlaylists(body.items);
}
async function deleteSelectedPlaylists() {
for (const playlistId of selectedPlaylists) {
await spotifyApi.unfollowPlaylist(playlistId);
}
}
function selectPlaylist(id: string, checked: boolean) {
const updatedSelections = new Set(selectedPlaylists);
if (checked) {
updatedSelections.add(id);
} else {
updatedSelections.delete(id);
}
setSelectedPlaylists(updatedSelections);
}
return (
<div>
<h1>Spotify Playlist Deleter</h1>
{accessToken ? (
<>
<button onClick={loadUserPlaylists}>Load Playlists</button>
<button
onClick={deleteSelectedPlaylists}
disabled={!selectedPlaylists.size}
>
Delete Selected Playlists
</button>
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>
<input
type="checkbox"
onChange={(e) =>
selectPlaylist(playlist.id, e.target.checked)
}
/>
{playlist.name}
</li>
))}
</ul>
</>
) : (
<>
<p>Please authorize the app and insert the code:</p>
<input value={code} onChange={(e) => setCode(e.target.value)} />
<button onClick={loginUser}>Log in</button>
</>
)}
</div>
);
}
Make sure to create a pages/api/auth.ts
file for handling Spotify authentication API call:
// pages/api/auth.ts
export { default } from "../../lib/spotifyAuth";
This should give you a working Next.js app that connects to Spotify, loads playlists, allows user selection, and deletes selected playlists. You might want to add CSS or use a UI library like Tailwind CSS for styling.
🔧 Fix authentication flow
Prompt
How the user will obtain the code to input?
Response
My apologies for the confusion. I should have provided a URL to redirect the user for authentication. In the pages/index.tsx
, modify the component to include the authentication URL:
Here's the updated Home
function in pages/index.tsx
:
// pages/index.tsx
// ... (other code) ...
export default function Home() {
// ... (other useState declarations) ...
const authUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(
"http://localhost:3000" // Replace this with your app's callback URL (e.g., https://yourdomain.com)
)}&scope=playlist-read-private%20playlist-modify-public%20playlist-modify-private`;
// ... (other functions) ...
return (
<div>
{/* ... (other JSX elements) ... */}
{accessToken ? (
// ... (JSX elements when accessToken is available) ...
) : (
<>
<p>Please authorize the app:</p>
<a href={authUrl}>Authorize Spotify Playlist Deleter</a>
<p>After authorization, you'll be redirected back to this page with a code parameter in the URL. Copy and paste that code below:</p>
<input value={code} onChange={(e) => setCode(e.target.value)} />
<button onClick={loginUser}>Log in</button>
</>
)}
</div>
);
}
Now, when the user clicks on the "Authorize Spotify Playlist Deleter" link, they will be redirected to the Spotify authentication page. After authorizing the app, the user will be redirected back to your app with a "code" parameter in the URL. The user can then copy and paste the code into the input field to log in.
Note that this implementation requires the user to manually copy and paste the code from the URL. You can further enhance the user experience by automating this process, e.g., parsing URL parameters when the component mounts and automatically sending the code to log in if it's present.
🔮 Guided fix on env. variable issue
Client ID is undefined, create a next config file to make the env. var available on the front end
Response
To make the environment variable available on the frontend, you'll need to create a Next.js configuration file named next.config.js
in your project's root directory.
Here's how you can create and configure the file:
- Create a new file named
next.config.js
in your project's root directory. - Add the following content to
next.config.js
:
// next.config.js
module.exports = {
env: {
NEXT_PUBLIC_CLIENT_ID: process.env.NEXT_PUBLIC_CLIENT_ID,
},
};
- Now, prefix your environment variable with
NEXT_PUBLIC_
. In your.env.local
file, changeCLIENT_ID
toNEXT_PUBLIC_CLIENT_ID
.
# .env.local
NEXT_PUBLIC_CLIENT_ID=your_client_id_here
- Update your
pages/index.tsx
file to useprocess.env.NEXT_PUBLIC_CLIENT_ID
instead ofclientId
.
// pages/index.tsx
// ... (other code) ...
const authUrl = `https://accounts.spotify.com/authorize?client_id=${
process.env.NEXT_PUBLIC_CLIENT_ID
}&response_type=code&redirect_uri=${encodeURIComponent(
"http://localhost:3000" // Replace this with your app's callback URL (e.g., https://yourdomain.com)
)}&scope=playlist-read-private%20playlist-modify-public%20playlist-modify-private`;
// ... (other code) ...
With these changes, your client ID will be accessible from the frontend as an environment variable. Make sure to restart your development server after making these changes for them to take effect.
🎨 Style with Tailwind
Make a simple, but beautiful interface for it using Tailwind.
Response
I have modified the code to include Tailwind CSS classes for a simple and beautiful interface:
import { useState } from "react";
import axios from "axios";
import SpotifyWebApi from "spotify-web-api-node";
import "tailwindcss/tailwind.css";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const spotifyApi = new SpotifyWebApi({
clientId: clientId,
});
export default function Home() {
const [code, setCode] = useState("");
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [userId, setUserId] = useState("");
const [playlists, setPlaylists] = useState([]);
const [selectedPlaylists, setSelectedPlaylists] = useState(new Set());
const authUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(
"http://localhost:3000/"
)}&scope=playlist-read-private%20playlist-modify-public%20playlist-modify-private`;
async function loginUser() {
if (!code) return;
const response = await axios.post("/api/auth", { code });
const { access_token, refresh_token } = response.data.accessTokenData;
setAccessToken(access_token);
setRefreshToken(refresh_token);
}
async function loadUserPlaylists() {
if (!accessToken) {
return;
}
spotifyApi.setAccessToken(accessToken);
const userData = await spotifyApi.getMe();
const { id } = userData.body;
setUserId(id);
const { body } = await spotifyApi.getUserPlaylists();
setPlaylists(body.items);
}
async function deleteSelectedPlaylists() {
for (const playlistId of selectedPlaylists) {
await spotifyApi.unfollowPlaylist(playlistId);
}
}
function selectPlaylist(id: string, checked: boolean) {
const updatedSelections = new Set(selectedPlaylists);
if (checked) {
updatedSelections.add(id);
} else {
updatedSelections.delete(id);
}
setSelectedPlaylists(updatedSelections);
}
return (
<div className="bg-gray-100 min-h-screen p-8">
<div className="container mx-auto">
<h1 className="text-4xl font-bold mb-6">Spotify Playlist Deleter</h1>
{accessToken ? (
<>
<button
onClick={loadUserPlaylists}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded mr-2"
>
Load Playlists
</button>
<button
onClick={deleteSelectedPlaylists}
disabled={!selectedPlaylists.size}
className={`${
!selectedPlaylists.size ? "opacity-50" : ""
} bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded`}
>
Delete Selected Playlists
</button>
<ul className="mt-4 space-y-2">
{playlists.map((playlist) => (
<li key={playlist.id} className="flex items-center">
<input
type="checkbox"
onChange={(e) =>
selectPlaylist(playlist.id, e.target.checked)
}
className="mr-2"
/>
<span className="text-xl">{playlist.name}</span>
</li>
))}
</ul>
</>
) : (
<>
<p className="mb-2">Please authorize the app:</p>
<a
href={authUrl}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded inline-block mb-4"
>
Authorize Spotify Playlist Deleter
</a>
<p className="mb-2">
After authorization, you'll be redirected back to this page with a
code parameter in the URL. Copy and paste that code below:
</p>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
className="border border-gray-300 p-2 rounded w-full mb-2"
/>
<button
onClick={loginUser}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded w-full"
>
Log in
</button>
</>
)}
</div>
</div>
);
}
This code incorporates Tailwind CSS classes to style the interface. You need to have Tailwind CSS installed in your project in order for the styles to work correctly.
⚙️ Automatic spotify authorization
Change the code so the user doesn't need to copy and paste the code from the URL after authorization.
Response
In order to avoid having the user manually copy and paste the code from the URL, you can use useEffect
hook to extract the code from the URL and log in automatically when the component is mounted.
Here's how you can modify your code:
import { useState, useEffect } from "react";
import axios from "axios";
import SpotifyWebApi from "spotify-web-api-node";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const redirectUri = process.env.SPOTIFY_REDIRECT_URI;
const spotifyApi = new SpotifyWebApi({
clientId: clientId,
});
export default function Home() {
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [userId, setUserId] = useState("");
const [playlists, setPlaylists] = useState([]);
const [selectedPlaylists, setSelectedPlaylists] = useState(new Set());
const authUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(
redirectUri
)}&scope=playlist-read-private%20playlist-modify-public%20playlist-modify-private`;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
loginUser(code);
}
}, []);
async function loginUser(code: string) {
if (!code) return;
const response = await axios.post("/api/auth", { code });
const { access_token, refresh_token } = response.data.accessTokenData;
setAccessToken(access_token);
setRefreshToken(refresh_token);
}
// ...rest of your code
}
In this modified code, I've added useEffect
and removed the manual input for the code. The useEffect
hook checks for the code in the URL upon component mount and calls loginUser
when a valid code is found. This way, the user won't need to copy and paste the code manually.
🔄 Refresh page error fix
It works, but if I refresh the page with the code in the URL, I get the error:
"Details: invalid_grant Invalid authorization code."
Response
That error occurs because the authorization code has already been used to request an access token, and it's no longer valid. To fix this, you can remove the code from the URL after using it, so that refreshing the page won't cause the issue.
Here's how you can modify your useEffect
hook to remove the code from the URL after using it:
import { useState, useEffect } from "react";
// ...other imports
export default function Home() {
// ...other state variables
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
loginUser(code);
// Update the URL without the 'code' parameter
window.history.pushState({}, "", window.location.pathname);
}
}, []);
// ...rest of your code
}
In this modified code, I've added a line window.history.pushState({}, "", window.location.pathname);
inside the useEffect
hook to update the URL without the 'code' parameter after calling loginUser
. This way, refreshing the page won't attempt to use the same code again, avoiding the error.
🏷️ Label clicking
Make the checkboxes selectable when clicking on the playlist names too
Response
To make the checkboxes selectable when clicking on the playlist names, you can wrap both the input and the span elements in a label. The label will automatically associate the text with the checkbox, and clicking on the text will toggle the checkbox. Here's the modified code:
return (
<div className="bg-gray-100 min-h-screen p-8">
<div className="container mx-auto">
<h1 className="text-4xl font-bold mb-6">Spotify Playlist Deleter</h1>
{accessToken ? (
<>
<button
onClick={loadUserPlaylists}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded mr-2"
>
Load Playlists
</button>
<button
onClick={deleteSelectedPlaylists}
disabled={!selectedPlaylists.size}
className={`${
!selectedPlaylists.size ? "opacity-50" : ""
} bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded`}
>
Delete Selected Playlists
</button>
<ul className="mt-4 space-y-2">
{playlists.map((playlist) => (
<li key={playlist.id} className="flex items-center">
<label>
<input
type="checkbox"
onChange={(e) =>
selectPlaylist(playlist.id, e.target.checked)
}
className="mr-2"
/>
<span className="text-xl">{playlist.name}</span>
</label>
</li>
))}
</ul>
</>
) : (
<>
<p className="mb-2">Please authorize the app:</p>
<a
href={authUrl}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded inline-block mb-4"
>
Authorize Spotify Playlist Deleter
</a>
</>
)}
</div>
</div>
);
Now, clicking on the playlist names will also toggle the associated checkboxes.
🎶 Load all playlists
Load all user playlists, not only the first 20
Response
To load all user playlists, not just the first 20, you'll need to make use of the offset
and limit
parameters when calling getUserPlaylists()
. You can create a function to fetch all playlists recursively:
async function fetchAllPlaylists(offset = 0, limit = 50) {
const { body } = await spotifyApi.getUserPlaylists({ offset, limit });
if (body.next) {
return [...body.items, ...(await fetchAllPlaylists(offset + limit))];
}
return body.items;
}
Then, modify the loadUserPlaylists()
function to use the newly created fetchAllPlaylists()
function instead of directly calling spotifyApi.getUserPlaylists()
:
async function loadUserPlaylists() {
if (!accessToken) {
return;
}
spotifyApi.setAccessToken(accessToken);
const userData = await spotifyApi.getMe();
const { id } = userData.body;
setUserId(id);
const allPlaylistsItems = await fetchAllPlaylists();
setPlaylists(allPlaylistsItems);
}
Here's the complete updated Home()
component:
import { useState, useEffect } from "react";
import axios from "axios";
import SpotifyWebApi from "spotify-web-api-node";
const clientId = process.env.SPOTIFY_CLIENT_ID;
const redirectUri = process.env.SPOTIFY_REDIRECT_URI;
const spotifyApi = new SpotifyWebApi({
clientId: clientId,
});
export default function Home() {
// ... existing state variables
// ... authUrl and useEffects are unchanged
async function loginUser(code: string) {
// ... no changes here
}
async function fetchAllPlaylists(offset = 0, limit = 50) {
const { body } = await spotifyApi.getUserPlaylists({ offset, limit });
if (body.next) {
return [...body.items, ...(await fetchAllPlaylists(offset + limit))];
}
return body.items;
}
async function loadUserPlaylists() {
if (!accessToken) {
return;
}
spotifyApi.setAccessToken(accessToken);
const userData = await spotifyApi.getMe();
const { id } = userData.body;
setUserId(id);
const allPlaylistsItems = await fetchAllPlaylists();
setPlaylists(allPlaylistsItems);
}
// ... rest of the functions and JSX are unchanged
}
Now, when you call loadUserPlaylists()
, it will fetch all user playlists without being limited to just the first 20.
⏳ Loading spinner
Add a loading spinner
Response
To add a loading spinner, you will need to add a new state variable, isLoading
, that is set to true when you start loading playlists and set back to false once the loading has completed. You can use this state variable to conditionally render a spinner.
First, add the isLoading
state:
const [isLoading, setIsLoading] = useState(false);
Then, update the loadUserPlaylists()
function to set isLoading
accordingly:
async function loadUserPlaylists() {
if (!accessToken) {
return;
}
setIsLoading(true);
spotifyApi.setAccessToken(accessToken);
// ... rest of the function remains unchanged
const allPlaylistsItems = await fetchAllPlaylists();
setPlaylists(allPlaylistsItems);
setIsLoading(false);
}
Now, add the spinner as a conditional element in your JSX. You can use a simple CSS spinner like the one below, or replace it with any other spinner component or library of your choice.
Add this CSS to your project (e.g. src/App.css):
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Then, import the CSS file inside your component (e.g., at the top of Home.jsx or Home.js):
import "./App.css";
Finally, add the spinner element in your JSX right before the closing fragment tag (</>
) for the Load Playlists button:
{accessToken ? (
<>
<button
onClick={loadUserPlaylists}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded mr-2"
>
Load Playlists {isLoading && <span className="spinner ml-1"></span>}
</button>
{/* Rest of the JSX... */}
</>
) : (
// ...
)}
Now, when you click on the "Load Playlists" button, a loading spinner will be displayed next to the button while the playlists are being fetched. Once the loading has completed, the spinner will disappear.
✨⏳ Tailwind spinner
Don't use CSS, use only native tailwind class names and the following SVG:
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Note: don't use this SVG directly in the JSX, import it from somewhere else.
Response
To achieve this, you can extract the SVG into a new file and import it as a React component.
- Create a new file called
Spinner.jsx
in your src folder:
import React from "react";
const Spinner = () => (
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
export default Spinner;
- Import the Spinner component in your main component (e.g., Home.jsx or Home.js):
import Spinner from "./Spinner";
- Replace
{isLoading && <span className="spinner ml-1"></span>}
with{isLoading && <Spinner />}
inside the button:
<button
onClick={loadUserPlaylists}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded mr-2"
>
Load Playlists {isLoading && <Spinner />}
</button>
Now, the spinner component will show up next to the "Load Playlists" button when loading playlists, using the provided Tailwind CSS classes and without requiring any additional CSS.
🔄 Reload playlists
Load the playlists again after deleting. Answer me the code only.
Response
async function deleteSelectedPlaylists() {
for (const playlistId of selectedPlaylists) {
await spotifyApi.unfollowPlaylist(playlistId);
}
await loadUserPlaylists();
}
🔠 Change text
Change Load Playlists to "Loading Playlists" when it is loading
Response
<button
onClick={loadUserPlaylists}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded mr-2 flex gap-4"
disabled={isLoading}
>
{isLoading ? "Loading Playlists" : "Load Playlists"}{" "}
{isLoading && <Spinner />}
</button>
💅 Button colors
Change the button colors when disabled
Response
<button
onClick={loadUserPlaylists}
className={`text-white px-4 py-2 rounded mr-2 flex gap-4 ${
isLoading
? "bg-green-400 cursor-not-allowed"
: "bg-green-500 hover:bg-green-600"
}`}
disabled={isLoading}
>
{isLoading ? "Loading Playlists" : "Load Playlists"}{" "}
{isLoading && <Spinner />}
</button>
📱 Responsiveness
Make it responsive using Tailwind only
Response
import { useState } from "react";
import Image from "next/image";
function App() {
// ... other code
return (
<div className="bg-gray-100 min-h-screen p-8">
<div className="container mx-auto max-w-screen-lg">
<div className="flex gap-2 mb-2 md:gap-4">
<Image
src="/logo.png"
alt="Logo"
width={50}
height={50}
className="w-12 h-12 md:w-14 md:h-14"
/>
<h1 className="text-4xl font-bold my-auto md:text-5xl">
Spotify Playlist Deleter
</h1>
</div>
{accessToken ? (
<>
<div className="flex flex-wrap gap-2">
<button
onClick={loadUserPlaylists}
className={`text-white px-4 py-2 rounded flex gap-4 ${
isLoading
? "bg-gray-500 cursor-not-allowed"
: "bg-green-500 hover:bg-green-600"
}`}
disabled={isLoading}
>
{isLoading ? "Loading Playlists" : "Load Playlists"}{" "}
{isLoading && <Spinner />}
</button>
<button
onClick={deleteSelectedPlaylists}
disabled={!selectedPlaylists.size}
className={`${
!selectedPlaylists.size ? "opacity-50" : ""
} bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded`}
>
Delete Selected Playlists
</button>
</div>
<ul className="mt-4 space-y-2">
{playlists.map((playlist) => (
<li key={playlist.id} className="flex items-center">
<label>
<input
type="checkbox"
onChange={(e) =>
selectPlaylist(playlist.id, e.target.checked)
}
className="mr-2"
/>
<span className="text-xl">{playlist.name}</span>
</label>
</li>
))}
</ul>
</>
) : (
<>
<p className="mb-2">Please authorize the app:</p>
<a
href={authUrl}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded inline-block mb-4"
>
Authorize Spotify Playlist Deleter
</a>
</>
)}
</div>
</div>
);
}
export default App;
🔗 Social media profiles
Add a github, twitter and linkedin icons on the bottom of the page that links to my social media profiles. Make them responsive.
Response
import { useState } from "react";
import Image from "next/image";
function App() {
// ... other code
return (
<div className="bg-gray-100 min-h-screen p-8 flex flex-col">
<div className="container mx-auto max-w-screen-lg flex-grow">
{/* ... rest of the content */}
{accessToken ? (
// ...
) : (
// ...
)}
</div>
<footer className="mt-8">
<div className="container mx-auto max-w-screen-lg flex justify-center items-center gap-4 md:gap-6">
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer">
<Image src="/icons/github.svg" alt="GitHub icon" width={24} height={24} />
</a>
<a href="https://twitter.com/yourusername" target="_blank" rel="noopener noreferrer">
<Image src="/icons/twitter.svg" alt="Twitter icon" width={24} height={24} />
</a>
<a href="https://www.linkedin.com/in/yourusername/" target="_blank" rel="noopener noreferrer">
<Image src="/icons/linkedin.svg" alt="LinkedIn icon" width={24} height={24} />
</a>
</div>
</footer>
</div>
);
}
export default App;
Replace yourusername
with your actual social media account names.
🔨 Refactor social media profiles
Refactor this to be an array mapped to anchors
Response
const icons = [
{
href: "https://github.com/MarkKop",
src: "/icons/github.svg",
alt: "GitHub icon",
},
{
href: "https://twitter.com/heyMarkKop",
src: "/icons/twitter.svg",
alt: "Twitter icon",
},
{
href: "https://www.linkedin.com/in/marcelo-kopmann",
src: "/icons/linkedin.svg",
alt: "LinkedIn icon",
},
];
return (
<div className="container mx-auto max-w-screen-lg flex justify-center items-center gap-4 md:gap-6">
{icons.map(({ href, src, alt }) => (
<a href={href} target="_blank" rel="noopener noreferrer" key={href}>
<Image src={src} alt={alt} width={24} height={24} />
</a>
))}
</div>
);
Disclosure: this blog post was partially restructured with the assistance of AI
Top comments (0)