0

When I'm trying to edit the playlist name, The accordion is getting closed, and when my mouse is active on the input field on the playlist name input field, and if I hit spacebar on the keyboard, it is the top playlist is going down, and bottom one is coming up.

Why is that?

Stack: NextJS + ShadCN UI + TailwindCSS


import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { PlusCircle, Trash2, Upload } from 'lucide-react';
import { getS3ObjectUrl, listS3Objects } from '@/lib/routes';
import { useEffect, useRef, useState } from 'react';

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import Image from 'next/image';
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Modal from "@/components/Modal";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from 'next/navigation';
import { v4 as uuidv4 } from 'uuid';

interface Playlist {
    end_time: string;
    images: { duration: number; url: string }[];
    start_time: string;
    videos: { duration: number; url: string }[];
    weekdays: string;
}

interface ScheduledPlaylist {
    [playlistName: string]: Playlist;
}

interface PlaylistData {
    id: number;
    data: ScheduledPlaylist;
}

interface PlaylistFormProps {
    onSave: (schedules: PlaylistData) => void;
    initialSchedules: PlaylistData;
    isEditing?: boolean;
    devices?: number;
}

interface MediaItem {
    name: string;
    url: string;
    presignedUrl: string;
    size: number;
}

interface S3Object {
    Key: string;
    Size: number;
}

