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 { motion, AnimatePresence } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
HiHome,
|
HiHome,
|
||||||
|
|
@ -14,9 +14,28 @@ import {
|
||||||
HiClipboardDocumentCheck,
|
HiClipboardDocumentCheck,
|
||||||
HiUserPlus,
|
HiUserPlus,
|
||||||
HiBars3,
|
HiBars3,
|
||||||
HiXMark,
|
|
||||||
HiChevronLeft,
|
HiChevronLeft,
|
||||||
|
HiCog6Tooth,
|
||||||
|
HiArrowsUpDown,
|
||||||
|
HiSquares2X2,
|
||||||
} from 'react-icons/hi2'
|
} 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 {
|
import {
|
||||||
mockTasks,
|
mockTasks,
|
||||||
mockEvents,
|
mockEvents,
|
||||||
|
|
@ -131,7 +150,36 @@ interface IntranetSidebarProps {
|
||||||
const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigate }) => {
|
const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigate }) => {
|
||||||
const [expandedMenus, setExpandedMenus] = useState<string[]>(['hr'])
|
const [expandedMenus, setExpandedMenus] = useState<string[]>(['hr'])
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
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
|
// Dinamik badge sayılarını hesapla
|
||||||
const badgeCounts = useMemo(() => {
|
const badgeCounts = useMemo(() => {
|
||||||
|
|
@ -172,30 +220,113 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
||||||
|
|
||||||
const menuItems = useMemo(() => getMenuItems(badgeCounts), [badgeCounts])
|
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 hasChildren = item.children && item.children.length > 0
|
||||||
const isExpanded = expandedMenus.includes(item.id)
|
const isExpanded = expandedMenus.includes(item.id)
|
||||||
const active = isActive(item.path)
|
const active = isActive(item.path)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div ref={setNodeRef} style={style} key={item.id}>
|
||||||
<button
|
<button
|
||||||
|
{...(isDesignMode ? { ...attributes, ...listeners } : {})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (isDesignMode) return // Tasarım modunda navigasyon engellenir
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
toggleMenu(item.id)
|
toggleMenu(item.id)
|
||||||
} else if (item.path) {
|
} else if (item.path) {
|
||||||
onNavigate(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
|
active
|
||||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
? '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'
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
} ${level > 0 ? 'ml-6' : ''}`}
|
} ${level > 0 ? 'ml-6' : ''} ${
|
||||||
title={isCollapsed ? item.label : undefined}
|
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">
|
<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" />
|
<item.icon className="w-5 h-5 flex-shrink-0" />
|
||||||
{!isCollapsed && <span className="font-medium text-sm truncate">{item.label}</span>}
|
{!isCollapsed && <span className="font-medium text-sm truncate">{item.label}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -206,7 +337,7 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
||||||
{item.badge}
|
{item.badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasChildren && (
|
{hasChildren && !isDesignMode && (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
animate={{ rotate: isExpanded ? 90 : 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
|
|
@ -229,7 +360,9 @@ const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigat
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="mt-1 space-y-1">
|
<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>
|
</div>
|
||||||
</motion.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 (
|
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 */}
|
{/* Sidebar */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{
|
animate={{
|
||||||
width: isCollapsed ? '80px' : '256px',
|
width: isCollapsed ? '60px' : '256px',
|
||||||
x: isMobileOpen ? 0 : undefined,
|
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className={`
|
className="bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 h-screen sticky top-0 overflow-y-auto"
|
||||||
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'}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<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 ? (
|
{!isCollapsed ? (
|
||||||
<>
|
<div className="flex items-center justify-between px-3 py-2.5">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white truncate">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
İntranet Portal
|
<HiSquares2X2 className="w-5 h-5 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
||||||
</h2>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
onClick={() => setIsCollapsed(true)}
|
<button
|
||||||
className="hidden lg:block p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
onClick={toggleDesignMode}
|
||||||
title="Daralt"
|
className={`p-1.5 rounded transition-colors ${
|
||||||
>
|
isDesignMode
|
||||||
<HiChevronLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400'
|
||||||
</button>
|
: '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
|
<button
|
||||||
onClick={() => setIsCollapsed(false)}
|
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"
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -371,10 +371,10 @@ const TasksModule: React.FC = () => {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-lg sm:text-xl">{column.icon}</span>
|
<span className="text-lg sm:text-xl">{column.icon}</span>
|
||||||
<h3
|
<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
|
dragOverColumn === column.id
|
||||||
? 'text-blue-600 dark:text-blue-400'
|
? 'text-red-600 font-bold dark:text-blue-400'
|
||||||
: 'text-gray-900 dark:text-white'
|
: 'text-gray-900 font-semibold dark:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{column.title}
|
{column.title}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue