Mobile menü tasarımı

This commit is contained in:
Sedat Öztürk 2025-10-19 11:24:55 +03:00
parent 33324aa456
commit 06658a1559
2 changed files with 212 additions and 62 deletions

View file

@ -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>
<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="hidden lg:block p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
className="p-1.5 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" />
<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>
</>
)

View file

@ -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}