{
+ setIsRedeemModalOpen(false)
+ },
+ isOpen: isRedeemModalOpen,
+ available: circleUsers.find(user => user.userId === selectedUser)
+ ?.points,
+ user: circleUsers.find(user => user.userId === selectedUser),
+ onSave: ({ userId, points }) => {
+ RedeemPoints(userId, points, userProfile.circleID)
+ .then(res => {
+ setIsRedeemModalOpen(false)
+ handleCircleMembersRefetch()
+ })
+ .catch(err => {
+ console.log(err)
+ })
+ },
+ }}
+ />
)
}
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx
new file mode 100644
index 0000000..5859ed7
--- /dev/null
+++ b/src/views/components/AddTaskModal.jsx
@@ -0,0 +1,532 @@
+import { KeyboardReturnOutlined, OpenInFull } from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Chip,
+ IconButton,
+ Input,
+ Modal,
+ ModalDialog,
+ Option,
+ Select,
+ Textarea,
+ Typography,
+} from '@mui/joy'
+import { FormControl } from '@mui/material'
+import * as chrono from 'chrono-node'
+import moment from 'moment'
+import { useContext, useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { CSSTransition } from 'react-transition-group'
+import { UserContext } from '../../contexts/UserContext'
+import { CreateChore } from '../../utils/Fetcher'
+const VALID_DAYS = {
+ monday: 'Monday',
+ mon: 'Monday',
+ tuesday: 'Tuesday',
+ tue: 'Tuesday',
+ wednesday: 'Wednesday',
+ wed: 'Wednesday',
+ thursday: 'Thursday',
+ thu: 'Thursday',
+ friday: 'Friday',
+ fri: 'Friday',
+ saturday: 'Saturday',
+ sat: 'Saturday',
+ sunday: 'Sunday',
+ sun: 'Sunday',
+}
+
+const VALID_MONTHS = {
+ january: 'January',
+ jan: 'January',
+ february: 'February',
+ feb: 'February',
+ march: 'March',
+ mar: 'March',
+ april: 'April',
+ apr: 'April',
+ may: 'May',
+ june: 'June',
+ jun: 'June',
+ july: 'July',
+ jul: 'July',
+ august: 'August',
+ aug: 'August',
+ september: 'September',
+ sep: 'September',
+ october: 'October',
+ oct: 'October',
+ november: 'November',
+ nov: 'November',
+ december: 'December',
+ dec: 'December',
+}
+
+const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
+ (v, i, a) => a.indexOf(v) === i,
+)
+
+const TaskInput = ({ autoFocus, onChoreUpdate }) => {
+ const { userProfile } = useContext(UserContext)
+ const navigate = useNavigate()
+ const [taskText, setTaskText] = useState('')
+ const [taskTitle, setTaskTitle] = useState('')
+ const [openModal, setOpenModal] = useState(false)
+ const textareaRef = useRef(null)
+ const mainInputRef = useRef(null)
+ const [priority, setPriority] = useState('0')
+ const [dueDate, setDueDate] = useState(null)
+ const [description, setDescription] = useState(null)
+ const [frequency, setFrequency] = useState(null)
+ const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
+
+ useEffect(() => {
+ if (openModal && textareaRef.current) {
+ textareaRef.current.focus()
+ textareaRef.current.selectionStart = textareaRef.current.value?.length
+ textareaRef.current.selectionEnd = textareaRef.current.value?.length
+ }
+ }, [openModal])
+
+ useEffect(() => {
+ if (autoFocus > 0 && mainInputRef.current) {
+ mainInputRef.current.focus()
+ mainInputRef.current.selectionStart = mainInputRef.current.value?.length
+ mainInputRef.current.selectionEnd = mainInputRef.current.value?.length
+ }
+ }, [autoFocus])
+
+ const handleEnterPressed = e => {
+ if (e.key === 'Enter') {
+ createChore()
+ handleCloseModal()
+ setTaskText('')
+ }
+ }
+
+ const handleCloseModal = () => {
+ setOpenModal(false)
+ setTaskText('')
+ }
+
+ const handleSubmit = () => {
+ createChore()
+ handleCloseModal()
+ setTaskText('')
+ }
+
+ const parsePriority = sentence => {
+ sentence = sentence.toLowerCase()
+ const priorityMap = {
+ 1: ['p1', 'priority 1', 'high priority', 'urgent', 'asap', 'important'],
+ 2: ['p2', 'priority 2', 'medium priority'],
+ 3: ['p3', 'priority 3', 'low priority'],
+ 4: ['p4', 'priority 4'],
+ }
+
+ for (const [priority, terms] of Object.entries(priorityMap)) {
+ if (terms.some(term => sentence.includes(term))) {
+ return {
+ result: priority,
+ cleanedSentence: terms.reduce((s, t) => s.replace(t, ''), sentence),
+ }
+ }
+ }
+ return { result: 0, cleanedSentence: sentence }
+ }
+
+ const parseRepeatV2 = sentence => {
+ const result = {
+ frequency: 1,
+ frequencyType: null,
+ frequencyMetadata: {
+ days: [],
+ months: [],
+ unit: null,
+ time: new Date().toISOString(),
+ },
+ }
+
+ const patterns = [
+ {
+ frequencyType: 'day_of_the_month:every',
+ regex: /(\d+)(?:th|st|nd|rd)? of every month$/i,
+ name: 'Every {day} of every month',
+ },
+ {
+ frequencyType: 'daily',
+ regex: /(every day|daily)$/i,
+ name: 'Every day',
+ },
+ {
+ frequencyType: 'weekly',
+ regex: /(every week|weekly)$/i,
+ name: 'Every week',
+ },
+ {
+ frequencyType: 'monthly',
+ regex: /(every month|monthly)$/i,
+ name: 'Every month',
+ },
+ {
+ frequencyType: 'yearly',
+ regex: /every year$/i,
+ name: 'Every year',
+ },
+ {
+ frequencyType: 'monthly',
+ regex: /every (?:other )?month$/i,
+ name: 'Bi Monthly',
+ value: 2,
+ },
+ {
+ frequencyType: 'interval:2week',
+ regex: /(bi-?weekly|every other week)/i,
+ value: 2,
+ name: 'Bi Weekly',
+ },
+ {
+ frequencyType: 'interval',
+ regex: /every (\d+) (days?|weeks?|months?|years?).*$/i,
+ name: 'Every {frequency} {unit}',
+ },
+ {
+ frequencyType: 'interval:every_other',
+ regex: /every other (days?|weeks?|months?|years?)$/i,
+ name: 'Every other {unit}',
+ },
+ {
+ frequencyType: 'days_of_the_week',
+ regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i,
+ name: 'Every {days} of the week',
+ },
+ {
+ frequencyType: 'day_of_the_month',
+ regex: /(\d+)(?:st|nd|rd|th)? of ([\w ]+(?:(?:,| and |\s)[\w ]+)*)/i,
+ name: 'Every {day} days of {months}',
+ },
+ ]
+
+ for (const pattern of patterns) {
+ const match = sentence.match(pattern.regex)
+ if (!match) continue
+
+ result.frequencyType = pattern.frequencyType
+ const unitMap = {
+ daily: 'days',
+ weekly: 'weeks',
+ monthly: 'months',
+ yearly: 'years',
+ }
+
+ switch (pattern.frequencyType) {
+ case 'daily':
+ case 'weekly':
+ case 'monthly':
+ case 'yearly':
+ result.frequencyType = 'interval'
+ result.frequency = pattern.value || 1
+ result.frequencyMetadata.unit = unitMap[pattern.frequencyType]
+ return {
+ result,
+ name: pattern.name,
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+
+ case 'interval':
+ result.frequency = parseInt(match[1], 10)
+ result.frequencyMetadata.unit = match[2]
+ return {
+ result,
+ name: pattern.name
+ .replace('{frequency}', result.frequency)
+ .replace('{unit}', result.frequencyMetadata.unit),
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+
+ case 'days_of_the_week':
+ result.frequencyMetadata.days = match[1]
+ .toLowerCase()
+ .split(/ and |,|\s/)
+ .map(day => day.trim())
+ .filter(day => VALID_DAYS[day])
+ .map(day => VALID_DAYS[day])
+ if (!result.frequencyMetadata.days.length)
+ return { result: null, name: null, cleanedSentence: sentence }
+ return {
+ result,
+ name: pattern.name.replace(
+ '{days}',
+ result.frequencyMetadata.days.join(', '),
+ ),
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+
+ case 'day_of_the_month':
+ result.frequency = parseInt(match[1], 10)
+ result.frequencyMetadata.months = match[2]
+ .toLowerCase()
+ .split(/ and |,|\s/)
+ .map(month => month.trim())
+ .filter(month => VALID_MONTHS[month])
+ .map(month => VALID_MONTHS[month])
+ result.frequencyMetadata.unit = 'days'
+ return {
+ result,
+ name: pattern.name
+ .replace('{day}', result.frequency)
+ .replace('{months}', result.frequencyMetadata.months.join(', ')),
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+
+ case 'interval:every_other':
+ case 'interval:2week':
+ result.frequency = 2
+ result.frequencyMetadata.unit = 'weeks'
+ result.frequencyType = 'interval'
+ return {
+ result,
+ name: pattern.name,
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+
+ case 'day_of_the_month:every':
+ result.frequency = parseInt(match[1], 10)
+ result.frequencyMetadata.months = ALL_MONTHS
+ result.frequencyMetadata.unit = 'days'
+ return {
+ result,
+ name: pattern.name
+ .replace('{day}', result.frequency)
+ .replace('{months}', result.frequencyMetadata.months.join(', ')),
+ cleanedSentence: sentence.replace(match[0], '').trim(),
+ }
+ }
+ }
+ return { result: null, name: null, cleanedSentence: sentence }
+ }
+
+ const handleTextChange = e => {
+ if (!e.target.value) {
+ setTaskText('')
+ setOpenModal(false)
+ setDueDate(null)
+ setFrequency(null)
+ setFrequencyHumanReadable(null)
+ setPriority(0)
+ return
+ }
+
+ let cleanedSentence = e.target.value
+ const priority = parsePriority(cleanedSentence)
+ if (priority.result) setPriority(priority.result)
+ cleanedSentence = priority.cleanedSentence
+
+ const parsedDueDate = chrono.parse(cleanedSentence, new Date(), {
+ forwardDate: true,
+ })
+ if (parsedDueDate[0]?.index > -1) {
+ setDueDate(
+ moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'),
+ )
+ cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '')
+ }
+
+ const repeat = parseRepeatV2(cleanedSentence)
+ if (repeat.result) {
+ setFrequency(repeat.result)
+ setFrequencyHumanReadable(repeat.name)
+ cleanedSentence = repeat.cleanedSentence
+ }
+
+ if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
+ setOpenModal(true)
+ }
+
+ setTaskText(e.target.value)
+ setTaskTitle(cleanedSentence.trim())
+ }
+
+ const createChore = () => {
+ const chore = {
+ name: taskTitle,
+ assignees: [{ userId: userProfile.id }],
+ dueDate: dueDate ? new Date(dueDate).toISOString() : null,
+ assignedTo: userProfile.id,
+ assignStrategy: 'random',
+ isRolling: false,
+ description: description || null,
+ labelsV2: [],
+ priority: priority || 0,
+ status: 0,
+ }
+
+ if (frequency) {
+ chore.frequencyType = frequency.frequencyType
+ chore.frequencyMetadata = frequency.frequencyMetadata
+ chore.frequency = frequency.frequency
+ }
+
+ CreateChore(chore).then(resp => {
+ resp.json().then(data => {
+ onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
+ })
+ })
+ }
+
+ return (
+ <>
+ {!openModal && (
+
+ 0}
+ ref={mainInputRef}
+ placeholder='Add a task...'
+ value={taskText}
+ onChange={handleTextChange}
+ sx={{
+ fontSize: '16px',
+ mt: 1,
+ mb: 1,
+ borderRadius: 24,
+ height: 24,
+ borderColor: 'text.disabled',
+ padding: 1,
+ width: '100%',
+ }}
+ onKeyUp={handleEnterPressed}
+ endDecorator={
+
+
+
+ }
+ />
+
+ )}
+
+
+
+
+
+ Create new task
+
+ Experimental
+
+
+
+
+ Task in a sentence:
+
+
+
+ Title:
+ setTaskTitle(e.target.value)}
+ placeholder='Type your full text here...'
+ sx={{ width: '100%', fontSize: '16px' }}
+ />
+
+
+ Description:
+
+
+
+
+ Priority
+
+
+
+ Due Date
+ setDueDate(e.target.value)}
+ sx={{ width: '100%', fontSize: '16px' }}
+ />
+
+
+
+
+ Assignee
+
+
+
+ Frequency
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default TaskInput
diff --git a/src/views/components/Calendar.css b/src/views/components/Calendar.css
new file mode 100644
index 0000000..bdd24c2
--- /dev/null
+++ b/src/views/components/Calendar.css
@@ -0,0 +1,70 @@
+.react-calendar {
+ width: 100%;
+ max-width: 600px;
+ font-family: 'Roboto', sans-serif;
+ background-color: transparent;
+ border: none;
+}
+
+.react-calendar__tile {
+ padding: 10px;
+ min-height: 45px;
+ text-align: center;
+ border-radius: 8px;
+ transition: background-color 0.3s ease;
+}
+
+.react-calendar__tile:enabled:hover {
+ background-color: #cbcbcb;
+}
+
+.react-calendar__tile--active {
+ background-color: #007bff !important;
+}
+
+.react-calendar__tile--now {
+ border: 1px dotted #4ec1a2e3 !important;
+ border-radius: 8px;
+ background-color: inherit;
+}
+
+.react-calendar__tile--now:enabled:hover {
+ background-color: rgba(0, 123, 255, 0.1);
+}
+
+.dot-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 16px;
+ flex-wrap: wrap;
+ position: relative;
+}
+
+.dot {
+ width: 0.3em;
+ height: 0.3em;
+ border-radius: 50%;
+ margin: 0 1px;
+}
+
+.chore-tooltip {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: #333;
+ color: white;
+ padding: 5px 10px;
+ border-radius: 4px;
+ z-index: 10;
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ opacity 0.2s ease,
+ visibility 0.2s ease;
+}
+
+.react-calendar__month-view__days__day--weekend {
+ color: inherit;
+}
diff --git a/src/views/components/CalendarView.jsx b/src/views/components/CalendarView.jsx
new file mode 100644
index 0000000..43e44b8
--- /dev/null
+++ b/src/views/components/CalendarView.jsx
@@ -0,0 +1,176 @@
+import { Box, Card, CardContent, Chip, Grid, Typography } from '@mui/joy'
+import moment from 'moment'
+import React, { useState } from 'react'
+import Calendar from 'react-calendar'
+import 'react-calendar/dist/Calendar.css'
+import { useNavigate } from 'react-router-dom'
+import { UserContext } from '../../contexts/UserContext'
+import { getTextColorFromBackgroundColor, TASK_COLOR } from '../../utils/Colors'
+import './Calendar.css'
+
+const getAssigneeColor = (assignee, userProfile) => {
+ return assignee === userProfile.id
+ ? TASK_COLOR.ASSIGNED_TO_ME
+ : TASK_COLOR.ASSIGNED_TO_OTHER
+}
+
+const CalendarView = ({ chores }) => {
+ const { userProfile } = React.useContext(UserContext)
+ const [selectedDate, setSeletedDate] = useState(null)
+ const Navigate = useNavigate()
+
+ const tileContent = ({ date, view }) => {
+ if (view === 'month') {
+ const dayChores = chores.filter(
+ chore =>
+ new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
+ date.toISOString().split('T')[0],
+ )
+
+ return (
+
+ {dayChores.map((chore, index) => {
+ if (index > 6) {
+ return null
+ }
+
+ return (
+
+ )
+ })}
+
+ )
+ }
+ return null
+ }
+
+ return (
+
+ {
+ setSeletedDate(new Date(d))
+ }}
+ />
+ {!selectedDate && (
+
+ {[
+ { name: 'Assigned to me', color: TASK_COLOR.ASSIGNED_TO_ME },
+ { name: 'Assigned to other', color: TASK_COLOR.ASSIGNED_TO_OTHER },
+ ].map((item, index) => (
+
+
+
+ {item.name}
+
+
+ ))}
+
+ )}
+ {selectedDate && (
+
+ {chores
+ .filter(
+ chore =>
+ new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
+ selectedDate.toISOString().split('T')[0],
+ )
+ .map((chore, idx) => (
+ {
+ Navigate('/chores/' + chore.id)
+ }}
+ sx={{
+ mb: 0.4,
+ py: 1,
+ px: 1,
+
+ // backgroundColor: getAssigneeColor(
+ // chore.assignedTo,
+ // userProfile,
+ // ),
+ // everything show in one row:
+ }}
+ >
+
+
+
+ {moment(chore.nextDueDate).format('hh:mm A')}
+
+ {chore.name}
+
+
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default CalendarView
diff --git a/src/views/components/IconButtonTouchable.jsx b/src/views/components/IconButtonTouchable.jsx
new file mode 100644
index 0000000..c8cc4e5
--- /dev/null
+++ b/src/views/components/IconButtonTouchable.jsx
@@ -0,0 +1,34 @@
+import IconButton from '@mui/joy/IconButton'
+import React, { useRef, useState } from 'react'
+
+const IconButtonTouchable = ({ onHold, onClick, ...props }) => {
+ const [holdTimeout, setHoldTimeout] = useState(null)
+ const holdRef = useRef(false)
+
+ const handleMouseDown = () => {
+ holdRef.current = false
+ setHoldTimeout(
+ setTimeout(() => {
+ holdRef.current = true
+ onHold && onHold()
+ }, 1000),
+ )
+ }
+
+ const handleMouseUp = () => {
+ clearTimeout(holdTimeout)
+ if (!holdRef.current) {
+ onClick && onClick()
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default IconButtonTouchable