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