DEV Community

Cover image for Creating a Spotify Playlist Management App with AI dev tools
Mark Kop
Mark Kop

Posted on

Creating a Spotify Playlist Management App with AI dev tools

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.

GitHub logo Markkop / Multiple-Playlist-deleter

A simple app to delete multiple spotify playlists at once.

🎶 Multiple Playlist Deleter

Demo image

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

  1. Click the "Authorize Multiple Playlist Deleter" link to authenticate with your Spotify account.
  2. After authorization, you'll be redirected back to the app logged in.
  3. Click the "Load Playlists" button to fetch your playlists.
  4. Select the playlists you want to delete by clicking on them.
  5. 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Genie demo usage


⌨️ Initial code

Gif

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" });
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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

Gif

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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:

  1. Create a new file named next.config.js in your project's root directory.
  2. Add the following content to next.config.js:
// next.config.js

module.exports = {
  env: {
    NEXT_PUBLIC_CLIENT_ID: process.env.NEXT_PUBLIC_CLIENT_ID,
  },
};
Enter fullscreen mode Exit fullscreen mode
  1. Now, prefix your environment variable with NEXT_PUBLIC_. In your .env.local file, change CLIENT_ID to NEXT_PUBLIC_CLIENT_ID.
# .env.local

NEXT_PUBLIC_CLIENT_ID=your_client_id_here
Enter fullscreen mode Exit fullscreen mode
  1. Update your pages/index.tsx file to use process.env.NEXT_PUBLIC_CLIENT_ID instead of clientId.
// 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) ...
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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

}
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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
}
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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>
);
Enter fullscreen mode Exit fullscreen mode

Now, clicking on the playlist names will also toggle the associated checkboxes.


🎶 Load all playlists

Gif
Prompt

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Now, when you call loadUserPlaylists(), it will fetch all user playlists without being limited to just the first 20.


⏳ Loading spinner

Gif
Prompt

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, import the CSS file inside your component (e.g., at the top of Home.jsx or Home.js):

import "./App.css";
Enter fullscreen mode Exit fullscreen mode

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... */}
  </>
) : (
  // ...
)}
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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>
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. Import the Spinner component in your main component (e.g., Home.jsx or Home.js):
import Spinner from "./Spinner";
Enter fullscreen mode Exit fullscreen mode
  1. 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>
Enter fullscreen mode Exit fullscreen mode

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

Gif
Prompt

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();
}
Enter fullscreen mode Exit fullscreen mode

🔠 Change text

Gif
Prompt

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>
Enter fullscreen mode Exit fullscreen mode

💅 Button colors

Gif
Prompt

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>
Enter fullscreen mode Exit fullscreen mode

📱 Responsiveness

Gif
Prompt

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;
Enter fullscreen mode Exit fullscreen mode

🔗 Social media profiles

Gif
Prompt

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;
Enter fullscreen mode Exit fullscreen mode

Replace yourusername with your actual social media account names.


🔨 Refactor social media profiles

Gif
Prompt

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>
);
Enter fullscreen mode Exit fullscreen mode

Disclosure: this blog post was partially restructured with the assistance of AI

Top comments (0)