const PlaylistForm: React.FC<PlaylistFormProps> = ({ onSave, initialSchedules, isEditing = false, devices = 0 }) => {
    const router = useRouter();
    const [schedules, setSchedules] = useState<PlaylistData>(initialSchedules);
    const [error, setError] = useState<string | null>(null);
    const [openItems, setOpenItems] = useState<string[]>([]);
    const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
    const [mediaList, setMediaList] = useState<MediaItem[]>([]);
    const [searchTerm, setSearchTerm] = useState('');
    const [currentPlaylist, setCurrentPlaylist] = useState('');
    const [currentMediaType, setCurrentMediaType] = useState<'images' | 'videos'>('images');
    const { authToken } = useAuth();
    const fileInputRef = useRef<HTMLInputElement>(null);

    useEffect(() => {
        if (isEditing && initialSchedules.id) {
            fetchScheduledPlaylist(initialSchedules.id);
        } else {
            setSchedules(initialSchedules);
            setOpenItems(Object.keys(initialSchedules.data));
        }
    }, [initialSchedules, isEditing]);

    const fetchScheduledPlaylist = async (id: number) => {
       //  fetchScheduledPlaylist code here
    };

    const addSchedule = () => {
         // addSchedule code here
    };

    const updatePlaylistName = (oldName: string, newName: string) => {
        if (oldName === newName) return;
        setSchedules(prev => {
            const newData = { ...prev.data };
            const playlistData = newData[oldName];
            delete newData[oldName];
            newData[newName] = playlistData;
            return { ...prev, data: newData };
        });
        setOpenItems(prev => prev.map(item => item === oldName ? newName : item));
    };

    const updateSchedule = (playlistName: string, field: keyof Playlist, value: unknown) => {
         // update schedule code here
    };

    const updateWeekday = (playlistName: string, dayIndex: number) => {
       // updateWeekday code here
    };

    const addMedia = (playlistName: string, type: 'images' | 'videos', url: string, duration: number = 20) => {
         // addMedia code here
    };

    const updateMedia = (playlistName: string, type: 'images' | 'videos', index: number, field: 'url' | 'duration', value: string | number) => {
        const updatedMedia = schedules.data[playlistName][type].map((item, i) =>
            i === index ? { ...item, [field]: value } : item
        );
        updateSchedule(playlistName, type, updatedMedia);
    };

    const removeMedia = (playlistName: string, type: 'images' | 'videos', index: number) => {
        const updatedMedia = schedules.data[playlistName][type].filter((_, i) => i !== index);
        updateSchedule(playlistName, type, updatedMedia);
    };

    const deletePlaylist = (playlistName: string) => {
        if (!confirm(`Are you sure you want to delete the playlist "${playlistName}"?`)) return;
        const newSchedules = { ...schedules };
        delete newSchedules.data[playlistName];
        setSchedules(newSchedules);
    };

    const deleteSetup = async () => {
        if (!confirm("Are you sure you want to delete this entire setup?")) return;
        try {
            const response = await fetch(`/api/proxy/scheduled_playlists/${initialSchedules.id}`, {
                method: 'DELETE',
                headers: {
                    'Authorization': `Bearer ${authToken}`,
                },
            });
            if (response.ok) {
                router.push('/dashboard/setups');
            } else {
                setError("Failed to delete setup");
            }
        } catch (error) {
            console.error("Error deleting setup:", error);
            setError("Error deleting setup");
        }
    };

    const savePlaylist = async () => {
         // savePlaylist code here
    };

    const fetchMediaList = async () => {
       // fetchMediaList code here
    };

    const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
         // handleUpload code here
    };

    const renderMediaSection = (playlistName: string, type: 'images' | 'videos') => {
        return (
            // code here
    };

    return (
        <div className="mt-8 space-y-6">
            <h2 className="text-2xl font-bold">Playlists</h2>

            {error && (
                <Alert variant="destructive">
                    <AlertTitle>Error</AlertTitle>
                    <AlertDescription>{error}</AlertDescription>
                </Alert>
            )}

            <Accordion
                type="multiple"
                value={openItems}
                onValueChange={setOpenItems}
                className="w-full"
            >
                {Object.entries(schedules.data).map(([playlistName, playlist]) => (
                    <AccordionItem value={playlistName} key={uuidv4()}>
                        <AccordionTrigger>
                            {playlistName}
                        </AccordionTrigger>


                        <AccordionContent>
                            <div className="space-y-4">
                                <div>

                                    <Label>Playlist Name</Label>
                                    <Input
                                        value={playlistName}
                                        onChange={(e) => updatePlaylistName(playlistName, e.target.value)}

                                        className="mt-1 bg-white text-black"
                                    />
                                </div>

                                <Button
                                    variant="ghost"
                                    size="sm"
                                    onClick={(e) => {
                                        e.stopPropagation();
                                        deletePlaylist(playlistName);
                                    }}
                                    className="hover:bg-red-100 p-2 mr-2"
                                >
                                    <Trash2 size={16} className="text-red-500" />
                                </Button>
                            </div>
                            <div className="space-y-4">
                                <div className="flex space-x-4">
                                    <div className="w-1/2">
                                        <Label>Start Time</Label>
                                        <Input
                                            type="time"
                                            value={playlist.start_time}
                                            onChange={(e) => updateSchedule(playlistName, 'start_time', e.target.value)}
                                            className="mt-1 bg-white text-black"
                                        />
                                    </div>
                                    <div className="w-1/2">
                                        <Label>End Time</Label>
                                        <Input
                                            type="time"
                                            value={playlist.end_time}
                                            onChange={(e) => updateSchedule(playlistName, 'end_time', e.target.value)}
                                            className="mt-1 bg-white text-black"
                                        />
                                    </div>
                                </div>
                                <div>
                                    <Label className="mb-2 block">Weekdays</Label>
                                    <div className="flex space-x-4 mt-2">
                                        {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day, index) => (
                                            <div key={day} className="flex items-center">
                                                <Checkbox
                                                    id={`${day}-${playlistName}`}
                                                    checked={playlist.weekdays[index] === '1'}
                                                    onCheckedChange={() => updateWeekday(playlistName, index)}
                                                />
                                                <Label htmlFor={`${day}-${playlistName}`} className="ml-1">{day}</Label>
                                            </div>
                                        ))}
                                    </div>
                                </div>
                                {renderMediaSection(playlistName, 'images')}
                                {renderMediaSection(playlistName, 'videos')}
                            </div>
                        </AccordionContent>
                    </AccordionItem>
                ))}
            </Accordion>

            <Button onClick={addSchedule} variant="outline" className="mt-4 dark:border-white border-black">
                <PlusCircle size={20} className="mr-2" /> Add Another Playlist
            </Button>

            <div className="flex space-x-12 items-center">
                <Button onClick={savePlaylist} className="min-w-fit py-4 text-lg mt-8 bg-green-600 hover:bg-green-800 text-white">
                    {isEditing ? "Update Setup" : "Save Setup"}
                </Button>
                {isEditing && (
                    <Button onClick={deleteSetup} className="min-w-fit py-4 text-lg mt-8 bg-red-600 hover:bg-red-800 text-white">
                        Delete Setup
                    </Button>
                )}
            </div>

            <Modal isOpen={isMediaModalOpen} onClose={() => setIsMediaModalOpen(false)}>
                <div className="p-4">
                    <h2 className="text-xl font-bold mb-4">Select Media</h2>
                    <div className="flex items-center mb-4">
                        <Input
                            type="text"
                            placeholder="Search media..."
                            value={searchTerm}
                            onChange={(e) => setSearchTerm(e.target.value)}
                            className="flex-grow mr-2"
                        />
                        <input
                            type="file"
                            ref={fileInputRef}
                            style={{ display: 'none' }}
                            onChange={handleUpload}
                            accept="image/*,video/*"
                        />
                        <Button
                            variant="outline"
                            size="icon"
                            title="Upload media"
                            onClick={() => fileInputRef.current?.click()}
                        >
                            <Upload size={20} />
                        </Button>
                    </div>
                    <div className="space-y-4">
                        <div>
                            <h3 className="text-lg font-medium mb-2">Images</h3>
                            <div className="grid grid-cols-3 gap-4 max-h-[30vh] overflow-y-auto">
                                {mediaList
                                    .filter(media =>
                                        media.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
                                        media.name.match(/\.(jpeg|jpg|gif|png)$/i)
                                    )
                                    .map((media) => (
                                        <button
                                            type="button"
                                            key={media.name}
                                            className="cursor-pointer text-left"
                                            onClick={() => handleMediaSelection(media)}
                                        >
                                            <Image
                                                src={media.presignedUrl}
                                                alt={media.name}
                                                width={100}
                                                height={100}
                                                className="w-full h-24 object-cover"
                                            />
                                            <p className="mt-2 text-sm truncate">{media.name}</p>
                                            <p className="text-xs text-gray-500">{(media.size / 1024 / 1024).toFixed(2)} MB</p>
                                        </button>
                                    ))}
                            </div>
                        </div>
                        <div>
                            <h3 className="text-lg font-medium mb-2">Videos</h3>
                            <div className="grid grid-cols-3 gap-4 max-h-[30vh] overflow-y-auto">
                                {mediaList
                                    .filter(media =>
                                        media.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
                                        media.name.match(/\.(mp4|webm|ogg)$/i)
                                    )
                                    .map((media) => (
                                        <button
                                            type="button"
                                            key={media.name}
                                            className="cursor-pointer text-left"
                                            onClick={() => handleMediaSelection(media)}
                                        >
                                            <video src={media.presignedUrl} className="w-full h-24 object-cover">
                                                <track kind="captions" />
                                                Your browser does not support the video tag.
                                            </video>
                                            <p className="mt-2 text-sm truncate">{media.name}</p>
                                            <p className="text-xs text-gray-500">{(media.size / 1024 / 1024).toFixed(2)} MB</p>
                                        </button>
                                    ))}
                            </div>
                        </div>
                    </div>
                </div>
            </Modal>
        </div>
    );
};

export default PlaylistForm;

I tried moving the Input field outside of the Accordion content, but it didn't work, Kindly help me either finding that bug.

Thanks in advance, Help appreciated

1 Answer 1

0

That seems a propagation error. Try this:

<AccordionContent>
<div className="space-y-4">
    <div>
        <Label>Playlist Name</Label>
        <Input
            value={playlistName}
            onChange={(e) => updatePlaylistName(playlistName, e.target.value)}
            onKeyDown={(e) => {
                if (e.key === ' ') {
                    e.stopPropagation();
                }
            }}
            className="mt-1 bg-white text-black"
        />
    </div>

    <Button
        variant="ghost"
        size="sm"
        onClick={(e) => {
            e.stopPropagation();
            deletePlaylist(playlistName);
        }}
        className="hover:bg-red-100 p-2 mr-2"
    >
        <Trash2 size={16} className="text-red-500" />
    </Button>
</div>
<div className="space-y-4">
    <div className="flex space-x-4">
        <div className="w-1/2">
            <Label>Start Time</Label>
            <Input
                type="time"
                value={playlist.start_time}
                onChange={(e) => updateSchedule(playlistName, 'start_time', e.target.value)}
                className="mt-1 bg-white text-black"
            />
        </div>
        <div className="w-1/2">
            <Label>End Time</Label>
            <Input
                type="time"
                value={playlist.end_time}
                onChange={(e) => updateSchedule(playlistName, 'end_time', e.target.value)}
                className="mt-1 bg-white text-black"
            />
        </div>
    </div>
    <div>
        <Label className="mb-2 block">Weekdays</Label>
        <div className="flex space-x-4 mt-2">
            {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day, index) => (
                <div key={day} className="flex items-center">
                    <Checkbox
                        id={`${day}-${playlistName}`}
                        checked={playlist.weekdays[index] === '1'}
                        onCheckedChange={() => updateWeekday(playlistName, index)}
                    />
                    <Label htmlFor={`${day}-${playlistName}`} className="ml-1">{day}</Label>
                </div>
            ))}
        </div>
    </div>
    {renderMediaSection(playlistName, 'images')}
    {renderMediaSection(playlistName, 'videos')}
</div>

The problem is that when you click the input you are also triggering the accordion, so you need to stop the event propagation when you click the input.

Not the answer you're looking for? Browse other questions tagged or ask your own question.