Mobile menü tasarımı
This commit is contained in:
parent
33324aa456
commit
06658a1559
2 changed files with 212 additions and 62 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useMemo } from 'react'
|
||||
import React, { useState, useMemo, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiHome,
|
||||
|
|
@ -14,9 +14,28 @@ import {
|
|||
HiClipboardDocumentCheck,
|
||||
HiUserPlus,
|
||||
HiBars3,
|
||||
HiXMark,
|
||||
HiChevronLeft,
|
||||
HiCog6Tooth,
|
||||
HiArrowsUpDown,
|
||||
HiSquares2X2,
|
||||
} from 'react-icons/hi2'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
mockTasks,
|
||||
mockEvents,
|
||||
|
|
@ -131,7 +150,36 @@ interface IntranetSidebarProps {
|
|||
const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigate }) => {
|
||||
const [expandedMenus, setExpandedMenus] = useState<string[]>(['hr'])
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
||||
const [customOrder, setCustomOrder] = useState<string[]>([])
|
||||
|
||||
// Mobil ekranlarda otomatik daralt
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth < 1024) {
|
||||
// lg breakpoint (1024px)
|
||||
setIsCollapsed(true)
|
||||
}
|
||||
}
|
||||
|
||||
// İlk yükleme
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
// localStorage'dan özel sıralamayı yükle
|
||||
useEffect(() => {
|
||||
const savedOrder = localStorage.getItem('intranet-menu-order')
|
||||
if (savedOrder) {
|
||||
try {
|
||||
setCustomOrder(JSON.parse(savedOrder))
|
||||
} catch (e) {
|
||||
console.error('Failed to load menu order:', e)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Dinamik badge sayılarını hesapla
|
||||
const badgeCounts = useMemo(() => {
|
||||
|
|
@ -172,30 +220,113 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
|||
|
||||
const menuItems = useMemo(() => getMenuItems(badgeCounts), [badgeCounts])
|
||||
|
||||
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
||||
// Menü sıralamasını customOrder'a göre düzenle
|
||||
const orderedMenuItems = useMemo(() => {
|
||||
if (customOrder.length === 0) {
|
||||
return menuItems
|
||||
}
|
||||
|
||||
const ordered = [...menuItems].sort((a, b) => {
|
||||
const indexA = customOrder.indexOf(a.id)
|
||||
const indexB = customOrder.indexOf(b.id)
|
||||
|
||||
// Eğer her ikisi de customOrder'da varsa, sıralarına göre sırala
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
// Sadece a customOrder'da varsa, a önce gelsin
|
||||
if (indexA !== -1) return -1
|
||||
|
||||
// Sadece b customOrder'da varsa, b önce gelsin
|
||||
if (indexB !== -1) return 1
|
||||
|
||||
// İkisi de yoksa, orijinal sıraları koru
|
||||
return 0
|
||||
})
|
||||
|
||||
return ordered
|
||||
}, [menuItems, customOrder])
|
||||
|
||||
// Drag & Drop sensörleri
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = orderedMenuItems.findIndex((item) => item.id === active.id)
|
||||
const newIndex = orderedMenuItems.findIndex((item) => item.id === over.id)
|
||||
|
||||
const newOrder = arrayMove(orderedMenuItems, oldIndex, newIndex)
|
||||
const newOrderIds = newOrder.map((item) => item.id)
|
||||
|
||||
setCustomOrder(newOrderIds)
|
||||
localStorage.setItem('intranet-menu-order', JSON.stringify(newOrderIds))
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDesignMode = () => {
|
||||
setIsDesignMode(!isDesignMode)
|
||||
}
|
||||
|
||||
const resetMenuOrder = () => {
|
||||
setCustomOrder([])
|
||||
localStorage.removeItem('intranet-menu-order')
|
||||
}
|
||||
|
||||
// Sortable MenuItem komponenti
|
||||
const SortableMenuItem = ({ item, level = 0 }: { item: MenuItem; level?: number }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: item.id,
|
||||
disabled: !isDesignMode,
|
||||
})
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const isExpanded = expandedMenus.includes(item.id)
|
||||
const active = isActive(item.path)
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<div ref={setNodeRef} style={style} key={item.id}>
|
||||
<button
|
||||
{...(isDesignMode ? { ...attributes, ...listeners } : {})}
|
||||
onClick={() => {
|
||||
if (isDesignMode) return // Tasarım modunda navigasyon engellenir
|
||||
|
||||
if (hasChildren) {
|
||||
toggleMenu(item.id)
|
||||
} else if (item.path) {
|
||||
onNavigate(item.path)
|
||||
setIsMobileOpen(false)
|
||||
}
|
||||
}}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors ${
|
||||
className={`w-full flex items-center ${
|
||||
isCollapsed ? 'justify-center' : 'justify-between'
|
||||
} px-3 py-2.5 rounded-lg transition-colors ${
|
||||
active
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
} ${level > 0 ? 'ml-6' : ''}`}
|
||||
title={isCollapsed ? item.label : undefined}
|
||||
} ${level > 0 ? 'ml-6' : ''} ${
|
||||
isDesignMode
|
||||
? 'cursor-move border-2 border-dashed border-blue-400 dark:border-blue-500'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
title={isCollapsed ? item.label : isDesignMode ? 'Sürükle' : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{isDesignMode && !isCollapsed && (
|
||||
<HiArrowsUpDown className="w-4 h-4 flex-shrink-0 text-blue-500" />
|
||||
)}
|
||||
<item.icon className="w-5 h-5 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="font-medium text-sm truncate">{item.label}</span>}
|
||||
</div>
|
||||
|
|
@ -206,7 +337,7 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
|||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
{hasChildren && (
|
||||
{hasChildren && !isDesignMode && (
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
|
|
@ -229,7 +360,9 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
|||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-1 space-y-1">
|
||||
{item.children!.map((child) => renderMenuItem(child, level + 1))}
|
||||
{item.children!.map((child) => (
|
||||
<SortableMenuItem key={child.id} item={child} level={level + 1} />
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
@ -239,73 +372,90 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
|||
)
|
||||
}
|
||||
|
||||
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
||||
return <SortableMenuItem key={item.id} item={item} level={level} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
||||
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{isMobileOpen ? (
|
||||
<HiXMark className="w-6 h-6 text-gray-700 dark:text-gray-300" />
|
||||
) : (
|
||||
<HiBars3 className="w-6 h-6 text-gray-700 dark:text-gray-300" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Mobile Overlay */}
|
||||
<AnimatePresence>
|
||||
{isMobileOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
className="lg:hidden fixed inset-0 bg-black/50 z-40"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Sidebar */}
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
width: isCollapsed ? '80px' : '256px',
|
||||
x: isMobileOpen ? 0 : undefined,
|
||||
width: isCollapsed ? '60px' : '256px',
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={`
|
||||
bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
|
||||
h-screen sticky top-0 overflow-y-auto
|
||||
${isMobileOpen ? 'fixed left-0 top-0 z-40 lg:relative' : 'hidden lg:block'}
|
||||
`}
|
||||
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 h-screen sticky top-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div className="p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
{!isCollapsed ? (
|
||||
<>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white truncate">
|
||||
İntranet Portal
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
className="hidden lg:block p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="Daralt"
|
||||
>
|
||||
<HiChevronLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</>
|
||||
<div className="flex items-center justify-between px-3 py-2.5">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<HiSquares2X2 className="w-5 h-5 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={toggleDesignMode}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
isDesignMode
|
||||
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
title={isDesignMode ? 'Düzenlemeyi Bitir' : 'Menü Düzenle'}
|
||||
>
|
||||
<HiCog6Tooth className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="Daralt"
|
||||
>
|
||||
<HiChevronLeft className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
className="w-full flex justify-center p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
className="w-full flex justify-center p-2.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Genişlet"
|
||||
>
|
||||
<HiBars3 className="w-6 h-6 text-gray-600 dark:text-gray-400" />
|
||||
<HiBars3 className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-1">{menuItems.map((item) => renderMenuItem(item))}</nav>
|
||||
{/* Tasarım Modu Bilgi Paneli */}
|
||||
{!isCollapsed && isDesignMode && (
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 flex items-center gap-2">
|
||||
<HiArrowsUpDown className="w-4 h-4" />
|
||||
Menü öğelerini sürükleyerek sıralayın
|
||||
</p>
|
||||
{customOrder.length > 0 && (
|
||||
<button
|
||||
onClick={resetMenuOrder}
|
||||
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
||||
title="Varsayılana Sıfırla"
|
||||
>
|
||||
Sıfırla
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext
|
||||
items={orderedMenuItems.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<nav className="p-2 space-y-1">
|
||||
{orderedMenuItems.map((item) => renderMenuItem(item))}
|
||||
</nav>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</motion.div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -371,10 +371,10 @@ const TasksModule: React.FC = () => {
|
|||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg sm:text-xl">{column.icon}</span>
|
||||
<h3
|
||||
className={`font-semibold text-sm sm:text-base transition-colors duration-200 ${
|
||||
className={`text-sm sm:text-base transition-colors duration-200 ${
|
||||
dragOverColumn === column.id
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-900 dark:text-white'
|
||||
? 'text-red-600 font-bold dark:text-blue-400'
|
||||
: 'text-gray-900 font-semibold dark:text-white'
|
||||
}`}
|
||||
>
|
||||
{column.title}
|
||||
|
|
|
|||
Loading…
Reference in a new issue