Creating a Spotify Playlist Management App with AI dev tools
Tags: webdev, ai, programming
Published: 2024-04-23 19:54:17

I've been a loyal user of [mixkeepr](https://mixkeepr.com/), 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.

{% embed https://github.com/Markkop/Multiple-Playlist-deleter %}

This blog post is a chronicle of my journey—how I used [Genie](https://marketplace.visualstudio.com/items?itemName=genieai.chatgpt-vscode) 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](https://github.com/vuongddang/nextjs-tailwindcss-typescript-starter) and sent the `package.json` file to Genie so the AI would know the base project.

```json
{
  "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`.


![Genie demo usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fah8l7eg5v127vr9w7y.png)

---

### ⌨️ Initial code
![Gif](https://media.tenor.com/EywWs4ZME6sAAAAC/bender-pc-control.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:

```json
{
  "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:

```sh
npm install axios spotify-web-api-node
npm install --save-dev @types/axios
```

Create a new file `lib/spotifyAuth.ts` for handling Spotify authentication:

```jsx
// 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:

```jsx
// 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:

```jsx
// 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
![Gif](https://media.tenor.com/qeS6BuiV_08AAAAd/baby-punch.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`:

```jsx
// 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
![Gif](https://media.tenor.com/X7NtT8gA0BcAAAAC/bang-alexandria.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`:

```js
// next.config.js

module.exports = {
  env: {
    NEXT_PUBLIC_CLIENT_ID: process.env.NEXT_PUBLIC_CLIENT_ID,
  },
};
```

3. 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
```

4. Update your `pages/index.tsx` file to use `process.env.NEXT_PUBLIC_CLIENT_ID` instead of `clientId`.

```jsx
// 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
![Gif](https://media.tenor.com/YAI5ev9GmawAAAAd/painting-the-house-zima.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:

```jsx
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
![Gif](https://media.tenor.com/RW7FmId9MPoAAAAd/taking-a-break-rest.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:

```jsx
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
![Gif](https://media.tenor.com/1pj30gX9wAcAAAAC/keyboard-hyperx.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:

```jsx
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
![Gif](https://media.tenor.com/xtXONS0VR3YAAAAC/singe-monkey.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:

```jsx
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
![Gif](https://media.tenor.com/7ns7GV2JmWgAAAAd/ball-dog.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:

```js
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()`:

```js
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:

```js
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
![Gif](https://media.tenor.com/BldKtygdlXgAAAAd/aharen-san-aharen-san-anime.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:

```js
const [isLoading, setIsLoading] = useState(false);
```

Then, update the `loadUserPlaylists()` function to set `isLoading` accordingly:

```js
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):

```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):

```js
import "./App.css";
```

Finally, add the spinner element in your JSX right before the closing fragment tag (`</>`) for the Load Playlists button:

```js
{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
![Gif](https://media.tenor.com/HfV1cXfkt6sAAAAd/hula-hoop-wii-fit.gif)
**Prompt**

> Don't use CSS, use only native tailwind class names and the following SVG:

```html
<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.

1. Create a new file called `Spinner.jsx` in your src folder:

```js
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;
```

2. Import the Spinner component in your main component (e.g., Home.jsx or Home.js):

```js
import Spinner from "./Spinner";
```

3. Replace `{isLoading && <span className="spinner ml-1"></span>}` with `{isLoading && <Spinner />}` inside the button:

```js
<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
![Gif](https://media.tenor.com/RVvnVPK-6dcAAAAC/reload-cat.gif)
**Prompt**

> Load the playlists again after deleting. Answer me the code only.

**Response**

```jsx
async function deleteSelectedPlaylists() {
  for (const playlistId of selectedPlaylists) {
    await spotifyApi.unfollowPlaylist(playlistId);
  }
  await loadUserPlaylists();
}
```

---

### 🔠 Change text
![Gif](https://media.tenor.com/t_e3F0kScTMAAAAd/math-problems-hilarious.gif)
**Prompt**

> Change Load Playlists to "Loading Playlists" when it is loading

**Response**

```js
<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
![Gif](https://media.tenor.com/c7nuZH2Cn50AAAAC/diy-buttons.gif)
**Prompt**

> Change the button colors when disabled

**Response**

```js
<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
![Gif](https://media.tenor.com/r5TU4Mzef30AAAAC/sadness-cliphy.gif)
**Prompt**

> Make it responsive using Tailwind only

**Response**

```jsx
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
![Gif](https://media.tenor.com/sIlGNc-5pooAAAAd/double-thumbs-up-there-you-go.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**

```jsx
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
![Gif](https://media.tenor.com/oH-UzQTbVJ8AAAAd/restore-odd-tinkering.gif)
**Prompt**

> Refactor this to be an array mapped to anchors

**Response**

```jsx


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