feat: implement ActivitiesCard component to display recent activities with enhanced status and time display
Support the new Status and Performed By
This commit is contained in:
42
src/views/components/SmartTaskTitleInput.css
Normal file
42
src/views/components/SmartTaskTitleInput.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.smart-task-display {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow-y: auto;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.smart-task-common {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
font-family: inherit;
|
||||
caret-color: #f08080;
|
||||
}
|
||||
|
||||
.highlight-date {
|
||||
color: #f08080;
|
||||
}
|
||||
|
||||
.highlight-repeat {
|
||||
color: #90ee90;
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
color: #add8e6;
|
||||
}
|
||||
|
||||
.highlight-priority {
|
||||
color: #ffb6c1;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
273
src/views/components/SmartTaskTitleInput.jsx
Normal file
273
src/views/components/SmartTaskTitleInput.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { useColorScheme } from '@mui/joy'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AutocompleteDropdown from '../TestView/AutocompleteDropdown'
|
||||
import './SmartTaskTitleInput.css'
|
||||
const renderHighlightedText = (text, cursorPosition) => {
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
|
||||
const regex = /(Tomorrow)|(#\w+)|(P\d)/gi
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const matchedText = match[0]
|
||||
const matchIndex = match.index
|
||||
|
||||
if (matchIndex > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, matchIndex))
|
||||
}
|
||||
|
||||
let className = ''
|
||||
if (matchedText.toLowerCase() === 'tomorrow') {
|
||||
className = 'highlight-date'
|
||||
} else if (matchedText.startsWith('#')) {
|
||||
className = 'highlight-label'
|
||||
} else if (matchedText.startsWith('P')) {
|
||||
className = 'highlight-priority'
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<span key={matchIndex} className={className}>
|
||||
{matchedText}
|
||||
</span>,
|
||||
)
|
||||
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex))
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
const SmartTaskTitleInput = ({
|
||||
value,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
onChange,
|
||||
suggestions,
|
||||
onEnterPressed,
|
||||
customRenderer,
|
||||
}) => {
|
||||
const { mode, setMode } = useColorScheme()
|
||||
const titleInputRef = useRef(null)
|
||||
const [cursorPosition, setCursorPosition] = useState(value?.length)
|
||||
const dropdownRef = useRef(null)
|
||||
const [lastWord, setLastWord] = useState('')
|
||||
const [suggestionTrigger, setSuggestionTrigger] = useState('P')
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setCursorPosition(prevPos => Math.min(prevPos, value.length))
|
||||
|
||||
if (
|
||||
titleInputRef.current &&
|
||||
document.activeElement === titleInputRef.current
|
||||
) {
|
||||
requestAnimationFrame(() => {
|
||||
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||
})
|
||||
}
|
||||
}, [value])
|
||||
useEffect(() => {
|
||||
// set focus on the input when the component is mounted:
|
||||
if (titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||
}
|
||||
}, [])
|
||||
const handleSuggestionChange = text => {
|
||||
// if the last word start with '@' or '#' or 'P':
|
||||
const lastWord = text.split(' ').pop()
|
||||
if (
|
||||
lastWord.startsWith('@') ||
|
||||
lastWord.startsWith('#') ||
|
||||
lastWord.startsWith('!')
|
||||
) {
|
||||
setSuggestionTrigger(lastWord[0])
|
||||
// last word without the first character:
|
||||
setLastWord(lastWord.slice(1))
|
||||
|
||||
setShowSuggestions(true)
|
||||
} else {
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextareaChange = e => {
|
||||
handleSuggestionChange(e.target.value)
|
||||
onChange(e.target.value)
|
||||
setCursorPosition(e.target.selectionStart)
|
||||
}
|
||||
|
||||
const handleTextareaKeyDown = e => {
|
||||
if (showSuggestions) {
|
||||
const currentSuggestions = suggestions[suggestionTrigger].options.filter(
|
||||
option => {
|
||||
if (typeof option === 'string') {
|
||||
return option.toLowerCase().includes(lastWord.toLowerCase())
|
||||
}
|
||||
return option[suggestions[suggestionTrigger].display]
|
||||
.toLowerCase()
|
||||
.includes(lastWord.toLowerCase())
|
||||
},
|
||||
)
|
||||
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
const newIndex =
|
||||
e.key === 'ArrowDown'
|
||||
? (selectedSuggestionIndex + 1) % currentSuggestions.length
|
||||
: (selectedSuggestionIndex - 1 + currentSuggestions.length) %
|
||||
currentSuggestions.length
|
||||
setSelectedSuggestionIndex(newIndex)
|
||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const selectedSuggestion = currentSuggestions[selectedSuggestionIndex]
|
||||
const suggestionValue = suggestions[suggestionTrigger].display
|
||||
? selectedSuggestion[suggestions[suggestionTrigger].display]
|
||||
: selectedSuggestion
|
||||
|
||||
if (suggestionValue) {
|
||||
const newValue = `${value.slice(0, cursorPosition - lastWord.length)}${suggestionValue} ${value.slice(cursorPosition)}`
|
||||
onChange(newValue)
|
||||
titleInputRef.current.value = newValue
|
||||
|
||||
setShowSuggestions(false)
|
||||
|
||||
const newCursorPosition = cursorPosition + suggestionValue.length + 1
|
||||
titleInputRef.current.setSelectionRange(
|
||||
newCursorPosition,
|
||||
newCursorPosition,
|
||||
)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
} else {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (onEnterPressed) {
|
||||
onEnterPressed(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentPos = e.target.selectionStart
|
||||
setCursorPosition(currentPos)
|
||||
}
|
||||
|
||||
const handleTextareaClick = e => {
|
||||
const currentPos = e.target.selectionStart
|
||||
setCursorPosition(currentPos)
|
||||
}
|
||||
|
||||
const handleDisplayClick = e => {
|
||||
const range = document.caretRangeFromPoint(e.clientX, e.clientY)
|
||||
if (range) {
|
||||
const offset = range.startOffset
|
||||
setCursorPosition(offset)
|
||||
|
||||
if (titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.setSelectionRange(offset, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='task-input overflow-auto rounded border'>
|
||||
<textarea
|
||||
ref={titleInputRef}
|
||||
autoFocus={autoFocus}
|
||||
rows={1}
|
||||
value={value}
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleTextareaKeyDown}
|
||||
onClick={handleTextareaClick}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
// opacity: 100,
|
||||
zIndex: 1,
|
||||
resize: 'none',
|
||||
overflow: 'hidden',
|
||||
padding: '0.5rem',
|
||||
boxSizing: 'border-box',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
|
||||
caretColor: mode === 'dark' ? '#fff' : '#000',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='smart-task-display smart-task-common'
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
minHeight: '100%',
|
||||
padding: '0.5rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
}}
|
||||
onClick={handleDisplayClick}
|
||||
>
|
||||
{placeholder && !value && (
|
||||
<span className='pointer-events-none text-gray-400'>
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
{customRenderer
|
||||
? customRenderer
|
||||
: renderHighlightedText(value, cursorPosition)}
|
||||
</div>
|
||||
</div>
|
||||
{showSuggestions && (
|
||||
<AutocompleteDropdown
|
||||
currentValue={lastWord}
|
||||
suggestions={suggestions[suggestionTrigger]}
|
||||
selectedIndex={selectedSuggestionIndex}
|
||||
onMouseEnterSuggestion={index => {
|
||||
setSelectedSuggestionIndex(index)
|
||||
}}
|
||||
onSelectSuggestion={suggestion => {
|
||||
const suggestionValue = suggestions[suggestionTrigger].display
|
||||
? suggestion[suggestions[suggestionTrigger].display]
|
||||
: suggestion
|
||||
const newValue = `${value.slice(0, cursorPosition)}${suggestionValue}${value.slice(cursorPosition)}`
|
||||
|
||||
onChange(newValue)
|
||||
titleInputRef?.current?.focus()
|
||||
|
||||
setCursorPosition(cursorPosition + suggestion.length)
|
||||
titleInputRef.current.value = newValue
|
||||
titleInputRef.current.setSelectionRange(
|
||||
cursorPosition + suggestionValue.length,
|
||||
cursorPosition + suggestionValue.length,
|
||||
)
|
||||
setShowSuggestions(false)
|
||||
}}
|
||||
parentRefer={dropdownRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmartTaskTitleInput
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DndContext, closestCenter } from '@dnd-kit/core'
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
@@ -24,7 +30,7 @@ import {
|
||||
ListItem,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import React, { useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||
|
||||
function SortableItem({
|
||||
@@ -39,7 +45,17 @@ function SortableItem({
|
||||
editMode,
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: task.id })
|
||||
useSortable({
|
||||
id: task.id,
|
||||
// Add touch sensor options for better mobile scrolling
|
||||
options: {
|
||||
activationConstraint: {
|
||||
// Require a small movement before activating drag to allow scrolling
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedText, setEditedText] = useState(task.name)
|
||||
@@ -58,7 +74,8 @@ function SortableItem({
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
touchAction: 'none',
|
||||
// Enable default touch behavior for scrolling
|
||||
touchAction: 'auto',
|
||||
paddingLeft: `${level * 24}px`,
|
||||
}
|
||||
|
||||
@@ -102,7 +119,15 @@ function SortableItem({
|
||||
<>
|
||||
<ListItem ref={setNodeRef} style={style} {...attributes}>
|
||||
{editMode && (
|
||||
<IconButton {...listeners} {...attributes} size='sm'>
|
||||
<IconButton
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
size='sm'
|
||||
// Add data attribute for selective activation
|
||||
data-drag-handle='true'
|
||||
// Only restrict touch actions on the drag handle
|
||||
sx={{ touchAction: 'none' }}
|
||||
>
|
||||
<DragIndicator />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -119,7 +144,7 @@ function SortableItem({
|
||||
)}
|
||||
|
||||
{!hasChildren && level > 0 && (
|
||||
<Box sx={{ width: 28 }} /> // Spacer for alignment not sure of better way for now it's good
|
||||
<Box sx={{ width: 28 }} /> // Spacer for alignment
|
||||
)}
|
||||
|
||||
<Box
|
||||
@@ -269,6 +294,17 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
|
||||
|
||||
const topLevelTasks = tasks.filter(task => task.parentId === null)
|
||||
|
||||
// Create sensors for touch handling
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
// Configure for better mobile scrolling
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 8,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const handleToggle = taskId => {
|
||||
const updatedTask = tasks.find(task => task.id === taskId)
|
||||
const newCompletedAt = updatedTask.completedAt
|
||||
@@ -405,9 +441,21 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<SortableContext items={tasks} strategy={verticalListSortingStrategy}>
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List
|
||||
sx={{
|
||||
padding: 0,
|
||||
// Improve scrolling behavior on mobile
|
||||
maxHeight: 'inherit',
|
||||
overflow: 'visible',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{topLevelTasks
|
||||
.sort((a, b) => a.orderId - b.orderId)
|
||||
.map((task, index) => (
|
||||
|
||||
Reference in New Issue
Block a user