diff --git a/jsconfig.json b/jsconfig.json index 0405287..f376ff5 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { + "jsx": "react-jsx" , "baseUrl": ".", "paths": { "@/*": ["./src/*"], "vComponents/*": ["./node_modules/vComponents/dist/components/*"], + "VCalendar": ["./node_modules/vCalendar/dist/VCalendar"], "via-ui/*": ["./node_modules/via-ui/dist/components/*"] } } diff --git a/package.json b/package.json index b628df8..532ba10 100644 --- a/package.json +++ b/package.json @@ -29,16 +29,29 @@ "@codemirror/lang-javascript": "^6.2.1", "@codemirror/theme-one-dark": "^6.1.2", "@codesandbox/sandpack-react": "^2.10.0", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.1.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-scroll-area": "^1.0.5", + "@uidotdev/usehooks": "^2.4.1", "@uiw/codemirror-theme-monokai": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", + "class-variance-authority": "^0.7.0", + "classnames": "^2.5.1", "config": "^3.3.9", + "embla-carousel-react": "^8.0.0-rc19", "js-cookie": "^3.0.5", "js-md5": "^0.8.3", "next": "14.0.4", "node-forge": "^1.3.1", "pristinejs": "^1.1.0", "react": "^18", + "react-beautiful-dnd": "^13.1.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18", "react-dropzone-component": "^3.2.0", "react-icons": "^4.12.0", @@ -49,6 +62,8 @@ "react-toastify": "^9.1.3", "rosetta": "^1.1.0", "v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git", + "v-kanban": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-kanban.git#dev_ja", + "vCalendar": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-calendar.git", "vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja", "via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja" }, diff --git a/src/components/Icons/index.jsx b/src/components/Icons/index.jsx index 6a72b7a..8ffc2bb 100644 --- a/src/components/Icons/index.jsx +++ b/src/components/Icons/index.jsx @@ -2,7 +2,7 @@ import { BsSearch } from 'react-icons/bs' import { IoEllipsisVertical, IoFilter } from 'react-icons/io5' import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid' import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa' -import { RiEyeLine } from 'react-icons/ri' +// import { RiEyeLine } from 'react-icons/ri' import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu' export const Icons = { diff --git a/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts b/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts new file mode 100644 index 0000000..99a0a33 --- /dev/null +++ b/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts @@ -0,0 +1,21 @@ +import { UniqueIdentifier } from '@dnd-kit/core' +import { Task } from '../TaskCard/taskCard.types' + +export interface Column { + id: UniqueIdentifier; + title: string; + color: string; +} + +export type ColumnType = 'Column'; + +export interface ColumnDragData { + type: ColumnType; + column: Column; +} + +export interface BoardColumnProps { + column: Column; + tasks: Task[]; + isOverlay?: boolean; +} diff --git a/src/components/v-kanban/components/BoardColumn/index.tsx b/src/components/v-kanban/components/BoardColumn/index.tsx new file mode 100644 index 0000000..76047f1 --- /dev/null +++ b/src/components/v-kanban/components/BoardColumn/index.tsx @@ -0,0 +1,163 @@ +import React, { useMemo } from 'react' + +// DnD: https://docs.dndkit.com/ +import { SortableContext, useSortable } from '@dnd-kit/sortable' +import { useDndContext } from '@dnd-kit/core' +import { CSS } from '@dnd-kit/utilities' + +// via-ui +import { Card, CardContent, CardHeader } from '../ui/card' +import { ScrollArea, ScrollBar } from '../ui/scroll-area' + +// Other +import { TaskCard } from '../TaskCard' +import { cva } from 'class-variance-authority' +import { cn } from '../../lib/utils' +import { BoardColumnProps, ColumnDragData } from './boardColumn.types' +import { useScreenType } from '../../hooks' +import { Button } from '../ui/button' +import { Icons } from '../icons' +import Tooltip from 'via-ui/tooltip' + +export const BoardColumn = ({ column, tasks, isOverlay }: BoardColumnProps) => { + const screenType = useScreenType() + + const tasksIds = useMemo(() => { + return tasks.map((task) => task.id) + }, [tasks]) + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging + } = useSortable({ + id: column.id, + data: { + type: 'Column', + column + } satisfies ColumnDragData, + attributes: { + roleDescription: `Column: ${column.title}` + } + }) + + const style = { + transition, + transform: CSS.Translate.toString(transform) + } + + const variants = cva( + 'group/board-column flex shrink-0 cursor-pointer select-none snap-center flex-col rounded-lg bg-gray-100 shadow-md hover:bg-gray-200', + { + variants: { + dragging: { + default: 'border-2 border-transparent', + over: 'border-4 border-dashed border-gray-400 opacity-30', + overlay: 'rotate-2 ring-2 ring-black' + }, + screenType: { + desktop: 'h-[500px] max-h-[500px] w-[300px] max-w-full', + mobile: 'h-[75vh] w-full min-w-full pb-2' + } + } + } + ) + + const variantsTask = cva( + 'flex grow gap-2 p-2', { + variants: { + screenType: { + desktop: 'flex-col', + mobile: 'flex-col' + } + } + }) + + return ( + + + + {column.title} + + + + + + + + + + {tasks.map((task) => ( + + ))} + + + + + + + ) +} + +export function BoardContainer ({ children }: { children: React.ReactNode }) { + const dndContext = useDndContext() + const screenType = useScreenType() + + const variations = cva('flex md:px-0 lg:justify-center', { + variants: { + dragging: { + default: 'snap-x snap-mandatory', + active: 'snap-none' + } + } + }) + + const variationsColumn = cva('flex w-full max-w-full items-start justify-start gap-4', { + variants: { + screenType: { + desktop: 'flex-row', + mobile: 'flex-row overflow-x-auto' + } + } + }) + + return ( + screenType.isMobile + ? ( +
+ {children} +
+ ) + : ( + +
+ {children} +
+ +
+ ) + ) +} diff --git a/src/components/v-kanban/components/Container/container.type.ts b/src/components/v-kanban/components/Container/container.type.ts new file mode 100644 index 0000000..762d7cc --- /dev/null +++ b/src/components/v-kanban/components/Container/container.type.ts @@ -0,0 +1,10 @@ +import { UniqueIdentifier } from '@dnd-kit/core' +import React from 'react' + +export default interface ContainerProps { + id: UniqueIdentifier; + children: React.ReactNode; + title?: string; + description?: string; + onAddItem?: () => void; +} diff --git a/src/components/v-kanban/components/Container/index.tsx b/src/components/v-kanban/components/Container/index.tsx new file mode 100644 index 0000000..779b2c5 --- /dev/null +++ b/src/components/v-kanban/components/Container/index.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import ContainerProps from './container.type' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import clsx from 'clsx' +import { Button } from '../ui/button' + +const Container = ({ + id, + children, + title, + description, + onAddItem +}: ContainerProps) => { + const { + attributes, + setNodeRef, + listeners, + transform, + transition, + isDragging + } = useSortable({ + id, + data: { + type: 'container' + } + }) + return ( +
+
+
+

{title}

+

{description}

+
+ +
+ + {children} + +
+ ) +} + +export default Container diff --git a/src/components/v-kanban/components/Item/index.tsx b/src/components/v-kanban/components/Item/index.tsx new file mode 100644 index 0000000..08464e6 --- /dev/null +++ b/src/components/v-kanban/components/Item/index.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { UniqueIdentifier } from '@dnd-kit/core' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import clsx from 'clsx' + +type ItemsType = { + id: UniqueIdentifier; + title: string; +}; + +const Items = ({ id, title }: ItemsType) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ + id, + data: { + type: 'item' + } + }) + return ( +
+
+ {title} + +
+
+ ) +} + +export default Items diff --git a/src/components/v-kanban/components/KanbanBoard/index.tsx b/src/components/v-kanban/components/KanbanBoard/index.tsx new file mode 100644 index 0000000..fba110d --- /dev/null +++ b/src/components/v-kanban/components/KanbanBoard/index.tsx @@ -0,0 +1,363 @@ +import React, { useMemo, useRef, useState } from 'react' +import { createPortal } from 'react-dom' + +// DndKit: +import { + DndContext, + type DragEndEvent, + type DragOverEvent, + DragOverlay, + type DragStartEvent, + useSensor, + useSensors, + KeyboardSensor, + Announcements, + UniqueIdentifier, + TouchSensor, + MouseSensor +} from '@dnd-kit/core' +import { SortableContext, arrayMove } from '@dnd-kit/sortable' +import { TaskCard } from '../TaskCard' + +// Others +import { BoardColumn, BoardContainer } from '../BoardColumn' +import { hasDraggableData } from '../../utils' +import { coordinateGetter } from '../../multipleContainersKeyboardPreset' +import { useIsClient, useScreenType } from '../../hooks' +import { initialTasks, ColumnId, defaultCols } from '../../constants/defaults' +import { toast } from 'react-toastify' +import { Column } from '../BoardColumn/boardColumn.types' +import { Task } from '../TaskCard/taskCard.types' +import { KanbanBoardProps } from './kanbanBoard.type' +import SearchHeader from '../SearchHeader' +import { Card, CardHeader } from '../ui/card' +import { cva } from 'class-variance-authority' +import { Icons } from '../icons' +import { cn } from '../../lib/utils' +// import { Button } from '../ui/button' + +const defaultSearchHeader = { + hide: false, + upperButtons: null, + hideInput: false, + inputSearchStyles: {}, + inputSearchClassName: '', + inputPlaceholder: 'Buscar...', + upperRightButtons: null, + newButtonOnClick: () => {}, + newButtonStyles: {}, + showNewButton: true +} + +export const KanbanBoard = ({ + columns = defaultCols, + tasks = initialTasks, + i18n = { t: () => { return 'txtNone' } }, + searchHeader = defaultSearchHeader +}: KanbanBoardProps) => { + const screenType = useScreenType() + + const [_columns, setColumns] = useState(columns) + const pickedUpTaskColumn = useRef(null) + const columnsId = useMemo(() => _columns.map((col) => col.id), [_columns]) + + const [_tasks, setTasks] = useState(tasks) + + const [activeColumn, setActiveColumn] = useState(null) + + const [activeTask, setActiveTask] = useState(null) + + const isClient = useIsClient() + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + delay: 250, + tolerance: 5 + } + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5 + } + }), + useSensor(KeyboardSensor, { + coordinateGetter + }) + ) + + function getDraggingTaskData (taskId: UniqueIdentifier, columnId: ColumnId) { + const tasksInColumn = _tasks.filter((task) => task.columnId === columnId) + const taskPosition = tasksInColumn.findIndex((task) => task.id === taskId) + const column = _columns.find((col) => col.id === columnId) + return { + tasksInColumn, + taskPosition, + column + } + } + + function onDragStart (event: DragStartEvent) { + if (!hasDraggableData(event.active)) return + const data = event.active.data.current + if (data?.type === 'Column') { + setActiveColumn(data.column) + return + } + + if (data?.type === 'Task') { + setActiveTask(data.task) + } + } + + function onDragOver (event: DragOverEvent) { + const { active, over } = event + if (!over) return + + const activeId = active.id + const overId = over.id + + if (activeId === overId) return + + if (!hasDraggableData(active) || !hasDraggableData(over)) return + + const activeData = active.data.current + const overData = over.data.current + + const isActiveATask = activeData?.type === 'Task' + const isOverATask = activeData?.type === 'Task' + + if (!isActiveATask) return + + // Im dropping a Task over another Task + if (isActiveATask && isOverATask) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId) + const overIndex = tasks.findIndex((t) => t.id === overId) + const activeTask = tasks[activeIndex] + const overTask = tasks[overIndex] + if ( + activeTask && + overTask && + activeTask.columnId !== overTask.columnId + ) { + activeTask.columnId = overTask.columnId + return arrayMove(tasks, activeIndex, overIndex - 1) + } + + return arrayMove(tasks, activeIndex, overIndex) + }) + } + + const isOverAColumn = overData?.type === 'Column' + + // Im dropping a Task over a column + if (isActiveATask && isOverAColumn) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId) + const activeTask = tasks[activeIndex] + if (activeTask) { + activeTask.columnId = overId as ColumnId + return arrayMove(tasks, activeIndex, activeIndex) + } + return tasks + }) + } + } + + function onDragEnd (event: DragEndEvent) { + setActiveColumn(null) + setActiveTask(null) + + const { active, over } = event + if (!over) return + + const activeId = active.id + const overId = over.id + + if (!hasDraggableData(active)) return + + const activeData = active.data.current + + if (activeId === overId) return + + const isActiveAColumn = activeData?.type === 'Column' + if (!isActiveAColumn) return + + setColumns((columns) => { + const activeColumnIndex = columns.findIndex((col) => col.id === activeId) + + const overColumnIndex = columns.findIndex((col) => col.id === overId) + + return arrayMove(columns, activeColumnIndex, overColumnIndex) + }) + } + + const announcements: Announcements = { + onDragStart ({ active }) { + if (!hasDraggableData(active)) return + if (active.data.current?.type === 'Column') { + const startColumnIdx = columnsId.findIndex((id) => id === active.id) + const startColumn = _columns[startColumnIdx] + return `Picked up Column ${startColumn?.title} at position: ${ + startColumnIdx + 1 + } of ${columnsId.length}` + } else if (active.data.current?.type === 'Task') { + pickedUpTaskColumn.current = active.data.current.task.columnId + const { tasksInColumn, taskPosition, column } = getDraggingTaskData( + active.id, + pickedUpTaskColumn.current + ) + return `Picked up Task ${ + active.data.current.task.content + } at position: ${taskPosition + 1} of ${ + tasksInColumn.length + } in column ${column?.title}` + } + }, + onDragOver ({ active, over }) { + if (!hasDraggableData(active) || !hasDraggableData(over)) return + + if ( + active.data.current?.type === 'Column' && + over.data.current?.type === 'Column' + ) { + const overColumnIdx = columnsId.findIndex((id) => id === over.id) + return `Column ${active.data.current.column.title} was moved over ${ + over.data.current.column.title + } at position ${overColumnIdx + 1} of ${columnsId.length}` + } else if ( + active.data.current?.type === 'Task' && + over.data.current?.type === 'Task' + ) { + const { tasksInColumn, taskPosition, column } = getDraggingTaskData( + over.id, + over.data.current.task.columnId + ) + if (over.data.current.task.columnId !== pickedUpTaskColumn.current) { + return `Task ${ + active.data.current.task.content + } was moved over column ${column?.title} in position ${ + taskPosition + 1 + } of ${tasksInColumn.length}` + } + return `Task was moved over position ${taskPosition + 1} of ${ + tasksInColumn.length + } in column ${column?.title}` + } + }, + onDragEnd ({ active, over }) { + toast.success(`${active.data.current?.type} dropped`) + + // Resetear a estado anterior + + if (!hasDraggableData(active) || !hasDraggableData(over)) { + pickedUpTaskColumn.current = null + return + } + if ( + active.data.current?.type === 'Column' && + over.data.current?.type === 'Column' + ) { + const overColumnPosition = columnsId.findIndex((id) => id === over.id) + + return `Column ${ + active.data.current.column.title + } was dropped into position ${overColumnPosition + 1} of ${ + columnsId.length + }` + } else if ( + active.data.current?.type === 'Task' && + over.data.current?.type === 'Task' + ) { + const { tasksInColumn, taskPosition, column } = getDraggingTaskData( + over.id, + over.data.current.task.columnId + ) + if (over.data.current.task.columnId !== pickedUpTaskColumn.current) { + return `Task was dropped into column ${column?.title} in position ${ + taskPosition + 1 + } of ${tasksInColumn.length}` + } + return `Task was dropped into position ${taskPosition + 1} of ${ + tasksInColumn.length + } in column ${column?.title}` + } + pickedUpTaskColumn.current = null + }, + onDragCancel ({ active }) { + pickedUpTaskColumn.current = null + if (!hasDraggableData(active)) return + return `Dragging ${active.data.current?.type} cancelled.` + } + } + + const variants = cva( + 'group flex shrink-0 cursor-pointer snap-center flex-col rounded-lg border-none bg-gray-50/90 shadow-md hover:bg-gray-100', + { + variants: { + screenType: { + desktop: 'h-[500px] max-h-[500px] w-[300px] max-w-full', + mobile: 'max-h-auto h-auto w-full min-w-full' + } + } + } + ) + + return ( +
+ + { !searchHeader.hide && } + + + + + {_columns.map((col) => ( + task.columnId === col.id)} + /> + ))} + {}}> + + + + Add section + + + + + + + {isClient && 'document' in window && + createPortal( + + {activeColumn && ( + task.columnId === activeColumn.id + )} + /> + )} + {activeTask && } + , + document.body + )} + +
+ ) +} diff --git a/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts b/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts new file mode 100644 index 0000000..9686a39 --- /dev/null +++ b/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts @@ -0,0 +1,32 @@ +import React from 'react' +import { Column } from '../BoardColumn/boardColumn.types' +import { Task } from '../TaskCard/taskCard.types' + +export interface SearchHeaderProps { + hide?: boolean; + searchHeaderStyles?: React.CSSProperties; + searchHeaderClassName?: string; + upperButtons?: React.ReactNode; + hideInput?: boolean; + inputSearchStyles?: React.CSSProperties; + inputSearchClassName?: string; + inputPlaceholder?: string; + upperRightButtons?: React.ReactNode; + showNewButton?: boolean; + newButtonOnClick?: () => void; + newButtonStyles?: React.CSSProperties; + newButtonClassName?: string; + newButtonText?: string; + newButtonIcon?: React.ReactNode; +} + +export interface I18n { + t: (key: string, options?: any) => string; +} + +export interface KanbanBoardProps { + columns?: Column[], + tasks?: Task[], + i18n?: I18n, + searchHeader?: SearchHeaderProps, +} diff --git a/src/components/v-kanban/components/SearchHeader/index.tsx b/src/components/v-kanban/components/SearchHeader/index.tsx new file mode 100644 index 0000000..bfaedd0 --- /dev/null +++ b/src/components/v-kanban/components/SearchHeader/index.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { I18n, SearchHeaderProps } from '../KanbanBoard/kanbanBoard.type' +import { Icons } from '../icons' +import { cva } from 'class-variance-authority' + +const SearchHeader: React.FC<{ searchHeader: SearchHeaderProps, i18n?: I18n }> = ({ searchHeader, i18n }) => { + const isMobile = true + const [isFocusedSearch, setIsFocusedSearch] = React.useState(false) + const { + upperButtons, + hideInput, + searchHeaderStyles, + searchHeaderClassName, + inputSearchClassName, + inputSearchStyles, + inputPlaceholder, + upperRightButtons, + showNewButton, + newButtonOnClick, + newButtonStyles, + newButtonClassName, + newButtonText, + newButtonIcon + } = searchHeader + + const gridClass = React.useMemo(() => { + if (upperButtons && upperRightButtons) { + return 'grid-cols-[minmax(auto,min-content),auto,minmax(auto,min-content),50px] sm:grid-cols-[minmax(auto,min-content),auto,minmax(auto,min-content),100px]' + } else if (upperButtons || upperRightButtons) { + return 'grid-cols-[minmax(auto,min-content),auto,50px] sm:grid-cols-[minmax(auto,min-content),auto,100px]' + } else { + return 'grid-cols-[auto,50px] sm:grid-cols-[auto,100px]' + } + }, [upperButtons, upperRightButtons]) + + const variantsButton = cva( + 'flex cursor-pointer items-center justify-center bg-theme-app-500 px-2 py-1 text-white drop-shadow-sm transition-all duration-500 ease-in-out hover:bg-theme-app-600', { + variants: { + screenType: { + desktop: 'rounded-lg', + mobile: 'rounded-r-lg' + } + } + }) + + return ( +
+ {upperButtons} + {!hideInput && ( +
+ + +
+ )} + {upperRightButtons} + {showNewButton === true && ( + + ) } +
+ ) +} + +export default SearchHeader diff --git a/src/components/v-kanban/components/TaskCard/index.tsx b/src/components/v-kanban/components/TaskCard/index.tsx new file mode 100644 index 0000000..2092c86 --- /dev/null +++ b/src/components/v-kanban/components/TaskCard/index.tsx @@ -0,0 +1,87 @@ +// DndKit documentation: https://docs.dndkit.com/ +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' + +// via-ui +import { Card, CardContent, CardHeader } from '../ui/card' +import { Badge } from '../ui/badge' + +// Others +import { cva } from 'class-variance-authority' +import { TaskCardProps, TaskDragData } from './taskCard.types' +import { useScreenType } from '../../hooks' + +export const TaskCard = ({ + task, + isOverlay, + renderHeader, + renderContent +}: TaskCardProps) => { + const screenType = useScreenType() + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging + } = useSortable({ + id: task.id, + data: { + type: 'Task', + task + } satisfies TaskDragData, + attributes: { + roleDescription: 'Task' + } + }) + + const style = { + transition, + transform: CSS.Translate.toString(transform) + } + + const variants = cva( + 'rounded-xl border border-transparent bg-white shadow-md hover:border-gray-200', + { + variants: { + dragging: { + over: 'opacity-30 ring-2', + overlay: 'rotate-2 bg-white/60 ring-2 ring-black' + }, + screenType: { + desktop: '', + mobile: 'w-full' + } + } + }) + + return ( + + + {renderHeader + ? renderHeader(task) + : ( + + {task.id} + + )} + + + {renderContent ? renderContent(task) : task.content} + + + ) +} diff --git a/src/components/v-kanban/components/TaskCard/taskCard.types.ts b/src/components/v-kanban/components/TaskCard/taskCard.types.ts new file mode 100644 index 0000000..db5f770 --- /dev/null +++ b/src/components/v-kanban/components/TaskCard/taskCard.types.ts @@ -0,0 +1,23 @@ +import { UniqueIdentifier } from '@dnd-kit/core' +import { ColumnId } from '../../constants/defaults' +import React from 'react' + +export interface Task { + id: UniqueIdentifier; + columnId: ColumnId; + content: string; +} + +export interface TaskCardProps { + task: Task; + isOverlay?: boolean; + renderHeader?: (task: Task) => React.ReactElement; + renderContent?: (task: Task) => React.ReactElement; +} + +export type TaskType = 'Task'; + +export interface TaskDragData { + type: TaskType; + task: Task; +} diff --git a/src/components/v-kanban/components/icons.tsx b/src/components/v-kanban/components/icons.tsx new file mode 100644 index 0000000..4618275 --- /dev/null +++ b/src/components/v-kanban/components/icons.tsx @@ -0,0 +1,17 @@ +import { BsSearch } from 'react-icons/bs' +import { FaCheck, FaChevronRight, FaRegCircle, FaArrowLeft, FaArrowRight, FaPlus } from 'react-icons/fa6' +import { LuGripVertical, LuPlusCircle } from 'react-icons/lu' +import { DocumentPlusIcon } from '@heroicons/react/24/solid' + +export const Icons = { + search: BsSearch, + check: FaCheck, + chevronRight: FaChevronRight, + circle: FaRegCircle, + gripVertical: LuGripVertical, + new: DocumentPlusIcon, + plus: FaPlus, + plusCircle: LuPlusCircle, + arrowLeft: FaArrowLeft, + arrowRight: FaArrowRight +} diff --git a/src/components/v-kanban/components/ui/badge.tsx b/src/components/v-kanban/components/ui/badge.tsx new file mode 100644 index 0000000..7533c9f --- /dev/null +++ b/src/components/v-kanban/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent', + outline: 'text-foreground' + } + }, + defaultVariants: { + variant: 'default' + } + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge ({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/v-kanban/components/ui/button.tsx b/src/components/v-kanban/components/ui/button.tsx new file mode 100644 index 0000000..6921f5f --- /dev/null +++ b/src/components/v-kanban/components/ui/button.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-blue-500 text-white hover:bg-blue-600', + destructive: 'bg-red-500 text-white hover:bg-red-600', + outline: 'border border-gray-300 bg-white hover:bg-gray-100 hover:text-gray-800', + secondary: 'bg-green-500 text-white hover:bg-green-600', + ghost: 'hover:bg-gray-100 hover:text-gray-800', + link: 'text-blue-500 underline-offset-4 hover:underline' + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/src/components/v-kanban/components/ui/card.tsx b/src/components/v-kanban/components/ui/card.tsx new file mode 100644 index 0000000..4902b39 --- /dev/null +++ b/src/components/v-kanban/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/v-kanban/components/ui/carousel.tsx b/src/components/v-kanban/components/ui/carousel.tsx new file mode 100644 index 0000000..df75b22 --- /dev/null +++ b/src/components/v-kanban/components/ui/carousel.tsx @@ -0,0 +1,258 @@ +'use client' + +import * as React from 'react' +import useEmblaCarousel, { + type UseEmblaCarouselType +} from 'embla-carousel-react' + +import { Button } from './button' +import { cn } from '../../lib/utils' +import { Icons } from '../icons' + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null) + +function useCarousel () { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error('useCarousel must be used within a ') + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y' + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault() + scrollPrev() + } else if (event.key === 'ArrowRight') { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on('reInit', onSelect) + api.on('select', onSelect) + + return () => { + api?.off('select', onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = 'Carousel' + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = 'CarouselContent' + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = 'CarouselItem' + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = 'CarouselPrevious' + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = 'CarouselNext' + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext +} diff --git a/src/components/v-kanban/components/ui/dropdown-menu.tsx b/src/components/v-kanban/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..26ee53a --- /dev/null +++ b/src/components/v-kanban/components/ui/dropdown-menu.tsx @@ -0,0 +1,197 @@ +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { cn } from '@/lib/utils' +import { Icons } from '../icons' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup +} diff --git a/src/components/v-kanban/components/ui/scroll-area.tsx b/src/components/v-kanban/components/ui/scroll-area.tsx new file mode 100644 index 0000000..ab822aa --- /dev/null +++ b/src/components/v-kanban/components/ui/scroll-area.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' +import { cn } from '../../lib/utils' + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/v-kanban/constants/defaults.ts b/src/components/v-kanban/constants/defaults.ts new file mode 100644 index 0000000..294689c --- /dev/null +++ b/src/components/v-kanban/constants/defaults.ts @@ -0,0 +1,90 @@ +import { Column } from '../components/BoardColumn/boardColumn.types' +import { Task } from '../components/TaskCard/taskCard.types' + +export const defaultCols = [ + { + id: 'todo' as const, + title: 'Todo', + color: 'bg-blue-500' + }, + { + id: 'in-progress' as const, + title: 'In progress', + color: 'bg-yellow-500' + }, + { + id: 'done' as const, + title: 'Done', + color: 'bg-green-500' + } +] satisfies Column[] + +export type ColumnId = (typeof defaultCols)[number]['id']; + +export const initialTasks: Task[] = [ + { + id: 'task1', + columnId: 'done', + content: 'Project initiation and planning' + }, + { + id: 'task2', + columnId: 'done', + content: 'Gather requirements from stakeholders' + }, + { + id: 'task3', + columnId: 'done', + content: 'Create wireframes and mockups' + }, + { + id: 'task4', + columnId: 'in-progress', + content: 'Develop homepage layout' + }, + { + id: 'task5', + columnId: 'in-progress', + content: 'Design color scheme and typography' + }, + { + id: 'task6', + columnId: 'todo', + content: 'Implement user authentication' + }, + { + id: 'task7', + columnId: 'todo', + content: 'Build contact us page' + }, + { + id: 'task8', + columnId: 'todo', + content: 'Create product catalog' + }, + { + id: 'task9', + columnId: 'todo', + content: 'Develop about us page' + }, + { + id: 'task10', + columnId: 'todo', + content: 'Optimize website for mobile devices' + }, + { + id: 'task11', + columnId: 'todo', + content: 'Integrate payment gateway' + }, + { + id: 'task12', + columnId: 'todo', + content: 'Perform testing and bug fixing' + }, + { + id: 'task13', + columnId: 'todo', + content: 'Launch website and deploy to server' + } +] diff --git a/src/components/v-kanban/hooks/index.js b/src/components/v-kanban/hooks/index.js new file mode 100644 index 0000000..1b811b7 --- /dev/null +++ b/src/components/v-kanban/hooks/index.js @@ -0,0 +1,3 @@ +export { useIsClient } from './useIsClient' +export { useWindowSize } from './useWindowSize' +export { useScreenType } from './useScreenType' diff --git a/src/components/v-kanban/hooks/useIsClient.ts b/src/components/v-kanban/hooks/useIsClient.ts new file mode 100644 index 0000000..a9587e6 --- /dev/null +++ b/src/components/v-kanban/hooks/useIsClient.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react' + +/** + * Hook de React para determinar si el entorno actual es del lado del cliente. + * + * En el contexto de aplicaciones de React, especialmente aquellas que utilizan + * renderización en el lado del servidor (SSR), es crucial identificar si el + * código se está ejecutando en el entorno del cliente (navegador) o del servidor. + * Este hook proporciona una manera confiable de determinar este contexto, + * permitiendo ejecutar código específico del lado del cliente de manera segura. + * + * El hook utiliza `useState` para establecer el estado inicial basado en la + * disponibilidad del objeto `window`, un indicativo claro del entorno del cliente. + * A través de `useEffect`, ajusta este estado solo cuando el componente se monta + * en el navegador, garantizando una detección precisa y evitando ejecuciones + * innecesarias en el lado del servidor. + * + * @returns {boolean} Retorna `true` si el código se está ejecutando en el entorno del cliente. + * De lo contrario, retorna `false`. + * + * @example + * import React from 'react'; + * import useIsClient from './useIsClient'; + * + * const MyComponent: React.FC = () => { + * const isClient = useIsClient(); + * + * if (isClient) { + * // Ejemplo: Código seguro para ejecutar en el lado del cliente + * console.log('Ancho de la ventana:', window.innerWidth); + * } + * + * return
Contenido del componente
; + * }; + * + * export default MyComponent; + */ +export const useIsClient = (): boolean => { + const [isClient, setIsClient] = useState(typeof window === 'object') + + useEffect(() => { + // Este efecto no se ejecutará en el servidor, solo se activa en el entorno del cliente. + if (!isClient) { + setIsClient(true) + } + }, [isClient]) + + return isClient +} diff --git a/src/components/v-kanban/hooks/useScreenType.ts b/src/components/v-kanban/hooks/useScreenType.ts new file mode 100644 index 0000000..21e3007 --- /dev/null +++ b/src/components/v-kanban/hooks/useScreenType.ts @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useState } from 'react' + +interface ScreenType { + isLargeDesktop: boolean; + isDesktop: boolean; + isMobile: boolean; + isTablet: boolean; +} + +/** + * Hook personalizado de React para determinar el tipo de pantalla basado en el ancho del viewport. + * + * Este hook es útil para el diseño responsivo, permitiendo ajustar la interfaz de usuario + * y la lógica del componente según el tamaño de la pantalla. Utiliza la API de `window` para + * escuchar cambios en el tamaño de la ventana y ajusta el estado del tipo de pantalla en consecuencia. + * + * Los breakpoints para mobile, tablet, desktop y large desktop son configurables según las necesidades + * del proyecto. + * + * @returns {ScreenType} Objeto que contiene booleanos para cada tipo de pantalla (mobile, tablet, desktop, largeDesktop). + * + * @example + * import React from 'react'; + * import useScreenType from './useScreenType'; + * + * const MyComponent: React.FC = () => { + * const screenType = useScreenType(); + * + * return ( + *
+ * {screenType.isMobile ? : } + *
+ * ); + * }; + * + * export default MyComponent; + */ +export const useScreenType = (): ScreenType => { + // Estado inicial para SSR o cliente + const initialState: ScreenType = { + isLargeDesktop: false, + isDesktop: false, + isMobile: true, + isTablet: false + } + + const [screenType, setScreenType] = useState(initialState) + + const getScreenType = useCallback((): ScreenType => { + if (typeof window !== 'undefined') { + const width = window.innerWidth + return { + isLargeDesktop: width >= 1200, + isDesktop: width >= 992 && width < 1200, + isTablet: width >= 768 && width < 992, + isMobile: width < 768 + } + } + return initialState + }, []) + + useEffect(() => { + if (typeof window !== 'undefined') { + const handleResize = () => { + setScreenType(getScreenType()) + } + + handleResize() + + window.addEventListener('resize', handleResize) + + return () => window.removeEventListener('resize', handleResize) + } + }, [getScreenType]) + + return screenType +} diff --git a/src/components/v-kanban/hooks/useWindowSize.ts b/src/components/v-kanban/hooks/useWindowSize.ts new file mode 100644 index 0000000..1fe1841 --- /dev/null +++ b/src/components/v-kanban/hooks/useWindowSize.ts @@ -0,0 +1,46 @@ +import React from 'react' + +/** + * Hook de React para obtener el ancho y alto de la ventana del navegador. + * + * @example + * + * import React from 'react' + * + * const MyComponent: React.FC = () => { + * const { width, height } = useWindowSize() + * return ( + *
+ *

Ancho: {width}

+ *

Alto: {height}

+ *
+ * )} + * + * export default MyComponent + * + * @returns {object} Retorna un objeto con las propiedades `width` y `height` que representan el ancho y alto de la ventana del navegador. + */ +export function useWindowSize (): { width: number, height: number } { + const [size, setSize] = React.useState({ + width: null, + height: null + }) + + React.useLayoutEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight + }) + } + + handleResize() + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return size +} diff --git a/src/components/v-kanban/index.jsx b/src/components/v-kanban/index.jsx new file mode 100644 index 0000000..64629ae --- /dev/null +++ b/src/components/v-kanban/index.jsx @@ -0,0 +1 @@ +export { KanbanBoard } from './components/KanbanBoard' diff --git a/src/components/v-kanban/lib/utils.js b/src/components/v-kanban/lib/utils.js new file mode 100644 index 0000000..d3eec68 --- /dev/null +++ b/src/components/v-kanban/lib/utils.js @@ -0,0 +1,13 @@ +import { twMerge } from 'tailwind-merge' + +export function cn (...inputs) { + const classes = inputs + .filter((input) => input) + .flatMap((input) => + typeof input === 'object' + ? Object.entries(input).filter(([, value]) => value).map(([key]) => key) + : input.split(' ') + ) + + return twMerge(...classes) +} diff --git a/src/components/v-kanban/multipleContainersKeyboardPreset.ts b/src/components/v-kanban/multipleContainersKeyboardPreset.ts new file mode 100644 index 0000000..802d648 --- /dev/null +++ b/src/components/v-kanban/multipleContainersKeyboardPreset.ts @@ -0,0 +1,109 @@ +import { + closestCorners, + getFirstCollision, + KeyboardCode, + DroppableContainer, + KeyboardCoordinateGetter +} from '@dnd-kit/core' + +const directions: string[] = [ + KeyboardCode.Down, + KeyboardCode.Right, + KeyboardCode.Up, + KeyboardCode.Left +] + +export const coordinateGetter: KeyboardCoordinateGetter = ( + event, + { context: { active, droppableRects, droppableContainers, collisionRect } } +) => { + if (directions.includes(event.code)) { + event.preventDefault() + + if (!active || !collisionRect) { + return + } + + const filteredContainers: DroppableContainer[] = [] + + droppableContainers.getEnabled().forEach((entry) => { + if (!entry || entry?.disabled) { + return + } + + const rect = droppableRects.get(entry.id) + + if (!rect) { + return + } + + const data = entry.data.current + + if (data) { + const { type, children } = data + + if (type === 'Column' && children?.length > 0) { + if (active.data.current?.type !== 'Column') { + return + } + } + } + + switch (event.code) { + case KeyboardCode.Down: + if (active.data.current?.type === 'Column') { + return + } + if (collisionRect.top < rect.top) { + // find all droppable areas below + filteredContainers.push(entry) + } + break + case KeyboardCode.Up: + if (active.data.current?.type === 'Column') { + return + } + if (collisionRect.top > rect.top) { + // find all droppable areas above + filteredContainers.push(entry) + } + break + case KeyboardCode.Left: + if (collisionRect.left >= rect.left + rect.width) { + // find all droppable areas to left + filteredContainers.push(entry) + } + break + case KeyboardCode.Right: + // find all droppable areas to right + if (collisionRect.left + collisionRect.width <= rect.left) { + filteredContainers.push(entry) + } + break + } + }) + const collisions = closestCorners({ + active, + collisionRect, + droppableRects, + droppableContainers: filteredContainers, + pointerCoordinates: null + }) + const closestId = getFirstCollision(collisions, 'id') + + if (closestId != null) { + const newDroppable = droppableContainers.get(closestId) + const newNode = newDroppable?.node.current + const newRect = newDroppable?.rect.current + + if (newNode && newRect) { + return { + x: newRect.left, + y: newRect.top + } + } + } + } + + return undefined +} diff --git a/src/components/v-kanban/utils.ts b/src/components/v-kanban/utils.ts new file mode 100644 index 0000000..3254917 --- /dev/null +++ b/src/components/v-kanban/utils.ts @@ -0,0 +1,23 @@ +import { Active, DataRef, Over } from '@dnd-kit/core' +import { ColumnDragData } from './components/BoardColumn' +import { TaskDragData } from './components/TaskCard' + +type DraggableData = ColumnDragData | TaskDragData; + +export function hasDraggableData ( + entry: T | null | undefined +): entry is T & { + data: DataRef; +} { + if (!entry) { + return false + } + + const data = entry.data.current + + if (data?.type === 'Column' || data?.type === 'Task') { + return true + } + + return false +} diff --git a/src/components/vCalendar/index.jsx b/src/components/vCalendar/index.jsx new file mode 100644 index 0000000..be11ed6 --- /dev/null +++ b/src/components/vCalendar/index.jsx @@ -0,0 +1,229 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Calendar, momentLocalizer } from 'react-big-calendar' +import moment from 'moment' +import Toolbar from 'react-big-calendar/lib/Toolbar' +import { FaCalendarDay, FaChevronLeft, FaChevronRight } from 'react-icons/fa' +import { DocumentPlusIcon } from '@heroicons/react/24/solid' + +VCalendar.PropTypes = { + children: PropTypes.node, + name: PropTypes.string, + headers: PropTypes.array, + events: PropTypes.array, + model: PropTypes.object, + isEdit: PropTypes.bool, + onSave: PropTypes.func, + onCancel: PropTypes.func, + reactBigCalendar: PropTypes.func, + onSuccessUpload: PropTypes.func, + tooblar: PropTypes.object, + showSaveNew: PropTypes.bool, + showCancel: PropTypes.bool, + cols: PropTypes.number, + i18n: PropTypes.object, + presets: PropTypes.object +} + +VCalendar.defaultProps = { + children: null, + name: '', + headers: [], + events: [], + model: {}, + isEdit: false, + onSave: () => {}, + onCancel: () => {}, + reactBigCalendar: () => {}, + onSuccessUpload: () => {}, + tooblar: {}, + showSaveNew: true, + showCancel: true, + cols: 1, + i18n: {}, + presets: {} +} + +export default function VCalendar ({ children, name, headers, events, model, isEdit, onSave, onCancel, onSuccessUpload, showSaveNew = true, showCancel = true, cols = 1, i18n = { t: () => { return 'txtNone' } }, presets }) { + moment.locale('es', { + month: { + dow: 1 // for starrting day from monday + } + }) + const localizer = momentLocalizer(moment) + const views = ['day', 'week', 'month', 'agenda'] + + const EventComponent = ({ events, change }) => (props) => { + // console.log('change', events, change) + return ( +
+

{props.event.title}

+

{props.event.observaciones}

+ {/* */} +
+ ) + } + + const handleDayChange = (event, mconte) => { + mconte(event.target.value) + } + const handleNamvigate = (detail, elem) => { + detail.navigate(elem) + } + + const CustomToolbar = ({ handleChange }) => { + return class BaseToolBar extends Toolbar { + constructor (props) { + super(props) + } + + handleDayChange = (event, mconte) => { + mconte(event.target.value) + } + + handleNamvigate = (detail, elem) => { + detail.navigate(elem) + } + + handleNavigateToEvent = () => { + onSave() + } + + render () { + return ( +
+ {/* */} +
+ +
+ +
+ + +
+ + {/* */} +
+ +
+ + {/* */} +
+ +
+ + {/* */} +
+ +
+
+ ) + } + } + } + + const eventStyleGetter = (event, start, end, isSelected) => { + const backgroundColor = '#' + Math.floor(Math.random() * 16777215).toString(16) + const style = { + backgroundColor: event.color ? event.color : backgroundColor, + borderRadius: '5px', + opacity: 1, + color: 'white', + border: '5px', + display: 'block' + } + return { + style + } + } + const handleNavigation = (date, view, action) => { + console.log(date, view, action) + // it returns current date, view options[month,day,week,agenda] and action like prev, next or today + } + const handleEventSelection = (e) => { + console.log(e) + } + + const handleChange = () => { + console.log(true) + } + + const openPopupForm = (slotInfo) => { + console.log(true) + } + + const onSelectEventSlotHandler = (slotInfo) => { + openPopupForm(slotInfo) // OPEN POPUP FOR CREATE/EDIT EVENT + } + + const change = handleChange + return ( +
+ + + localizer.format(date, 'dddd D', culture) + }} + dayLayoutAlgorithm="no-overlap" + dayStyle={{ + backgroundColor: '#fff', // Fondo blanco para la vista de día + borderBottom: '2px solid #ddd', // Línea de separación entre días + padding: '10px', // Ajusta el relleno para un aspecto más espaciado + boxShadow: '0 0 5px rgba(0, 0, 0, 0.1)', // Sombra suave + borderRadius: '5px' // Bordes redondeados + }} + views={views} + events={events} + style={{ height: 600 }} + showMultiDayTimes + onSelectSlot={event => { onSelectEventSlotHandler(event) }} + culture={['es']} + onNavigate={handleNavigation} + onSelectEvent={handleEventSelection} + components={{ + event: EventComponent({ events, change }), + // data as events array and change is custom method passed into component(for perform any functionality on parent state) + toolbar: CustomToolbar({ events, change }) + }} + eventPropGetter={(eventStyleGetter)} + // onSelectSlot={(slotInfo) => onSelectEventSlotHandler(slotInfo)} + /> +
) +} diff --git a/src/data/menu.json b/src/data/menu.json index 45774c8..ecb9217 100644 --- a/src/data/menu.json +++ b/src/data/menu.json @@ -39,13 +39,23 @@ "id_menu_padre": null, "badge": "New" }, + { + "id_menu": "button", + "title": "Button", + "descripcion": null, + "icon": "FcRotateToPortrait", + "path": "/via-ui/button", + "orden": 2.1, + "id_menu_padre": "via-ui", + "badge": null + }, { "id_menu": "tooltip", "title": "Tooltip", "descripcion": null, "icon": "FcMms", "path": "/via-ui/tooltip", - "orden": 2.1, + "orden": 2.2, "id_menu_padre": "via-ui", "badge": null }, @@ -55,7 +65,17 @@ "descripcion": null, "icon": "FcTemplate", "path": "/via-ui/tab", - "orden": 2.1, + "orden": 2.3, + "id_menu_padre": "via-ui", + "badge": null + }, + { + "id_menu": "stepper", + "title": "Stepper", + "descripcion": null, + "icon": "FcSerialTasks", + "path": "/via-ui/stepper", + "orden": 2.4, "id_menu_padre": "via-ui", "badge": null }, @@ -65,8 +85,68 @@ "descripcion": null, "icon": "FcIdea", "path": "/via-ui/test", - "orden": 2.1, + "orden": 2.40, "id_menu_padre": "via-ui", "badge": null + }, + { + "id_menu": "v-calendar", + "title": "v-calendar", + "descripcion": null, + "icon": "FcCalendar", + "path": null, + "orden": 3, + "id_menu_padre": null, + "badge": null + }, + { + "id_menu": "VCalendar", + "title": "VCalendar", + "descripcion": null, + "icon": "FcCalendar", + "path": "/v-calendar/VCalendar", + "orden": 3.1, + "id_menu_padre": "v-calendar", + "badge": null + }, + { + "id_menu": "v-kanban", + "title": "v-kanban", + "descripcion": null, + "icon": "FcTemplate", + "path": null, + "orden": 4, + "id_menu_padre": null, + "badge": null + }, + { + "id_menu": "KanbanBoard", + "title": "KanbanBoard", + "descripcion": null, + "icon": "FcTemplate", + "path": "/v-kanban/KanbanBoard", + "orden": 4.1, + "id_menu_padre": "v-kanban", + "badge": null + }, + { + "id_menu": "test", + "title": "Test", + "descripcion": null, + "icon": "FcBiomass", + "path": null, + "orden": 5, + "id_menu_padre": null, + "badge": null + }, + { + "id_menu": "dndkit", + "title": "dnd kit", + "descripcion": null, + "icon": "FcCollect", + "path": "/test/dndkit", + "orden": 5.1, + "id_menu_padre": "test", + "badge": null } ] \ No newline at end of file diff --git a/src/pages/test/dndkit/[...index].jsx b/src/pages/test/dndkit/[...index].jsx new file mode 100644 index 0000000..909c48c --- /dev/null +++ b/src/pages/test/dndkit/[...index].jsx @@ -0,0 +1,11 @@ +import VKanban from '@/components/v-kanban' + +const DndKitPage = () => { + return ( +
+ +
+ ) +} + +export default DndKitPage diff --git a/src/pages/v-calendar/VCalendar/[...index].jsx b/src/pages/v-calendar/VCalendar/[...index].jsx new file mode 100644 index 0000000..acffe7d --- /dev/null +++ b/src/pages/v-calendar/VCalendar/[...index].jsx @@ -0,0 +1,14 @@ +import VCalendar from '@/components/vCalendar' +import React from 'react' + +const VCalendarPage = () => { + return ( + <> + { alert('onSave') }} + /> + + ) +} + +export default VCalendarPage diff --git a/src/pages/v-kanban/KanbanBoard/[...index].jsx b/src/pages/v-kanban/KanbanBoard/[...index].jsx new file mode 100644 index 0000000..d243bf2 --- /dev/null +++ b/src/pages/v-kanban/KanbanBoard/[...index].jsx @@ -0,0 +1,41 @@ +import CodePlayground from '@/components/CodePlayground' +import React, { useState } from 'react' +import { KanbanBoard } from 'v-kanban' + +const initialCode = +`//import { KanbanBoard } from 'v-kanban' + +const columns = [ + { id: 1, title: 'To Do', items: [{ id: 1, content: 'Task 1' }, { id: 2, content: 'Task 2' }] }, + { id: 2, title: 'In Progress', items: [{ id: 3, content: 'Task 3' }] }, + { id: 3, title: 'Done', items: [{ id: 4, content: 'Task 4' }] } +] + +const KanbanBoardExample = () => { + return ( + + ) +} + +render() +` + +const KanbanBoardPage = () => { + const [code, setCode] = useState(initialCode) + + const scope = { + React, + useState, + KanbanBoard + } + + return ( +
+ { setCode(newCode) }} scope={scope} /> +
+ ) +} + +export default KanbanBoardPage diff --git a/src/pages/via-ui/button/[...index].jsx b/src/pages/via-ui/button/[...index].jsx new file mode 100644 index 0000000..b55bb49 --- /dev/null +++ b/src/pages/via-ui/button/[...index].jsx @@ -0,0 +1,39 @@ +import CodePlayground from '@/components/CodePlayground' +import React, { useState } from 'react' +import Button from 'via-ui/button' + +const initialCode = +`// import { Button } from 'via-ui' + + const ButtonExample = () => { + return ( + <> + + + ); + }; + + render(); +` + +const ExampleComponentPage = () => { + const [code, setCode] = useState(initialCode) + + const scope = { + React, + useState, + Button + } + + return ( + <> +
+ { setCode(newCode) }} scope={scope} /> +
+ + ) +} + +export default ExampleComponentPage diff --git a/src/pages/via-ui/stepper/[...index].jsx b/src/pages/via-ui/stepper/[...index].jsx new file mode 100644 index 0000000..d1c1cea --- /dev/null +++ b/src/pages/via-ui/stepper/[...index].jsx @@ -0,0 +1,86 @@ +import CodePlayground from '@/components/CodePlayground' +import React, { useState } from 'react' +import { Stepper, StepperList, StepperTrigger, StepContent, StepLabel } from 'via-ui/stepper' + +const initialCode = ` +// import { Stepper, StepperList, StepperTrigger, StepContent, StepLabel, Button } from 'via-ui/stepper' + +const StepperExample = () => { + const [activeStep, setActiveStep] = useState('step1'); + const steps = ['step1', 'step2', 'step3']; + + const handleStepChange = (step) => { + setActiveStep(step); + }; + + const handleNext = () => { + const currentIndex = steps.indexOf(activeStep); + if (currentIndex < steps.length - 1) { + setActiveStep(steps[currentIndex + 1]); + } + }; + + const handleBack = () => { + const currentIndex = steps.indexOf(activeStep); + if (currentIndex > 0) { + setActiveStep(steps[currentIndex - 1]); + } + }; + + return ( + + + {steps.map((step) => ( + handleStepChange(step)} + isClickable={true} + > + {'Step ' + (steps.indexOf(step) + 1)} + + ))} + + + +

{'Content for ' + activeStep}

+
+ + +
+
+
+ ); +}; + +render(); +` + +const ExampleComponentPage = () => { + const [code, setCode] = useState(initialCode) + + // El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita + const scope = { + React, + useState, + Stepper, + StepperList, + StepperTrigger, + StepContent, + StepLabel + } + + return ( + <> +
+ { setCode(newCode) }} scope={scope} /> +
+ + ) +} + +export default ExampleComponentPage diff --git a/src/pages/via-ui/tab/[...index].jsx b/src/pages/via-ui/tab/[...index].jsx index d465f67..d2c6340 100644 --- a/src/pages/via-ui/tab/[...index].jsx +++ b/src/pages/via-ui/tab/[...index].jsx @@ -1,29 +1,55 @@ -import React from 'react' +import CodePlayground from '@/components/CodePlayground' +import React, { useState } from 'react' + import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' -const TabPage = () => { - const [tab, setTab] = React.useState('data') +const initialCode = +`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' + + const TabsExample = () => { + const [tab, setTab] = React.useState('data'); + return ( + <> + + + Datos del Evento + Documentos del Evento + + + + tabs data + + + + tabs docs + + + + ); + }; + + render(); +` + +const TabExamplePage = () => { + const [code, setCode] = useState(initialCode) + + const scope = { + React, + useState, + Tabs, + TabsContent, + TabsList, + TabsTrigger + } return ( - - - setTab('data')}> - Datos del Evento - - setTab('docs')}> - Documentos del Evento - - - - - tabs data - - - - tabs docs - - + <> +
+ { setCode(newCode) }} scope={scope} /> +
+ ) } -export default TabPage +export default TabExamplePage diff --git a/src/pages/via-ui/test/[...index].jsx b/src/pages/via-ui/test/[...index].jsx index 72c1a13..d2c5d03 100644 --- a/src/pages/via-ui/test/[...index].jsx +++ b/src/pages/via-ui/test/[...index].jsx @@ -1,56 +1,55 @@ -import CodePlayground from '@/components/CodePlayground' -import React, { useState } from 'react' +import React from 'react' +import { SandpackProvider, SandpackLayout, SandpackCodeEditor, SandpackPreview } from '@codesandbox/sandpack-react' -import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' - -const initialCode = -`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' - - const TabsExample = () => { - const [tab, setTab] = React.useState('data'); - return ( - <> - - - Datos del Evento - Documentos del Evento - - - - tabs data - - - - tabs docs - - - - ); - }; - - render(); +const tabComponentCode = ` + import React, { useState } from 'react'; + import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'; ` -const ExampleComponentPage = () => { - const [code, setCode] = useState(initialCode) +const files = { + '/App.js': { + code: ` - // El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita - const scope = { - React, - useState, - Tabs, - TabsContent, - TabsList, - TabsTrigger + const TabsExample = () => { + const [tab, setTab] = useState('data'); + return ( + <> + + + Datos del Evento + Documentos del Evento + + + + tabs data + + + + tabs docs + + + + ); + }; + + export default TabsExample;` + }, + '/node_modules/via-ui/tab/index.js': { + hidden: true, + code: tabComponentCode } +} +const TestPage = () => { + console.log('tabComponentCode', tabComponentCode) return ( - <> -
- { setCode(newCode) }} scope={scope} /> -
- + + + + + + ) } -export default ExampleComponentPage +export default TestPage diff --git a/src/styles/additional-styles/react-big-calendar.css b/src/styles/additional-styles/react-big-calendar.css new file mode 100644 index 0000000..760205a --- /dev/null +++ b/src/styles/additional-styles/react-big-calendar.css @@ -0,0 +1,695 @@ +.rbc-btn { + color: inherit; + font: inherit; + margin: 0; +} +button.rbc-btn { + overflow: visible; + text-transform: none; + -webkit-appearance: button; + cursor: pointer; +} +button[disabled].rbc-btn { + cursor: not-allowed; +} +button.rbc-input::-moz-focus-inner { + border: 0; + padding: 0; +} +.rbc-calendar { + box-sizing: border-box; + height: 100%; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; +} +.rbc-calendar *, +.rbc-calendar *:before, +.rbc-calendar *:after { + box-sizing: inherit; +} +.rbc-abs-full, +.rbc-row-bg { + overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.rbc-ellipsis, +.rbc-event-label, +.rbc-row-segment .rbc-event-content, +.rbc-show-more { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.rbc-rtl { + direction: rtl; +} +.rbc-off-range { + color: #999999; +} +.rbc-off-range-bg { + background: #e5e5e5; +} +.rbc-header { + overflow: hidden; + -webkit-flex: 1 0 0%; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 3px; + text-align: center; + vertical-align: middle; + font-weight: bold; + font-size: 90%; + min-height: 0; + border-bottom: 1px solid #DDD; +} +.rbc-header + .rbc-header { + border-left: 1px solid #DDD; +} +.rbc-rtl .rbc-header + .rbc-header { + border-left-width: 0; + border-right: 1px solid #DDD; +} +.rbc-header > a, +.rbc-header > a:active, +.rbc-header > a:visited { + color: inherit; + text-decoration: none; +} +.rbc-row-content { + position: relative; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + z-index: 4; +} +.rbc-today { + background-color: #eaf6ff; +} +.rbc-toolbar { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + margin-bottom: 10px; + font-size: 16px; +} +.rbc-toolbar .rbc-toolbar-label { + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 0 10px; + text-align: center; +} +.rbc-toolbar button { + color: #373a3c; + display: inline-block; + margin: 0; + text-align: center; + vertical-align: middle; + background: none; + background-image: none; + border: 1px solid #ccc; + padding: .375rem 1rem; + border-radius: 4px; + line-height: normal; + white-space: nowrap; +} +.rbc-toolbar button:active, +.rbc-toolbar button.rbc-active { + background-image: none; + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + background-color: #e6e6e6; + border-color: #adadad; +} +.rbc-toolbar button:active:hover, +.rbc-toolbar button.rbc-active:hover, +.rbc-toolbar button:active:focus, +.rbc-toolbar button.rbc-active:focus { + color: #373a3c; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.rbc-toolbar button:focus { + color: #373a3c; + background-color: #e6e6e6; + border-color: #adadad; +} +.rbc-toolbar button:hover { + color: #373a3c; + background-color: #e6e6e6; + border-color: #adadad; +} +.rbc-btn-group { + display: inline-block; + white-space: nowrap; +} +.rbc-btn-group > button:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-btn-group > button:last-child:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.rbc-rtl .rbc-btn-group > button:first-child:not(:last-child) { + border-radius: 4px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.rbc-rtl .rbc-btn-group > button:last-child:not(:first-child) { + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-btn-group > button:not(:first-child):not(:last-child) { + border-radius: 0; +} +.rbc-btn-group button + button { + margin-left: -1px; +} +.rbc-rtl .rbc-btn-group button + button { + margin-left: 0; + margin-right: -1px; +} +.rbc-btn-group + .rbc-btn-group, +.rbc-btn-group + button { + margin-left: 10px; +} +.rbc-event { + border: none; + box-shadow: none; + margin: 0; + padding: 2px 5px; + background-color: #3174ad; + border-radius: 5px; + color: #fff; + cursor: pointer; + width: 100%; + text-align: left; +} +.rbc-slot-selecting .rbc-event { + cursor: inherit; + pointer-events: none; +} +.rbc-event.rbc-selected { + background-color: #265985; +} +.rbc-event:focus { + outline: 0px auto #3b99fc; +} +.rbc-event-label { + font-size: 80%; +} +.rbc-event-overlaps { + box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5); +} +.rbc-event-continues-prior { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.rbc-event-continues-after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-event-continues-earlier { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.rbc-event-continues-later { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.rbc-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} +.rbc-row-segment { + padding: 0 1px 1px 1px; +} +.rbc-selected-cell { + background-color: rgba(0, 0, 0, 0.1); +} +.rbc-show-more { + background-color: rgba(255, 255, 255, 0.3); + z-index: 4; + font-weight: bold; + font-size: 85%; + height: auto; + line-height: normal; + white-space: nowrap; +} +.rbc-month-view { + position: relative; + border: 1px solid #DDD; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; + width: 100%; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + height: 100%; +} +.rbc-month-header { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} +.rbc-month-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + position: relative; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; + -webkit-flex-basis: 0px; + -ms-flex-preferred-size: 0px; + flex-basis: 0px; + overflow: hidden; + height: 100%; +} +.rbc-month-row + .rbc-month-row { + border-top: 1px solid #DDD; +} +.rbc-date-cell { + -webkit-flex: 1 1 0; + -ms-flex: 1 1 0px; + flex: 1 1 0; + min-width: 0; + padding-right: 5px; + text-align: right; +} +.rbc-date-cell.rbc-now { + font-weight: bold; +} +.rbc-date-cell > a, +.rbc-date-cell > a:active, +.rbc-date-cell > a:visited { + color: inherit; + text-decoration: none; +} +.rbc-row-bg { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: hidden; +} +.rbc-day-bg { + -webkit-flex: 1 0 0%; + -ms-flex: 1 0 0%; + flex: 1 0 0%; +} +.rbc-day-bg + .rbc-day-bg { + border-left: 1px solid #DDD; +} +.rbc-rtl .rbc-day-bg + .rbc-day-bg { + border-left-width: 0; + border-right: 1px solid #DDD; +} +.rbc-overlay { + position: absolute; + z-index: 5; + border: 1px solid #e5e5e5; + background-color: #fff; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25); + padding: 10px; +} +.rbc-overlay > * + * { + margin-top: 1px; +} +.rbc-overlay-header { + border-bottom: 1px solid #e5e5e5; + margin: -10px -10px 5px -10px; + padding: 2px 10px; +} +.rbc-agenda-view { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: auto; +} +.rbc-agenda-view table.rbc-agenda-table { + width: 100%; + border: 1px solid #DDD; + border-spacing: 0; + border-collapse: collapse; +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr > td { + padding: 5px 10px; + vertical-align: top; +} +.rbc-agenda-view table.rbc-agenda-table .rbc-agenda-time-cell { + padding-left: 15px; + padding-right: 15px; + text-transform: lowercase; +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td { + border-left: 1px solid #DDD; +} +.rbc-rtl .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td { + border-left-width: 0; + border-right: 1px solid #DDD; +} +.rbc-agenda-view table.rbc-agenda-table tbody > tr + tr { + border-top: 1px solid #DDD; +} +.rbc-agenda-view table.rbc-agenda-table thead > tr > th { + padding: 3px 5px; + text-align: left; + border-bottom: 1px solid #DDD; +} +.rbc-rtl .rbc-agenda-view table.rbc-agenda-table thead > tr > th { + text-align: right; +} +.rbc-agenda-time-cell { + text-transform: lowercase; +} +.rbc-agenda-time-cell .rbc-continues-after:after { + content: ' »'; +} +.rbc-agenda-time-cell .rbc-continues-prior:before { + content: '« '; +} +.rbc-agenda-date-cell, +.rbc-agenda-time-cell { + white-space: nowrap; +} +.rbc-agenda-event-cell { + width: 100%; +} +.rbc-time-column { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + min-height: 100%; +} +.rbc-time-column .rbc-timeslot-group { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +.rbc-timeslot-group { + border-bottom: 1px solid #DDD; + min-height: 40px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; +} +.rbc-time-gutter, +.rbc-header-gutter { + -webkit-flex: none; + -ms-flex: none; + flex: none; +} +.rbc-label { + padding: 0 5px; +} +.rbc-day-slot { + position: relative; +} +.rbc-day-slot .rbc-events-container { + bottom: 0; + left: 0; + position: absolute; + right: 0; + margin-right: 10px; + top: 0; +} +.rbc-day-slot .rbc-events-container.rbc-is-rtl { + left: 10px; + right: 0; +} +.rbc-day-slot .rbc-event { + border: 1px solid #265985; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + max-height: 100%; + min-height: 20px; + -webkit-flex-flow: column wrap; + -ms-flex-flow: column wrap; + flex-flow: column wrap; + -webkit-align-items: flex-start; + -ms-flex-align: start; + align-items: flex-start; + overflow: hidden; + position: absolute; +} +.rbc-day-slot .rbc-event-label { + -webkit-flex: none; + -ms-flex: none; + flex: none; + padding-right: 5px; + width: auto; +} +.rbc-day-slot .rbc-event-content { + width: 100%; + -webkit-flex: 1 1 0; + -ms-flex: 1 1 0px; + flex: 1 1 0; + word-wrap: break-word; + line-height: 1; + height: 100%; + min-height: 1em; +} +.rbc-day-slot .rbc-time-slot { + border-top: 1px solid #f7f7f7; +} +.rbc-time-view-resources .rbc-time-gutter, +.rbc-time-view-resources .rbc-time-header-gutter { + position: -webkit-sticky; + position: sticky; + left: 0; + background-color: white; + border-right: 1px solid #DDD; + z-index: 10; + margin-right: -1px; +} +.rbc-time-view-resources .rbc-time-header { + overflow: hidden; +} +.rbc-time-view-resources .rbc-time-header-content { + min-width: auto; + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; + -webkit-flex-basis: 0px; + -ms-flex-preferred-size: 0px; + flex-basis: 0px; +} +.rbc-time-view-resources .rbc-day-slot { + min-width: 140px; +} +.rbc-time-view-resources .rbc-header, +.rbc-time-view-resources .rbc-day-bg { + width: 140px; + -webkit-flex: 1 1 0; + -ms-flex: 1 1 0px; + flex: 1 1 0; + -webkit-flex-basis: 0 px; + -ms-flex-preferred-size: 0 px; + flex-basis: 0 px; +} +.rbc-time-header-content + .rbc-time-header-content { + margin-left: -1px; +} +.rbc-time-slot { + -webkit-flex: 1 0 0; + -ms-flex: 1 0 0px; + flex: 1 0 0; +} +.rbc-time-slot.rbc-now { + font-weight: bold; +} +.rbc-day-header { + text-align: center; +} +.rbc-slot-selection { + z-index: 10; + position: absolute; + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-size: 75%; + width: 100%; + padding: 3px; +} +.rbc-slot-selecting { + cursor: move; +} +.rbc-time-view { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + width: 100%; + border: 1px solid #DDD; + min-height: 0; +} +.rbc-time-view .rbc-time-gutter { + white-space: nowrap; +} +.rbc-time-view .rbc-allday-cell { + box-sizing: content-box; + width: 100%; + height: 100%; + position: relative; +} +.rbc-time-view .rbc-allday-cell + .rbc-allday-cell { + border-left: 1px solid #DDD; +} +.rbc-time-view .rbc-allday-events { + position: relative; + z-index: 4; +} +.rbc-time-view .rbc-row { + box-sizing: border-box; + min-height: 20px; +} +.rbc-time-header { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 0 0 auto; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} +.rbc-time-header.rbc-overflowing { + border-right: 1px solid #DDD; +} +.rbc-rtl .rbc-time-header.rbc-overflowing { + border-right-width: 0; + border-left: 1px solid #DDD; +} +.rbc-time-header > .rbc-row:first-child { + border-bottom: 1px solid #DDD; +} +.rbc-time-header > .rbc-row.rbc-row-resource { + border-bottom: 1px solid #DDD; +} +.rbc-time-header-content { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-width: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + border-left: 1px solid #DDD; +} +.rbc-rtl .rbc-time-header-content { + border-left-width: 0; + border-right: 1px solid #DDD; +} +.rbc-time-content { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 0 0%; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + -webkit-align-items: flex-start; + -ms-flex-align: start; + align-items: flex-start; + width: 100%; + border-top: 2px solid #DDD; + overflow-y: auto; + position: relative; +} +.rbc-time-content > .rbc-time-gutter { + -webkit-flex: none; + -ms-flex: none; + flex: none; +} +.rbc-time-content > * + * > * { + border-left: 1px solid #DDD; +} +.rbc-rtl .rbc-time-content > * + * > * { + border-left-width: 0; + border-right: 1px solid #DDD; +} +.rbc-time-content > .rbc-day-slot { + width: 100%; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; +} +.rbc-current-time-indicator { + position: absolute; + z-index: 3; + left: 0; + right: 0; + height: 1px; + background-color: #74ad31; + pointer-events: none; +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 0eb0451..fb74989 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -82,6 +82,7 @@ /* Additional Styles */ @import 'additional-styles/toastify.css'; +@import 'additional-styles/react-big-calendar.css'; @import 'component-styles/loading.css'; @import 'component-styles/sidebar.css'; @import 'component-styles/navbar.css'; diff --git a/tailwind.config.ts b/tailwind.config.ts index 1d30e23..28d39d8 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -61,6 +61,15 @@ const config: Config = { blue: '2px solid rgba(0, 112, 244, 0.5)' }, colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, 'theme-text': { principal: '#323338', disabled: '#cbd5e1'