Search code examples
javascriptnext.jstailwind-cssjsx

How to show full text as container width increases in tailwind and next.js


I have a feature where a user can create an album and assign it a name, these names can be of any length. When the user creates an album it creates a div, as well as a link (the albums name) and 2 icons. I want to make it so that if the user enters in a name that is longer than the width of the container, then the name they entered will show as much of the name as it can and just before it reaches the edge of the container, the name is rendered with a ... at the end. But the size of these containers can increase and decrease depending on the users screen size and if they make the size smaller or bigger. With the implementation I have right now it renders long album names with ... even if more text can fit within the container, not sure how to fix this.

page.js

'use client';

import { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashCan, faPenToSquare } from '@fortawesome/free-solid-svg-icons';
import Link from 'next/link';
import axios from 'axios';
import Header from '@/components/Header';

export default function AlbumsPage() {
  const [albumName, setAlbumName] = useState('');
  const [albums, setAlbums] = useState([]);
  const [showModal, setShowModal] = useState(false);
  const [deleteAlbumName, setDeleteAlbum] = useState('');
  const [editMode, setEditMode] = useState(false);
  const [editAlbumName, setEditAlbumName] = useState('');
  const [errorMessage, setErrorMessage] = useState('');
  const [oldAlbumName, setOldAlbumName] = useState('');

  useEffect(() => {
    fetchAlbums();
  }, []);

  const fetchAlbums = async () => {
    try {
      const response = await axios.get('/api/users/getUserAlbums');
      setAlbums(response.data.albums);
    } catch (error) {
      console.log(error);
    }
  };

  const handleCreateAlbum = () => {
    setShowModal(true);
    setErrorMessage('');
  };

  const handleModalInputChange = (e) => {
    setAlbumName(e.target.value);
    setErrorMessage('');
  };

  const handleModalClose = () => {
    setShowModal(false);
    setAlbumName('');
    setEditAlbumName('');
    setDeleteAlbum('');
    setEditMode(false);
    setErrorMessage('');
  };

  const handleModalSubmit = async () => {
    const trimmedAlbumName = albumName.trim();
    if (!trimmedAlbumName) {
      setErrorMessage('Album name cannot be empty');
      return;
    }
    if (trimmedAlbumName[0] === ' ') {
      setErrorMessage('Album name cannot start with a space');
      return;
    }
    try {
      const response = await axios.post('/api/users/createAlbums', { albumName: trimmedAlbumName });
      setAlbums(prevAlbums => [...prevAlbums, { albumName: trimmedAlbumName }]);
      setShowModal(false);
      setAlbumName('');
    } catch (error) {
      if (error.response) {
        if (error.response.status === 400) {
          setErrorMessage('Album already exists');
        } else {
          setErrorMessage('Failed to create album');
        }
      } else {
        setErrorMessage('Network error. Please try again.');
      }
    }
  };

  const handleDeleteAlbum = async (albumName) => {
    setDeleteAlbum(albumName);
    setShowModal(true);
  };

  const confirmDeleteAlbum = async () => {
    if (deleteAlbumName) {
      try {
        const response = await axios.delete(`/api/users/deleteAlbums?albumName=${deleteAlbumName}`);
        setShowModal(false);
        setDeleteAlbum("");
        await fetchAlbums();
      } catch (error) {
        console.log("Failed to delete album", error);
      }
    }
  };

  const handleEditAlbum = (albumName) => {
    setOldAlbumName(albumName);
    setEditAlbumName('');
    setEditMode(true);
    setShowModal(true);
    setErrorMessage('');
  };

  const handleEditInputChange = (e) => {
    setEditAlbumName(e.target.value);
    setErrorMessage('');
  };

  const handleEditModalSubmit = async () => {
    const trimmedEditAlbumName = editAlbumName.trim();
    if (!trimmedEditAlbumName) {
      setErrorMessage('Album name cannot be empty');
      return;
    }
    if (trimmedEditAlbumName[0] === ' ') {
      setErrorMessage('Album name cannot start with a space');
      return;
    }
    const existingAlbum = albums.find(album => album.albumName.toLowerCase() === trimmedEditAlbumName.toLowerCase());
    if (existingAlbum) {
      setErrorMessage('Album already exists');
      return;
    }
    try {
      const response = await axios.put('/api/users/editAlbumName', { oldName: oldAlbumName, newName: trimmedEditAlbumName });
      setShowModal(false);
      setEditAlbumName('');
      setEditMode(false);
      await fetchAlbums();
    } catch (error) {
      console.log("Failed to edit album", error);
    }
  };

  useEffect(() => {
    const handleKeyPress = (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        if (editMode) {
          handleEditModalSubmit();
        } else {
          handleModalSubmit();
        }
      }
    };
    document.addEventListener('keydown', handleKeyPress);
    return () => {
      document.removeEventListener('keydown', handleKeyPress);
    };
  }, [editMode, editAlbumName, albumName]);

  return (
    <main>
      <Header />
      {showModal && (
        <div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-gray-500 bg-opacity-50 z-50"></div>
      )}

      {showModal && (
        <div className="fixed top-0 left-0 w-full h-full flex items-center justify-center z-50">
          <div className="bg-white p-10 rounded-md shadow-lg">
            {deleteAlbumName ? (
              <>
                <h2 className="text-lg font-semibold mb-2">Are you sure you want to delete this album?</h2>
                <div className="flex items-center justify-center">
                  <button className="bg-black text-white px-4 py-2 rounded-md mr-2 font-semibold" onClick={confirmDeleteAlbum}>Yes</button>
                  <button className="bg-gray-300 text-gray-800 px-4 py-2 rounded-md font-semibold" onClick={handleModalClose}>No</button>
                </div>
              </>
            ) : editMode ? (
              <>
                <h2 className="text-lg font-semibold mb-2">Edit Album Name</h2>
                {errorMessage && <p className='text-sm text-red-500 mb-2'>{errorMessage}</p>}
                <input
                  type="text"
                  className="border border-gray-300 rounded-md p-2 mb-2 outline-none"
                  value={editAlbumName}
                  onChange={handleEditInputChange}
                  placeholder='New Album Name'
                />
                <div className="flex items-center justify-center">
                  <button className="bg-black text-white px-4 py-2 rounded-md mr-2 font-semibold w-1/3 disabled:opacity-50" onClick={handleEditModalSubmit} disabled={!editAlbumName}>Save</button>
                  <button className="bg-gray-300 text-gray-800 px-4 py-2 rounded-md font-semibold w-1/3" onClick={handleModalClose}>Cancel</button>
                </div>
              </>
            ) : (
              <>
                <h2 className="text-lg font-semibold mb-2">Enter Album Name</h2>
                {errorMessage && <p className='text-sm text-red-500 mb-2'>{errorMessage}</p>}
                <input
                  type="text"
                  className="border border-gray-300 rounded-md p-2 mb-2 outline-none"
                  value={albumName}
                  onChange={handleModalInputChange}
                  placeholder='Album Name'
                />
                <div className="flex items-center justify-center">
                  <button className="bg-black text-white px-4 py-2 rounded-md mr-2 font-semibold disabled:opacity-50" onClick={handleModalSubmit} disabled={!albumName}>Create</button>
                  <button className="bg-gray-300 text-gray-800 px-4 py-2 rounded-md font-semibold" onClick={handleModalClose}>Cancel</button>
                </div>
              </>
            )}
          </div>
        </div>
      )}

      <div className="flex flex-col items-center justify-center bg-gray-300 shadow-lg p-4">
        <h1 className="font-bold text-3xl mb-4">Albums</h1>
        <button className="bg-black text-white px-4 py-2 rounded-xl mb-4 font-semibold" onClick={handleCreateAlbum}>
          Create Album
        </button>
      </div>

      <div className="grid grid-cols-3 gap-4 m-5">
        {albums.map((album, index) => (
          album && album.albumName && (
            <div key={index} className="bg-white rounded-lg shadow-md p-4 relative text-center">
              <Link
                href={`/albums/${album.albumName}`}
                className="text-lg font-semibold mb-2 hover:opacity-70 truncate w-32 block overflow-hidden"
                style={{ textOverflow: 'ellipsis' }}>
                {album.albumName}
              </Link>
              <div className='flex justify-end absolute bottom-0 right-0 space-x-2 mr-2'>
                <button
                  className="text-white"
                  onClick={() => handleEditAlbum(album.albumName)}
                  title='Edit Album'>
                  <FontAwesomeIcon icon={faPenToSquare} className='text-blue-500' />
                </button>
                <button
                  className="text-white"
                  onClick={() => handleDeleteAlbum(album.albumName)}
                  title='Delete Album'>
                  <FontAwesomeIcon icon={faTrashCan} className='text-red-500' />
                </button>
              </div>
            </div>
          )
        ))}
      </div>
    </main>
  );
}


Solution

  • Since you are using tailwindcss you could easily achieve that by combining truncate with flexbox classes.

    Note: Ignore the warning in console it's because of importing tailwindcss

    i.e. <script src="https://cdn.tailwindcss.com"></script>

    <script src="https://cdn.tailwindcss.com"></script>
    <div class="flex space-x-4 items-center m-4">
      <div class="truncate flex-1">
    Continually incentivize global methodologies for high-payoff opportunities. Quickly incentivize cross functional networks without turnkey core competencies. Synergistically negotiate resource maximizing infrastructures with flexible technologies. Compellingly procrastinate maintainable channels with.
      </div>
      <div class="whitespace-nowrap">
        <div class="bg-gray-600 h-6 w-6 inline-block"></div>
        <div class="bg-gray-600 h-6 w-6 inline-block"></div>
      </div>
    </div>
    
    <div class="flex space-x-4 items-center m-4">
      <div class="truncate flex-1">
    Continually incentivize global methodologies for high-payoff opportunities.
      </div>
      <div class="whitespace-nowrap">
        <div class="bg-gray-600 h-6 w-6 inline-block"></div>
        <div class="bg-gray-600 h-6 w-6 inline-block"></div>
      </div>
    </div>