First commit with sources
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@armco/react-vite-rtk-template",
|
||||
"name": "@armco/calendar",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -65,7 +65,7 @@
|
||||
"main": "index.tsx",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/.git"
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/calendar.git"
|
||||
},
|
||||
"keywords": [
|
||||
"components",
|
||||
@@ -75,7 +75,7 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues"
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/calendar/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme"
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/calendar#readme"
|
||||
}
|
||||
|
||||
54
src/Calendar.component.scss
Executable file
54
src/Calendar.component.scss
Executable file
@@ -0,0 +1,54 @@
|
||||
@use "./MonthSelector.component.scss";
|
||||
@use "./JustCalendar.component.scss";
|
||||
@use "./MonthNavigator.component.scss";
|
||||
@use "./EventManager.component.scss";
|
||||
|
||||
|
||||
.ar-Calendar {
|
||||
--ar-footer-height: 3rem;
|
||||
&.is-mini {
|
||||
--ar-footer-height: 2rem;
|
||||
width: calc(2.5rem * 7 + 1rem) !important;
|
||||
height: calc(3rem + (2.5rem * 6) + 3rem + 1px) !important;
|
||||
}
|
||||
|
||||
&__cal-event-decade-month-year-selector {
|
||||
|
||||
& > * {
|
||||
transition: left 0.3s;
|
||||
}
|
||||
|
||||
.ar-MonthSelector, .ar-EventForm {
|
||||
left: 100%;
|
||||
}
|
||||
.ar-JustCalendar {
|
||||
left: 0;
|
||||
}
|
||||
&.to-month-year, &.to-event-form {
|
||||
.ar-JustCalendar {
|
||||
left: -100%
|
||||
}
|
||||
}
|
||||
&.to-month-year {
|
||||
.ar-MonthSelector {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
&.to-event-form {
|
||||
.ar-EventForm {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ar-Calendar__cal-event-decade-month-year-selector {
|
||||
height: calc(100% - var(--ar-footer-height));
|
||||
}
|
||||
.ar-Calendar__footer {
|
||||
transition: height 0.3s;
|
||||
height: var(--ar-footer-height);
|
||||
|
||||
.ar-Calendar__status {
|
||||
color: var(--ar-color-obscure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import Home from "./Home"
|
||||
import Calendar from "./Calendar"
|
||||
|
||||
describe("Home", () => {
|
||||
describe("Calendar", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
298
src/Calendar.tsx
Executable file
298
src/Calendar.tsx
Executable file
@@ -0,0 +1,298 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import MonthSelector from "./MonthSelector"
|
||||
import JustCalendar from "./JustCalendar"
|
||||
import EventManager from "./EventManager"
|
||||
import MonthNavigator from "./MonthNavigator"
|
||||
import {
|
||||
ArButtonVariants,
|
||||
ArCalViews,
|
||||
ArMonthSelectorViews,
|
||||
ArSizes,
|
||||
CalendarDate,
|
||||
CalendarProps,
|
||||
Event,
|
||||
} from "@armco/types"
|
||||
import { Button, CalHelper, isWithinSchedule } from "../.."
|
||||
import "./Calendar.component.scss"
|
||||
|
||||
const calHelper = new CalHelper({ siblingMonths: true, weekStart: 1 })
|
||||
|
||||
const today = new Date()
|
||||
|
||||
const Calendar = (props: CalendarProps): JSX.Element => {
|
||||
const {
|
||||
allowEventSetting,
|
||||
classes,
|
||||
events,
|
||||
customDayEventSetter,
|
||||
hasTimeControls,
|
||||
isSingleSelect,
|
||||
maxDate,
|
||||
minDate,
|
||||
miniMode,
|
||||
onDateSelected,
|
||||
startDate,
|
||||
endDate,
|
||||
theme,
|
||||
} = props
|
||||
const [calendar, setCalendar] = useState<Array<CalendarDate | false>>()
|
||||
const [refDate, setRefDate] = useState<Date>(today)
|
||||
const [startDateLocal, setStartDate] = useState<CalendarDate | null>()
|
||||
const [endDateLocal, setEndDate] = useState<CalendarDate | null>()
|
||||
const [eventDate, setEventDate] = useState<CalendarDate | null>()
|
||||
const [hovered, setHovered] = useState<CalendarDate | null>()
|
||||
const [currentView, setCurrentView] = useState<ArCalViews>(
|
||||
ArCalViews.CALENDAR,
|
||||
)
|
||||
const [localEvents, setLocalEvents] = useState<Array<Event>>()
|
||||
const [currentMonthNavView, setCurrentMonthNavView] =
|
||||
useState<ArMonthSelectorViews | null>()
|
||||
|
||||
const currentDate = (startDateLocal || {
|
||||
day: today.getDate(),
|
||||
month: today.getMonth(),
|
||||
year: today.getFullYear(),
|
||||
}) as CalendarDate
|
||||
|
||||
const [currentDecade, setCurrentDecade] = useState<number | undefined>(
|
||||
currentDate?.year && Math.floor(currentDate.year / 10) * 10,
|
||||
)
|
||||
const [currentYear, setCurrentYear] = useState<number | undefined>(
|
||||
currentDate?.year,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalEvents(events)
|
||||
}, [events])
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate) {
|
||||
setStartDate({
|
||||
year: (startDate as Date).getFullYear(),
|
||||
month: (startDate as Date).getMonth(),
|
||||
day: (startDate as Date).getDate(),
|
||||
})
|
||||
}
|
||||
}, [startDate])
|
||||
|
||||
useEffect(() => {
|
||||
if (endDate) {
|
||||
setEndDate({
|
||||
year: (endDate as Date).getFullYear(),
|
||||
month: (endDate as Date).getMonth(),
|
||||
day: (endDate as Date).getDate(),
|
||||
})
|
||||
}
|
||||
}, [endDate])
|
||||
|
||||
useEffect(() => {
|
||||
if (refDate) {
|
||||
const year = (refDate as Date).getFullYear()
|
||||
const month = (refDate as Date).getMonth()
|
||||
const calendar = calHelper.getCalendar(year, month)
|
||||
setCalendar(calendar)
|
||||
}
|
||||
}, [refDate])
|
||||
|
||||
const onDateSelectedLocal = (date: CalendarDate, isSpecialOp?: boolean) => {
|
||||
if (!startDateLocal || isSingleSelect) {
|
||||
setStartDate(date)
|
||||
calHelper.setStartDate(date)
|
||||
onDateSelected &&
|
||||
onDateSelected(isSingleSelect ? date : { startDate: date })
|
||||
if (isSpecialOp && allowEventSetting) {
|
||||
setEventDate(date)
|
||||
setCurrentView(ArCalViews.EVENT_FORM)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (startDateLocal && endDateLocal) {
|
||||
if (
|
||||
CalHelper.compare(startDateLocal, date) === -1 &&
|
||||
CalHelper.compare(date, endDateLocal) === -1
|
||||
) {
|
||||
calHelper.setEndDate(date)
|
||||
setEndDate(date)
|
||||
onDateSelected &&
|
||||
onDateSelected(
|
||||
isSingleSelect
|
||||
? date
|
||||
: { startDate: startDateLocal, endDate: date },
|
||||
)
|
||||
} else {
|
||||
calHelper.setEndDate(null)
|
||||
setEndDate(null)
|
||||
calHelper.setStartDate(date)
|
||||
setStartDate(date)
|
||||
onDateSelected &&
|
||||
onDateSelected(
|
||||
isSingleSelect ? date : { startDate: date, endDate: null },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (CalHelper.compare(date, startDateLocal) === -1) {
|
||||
setEndDate(startDateLocal)
|
||||
calHelper.setEndDate(startDateLocal)
|
||||
setStartDate(date)
|
||||
calHelper.setStartDate(date)
|
||||
onDateSelected &&
|
||||
onDateSelected(
|
||||
isSingleSelect
|
||||
? date
|
||||
: { startDate: date, endDate: startDateLocal },
|
||||
)
|
||||
} else {
|
||||
setEndDate(date)
|
||||
calHelper.setEndDate(date)
|
||||
onDateSelected &&
|
||||
onDateSelected(
|
||||
isSingleSelect
|
||||
? date
|
||||
: { startDate: startDateLocal, endDate: date },
|
||||
)
|
||||
}
|
||||
setHovered(null)
|
||||
}
|
||||
if (isSpecialOp && allowEventSetting) {
|
||||
setEventDate(date)
|
||||
setCurrentView(ArCalViews.EVENT_FORM)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-Calendar w-100 h-100 d-flex flex-column${
|
||||
hasTimeControls ? " has-time-controls" : ""
|
||||
}${classes ? " " + classes : ""}${miniMode ? " is-mini" : ""}`}
|
||||
>
|
||||
{refDate && currentView !== ArCalViews.EVENT_FORM && (
|
||||
<MonthNavigator
|
||||
currentDecade={currentDecade}
|
||||
currentMonthNavView={currentMonthNavView}
|
||||
currentView={currentView}
|
||||
currentYear={currentYear}
|
||||
miniMode={miniMode}
|
||||
refDate={refDate}
|
||||
setCurrentDecade={setCurrentDecade}
|
||||
setCurrentMonthNavView={setCurrentMonthNavView}
|
||||
setCurrentView={setCurrentView}
|
||||
setCurrentYear={setCurrentYear}
|
||||
setRefDate={setRefDate}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`ar-Calendar__cal-event-decade-month-year-selector d-flex flex-1 position-relative overflow-hidden border-bottom${
|
||||
currentView === ArCalViews.MONTH_YEAR_SELECTOR ? " to-month-year" : ""
|
||||
}${currentView === ArCalViews.EVENT_FORM ? " to-event-form" : ""}${
|
||||
miniMode ? " is-mini" : ""
|
||||
}`}
|
||||
>
|
||||
<JustCalendar
|
||||
allowEventSetting={allowEventSetting}
|
||||
calHelper={calHelper}
|
||||
calendar={calendar}
|
||||
endDate={endDateLocal as CalendarDate}
|
||||
hovered={hovered as CalendarDate}
|
||||
isSingleSelect={isSingleSelect}
|
||||
miniMode={miniMode}
|
||||
onDateSelected={onDateSelectedLocal}
|
||||
setHovered={setHovered}
|
||||
startDate={startDateLocal as CalendarDate}
|
||||
/>
|
||||
<MonthSelector
|
||||
currentDecade={currentDecade}
|
||||
currentMonthNavView={currentMonthNavView}
|
||||
currentView={currentView}
|
||||
currentYear={currentYear}
|
||||
miniMode={miniMode}
|
||||
onMonthSelect={(date: Date) => {
|
||||
setCurrentView(ArCalViews.CALENDAR)
|
||||
setRefDate(date)
|
||||
}}
|
||||
setCurrentDecade={setCurrentDecade}
|
||||
setCurrentMonthNavView={setCurrentMonthNavView}
|
||||
setCurrentView={setCurrentView}
|
||||
setCurrentYear={setCurrentYear}
|
||||
/>
|
||||
{eventDate && currentView === ArCalViews.EVENT_FORM && (
|
||||
<EventManager
|
||||
events={localEvents?.filter((event) =>
|
||||
isWithinSchedule(eventDate, event),
|
||||
)}
|
||||
miniMode={miniMode}
|
||||
onEventSubmit={(event: Event) => {
|
||||
const localEventsClone = localEvents ? [...localEvents] : []
|
||||
localEventsClone.push(event)
|
||||
setLocalEvents(localEventsClone)
|
||||
setEventDate(null)
|
||||
setCurrentView(ArCalViews.CALENDAR)
|
||||
}}
|
||||
selectedDate={eventDate as CalendarDate}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`ar-Calendar__footer flex-v-center${
|
||||
miniMode ? " p-2" : " px-3 py-2"
|
||||
}${
|
||||
currentView === ArCalViews.EVENT_FORM ? " h-0 overflow-hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{startDateLocal && !miniMode && (
|
||||
<div className="ar-Calendar__status justify-self-start">
|
||||
<span className="fw-bold">
|
||||
{CalHelper.toString(startDateLocal)}
|
||||
{endDateLocal && " - " + CalHelper.toString(endDateLocal)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="ar-Calendar__controls ms-auto d-flex">
|
||||
<Button
|
||||
content="Clear"
|
||||
classes={miniMode ? "me-2" : "me-3"}
|
||||
size={miniMode ? ArSizes.XSMALL : ArSizes.SMALL}
|
||||
variant={ArButtonVariants.SECONDARY}
|
||||
onClick={() => {
|
||||
setStartDate(null)
|
||||
setEndDate(null)
|
||||
calHelper.setStartDate(null)
|
||||
calHelper.setEndDate(null)
|
||||
}}
|
||||
disabled={!startDateLocal}
|
||||
/>
|
||||
<Button
|
||||
content="Today"
|
||||
size={miniMode ? ArSizes.XSMALL : ArSizes.SMALL}
|
||||
onClick={() => {
|
||||
const date = today
|
||||
const calDate = {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear(),
|
||||
}
|
||||
setStartDate(calDate)
|
||||
setEndDate(null)
|
||||
calHelper.setStartDate(calDate)
|
||||
calHelper.setEndDate(null)
|
||||
setRefDate(date)
|
||||
setCurrentDecade(Math.floor(calDate.year / 10) * 10)
|
||||
setCurrentYear(calDate.year)
|
||||
setCurrentMonthNavView(null)
|
||||
setCurrentView(ArCalViews.CALENDAR)
|
||||
onDateSelected &&
|
||||
onDateSelected(
|
||||
isSingleSelect
|
||||
? calDate
|
||||
: { startDate: calDate, endDate: null },
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Calendar
|
||||
17
src/DatePicker.component.scss
Executable file
17
src/DatePicker.component.scss
Executable file
@@ -0,0 +1,17 @@
|
||||
.ar-DatePicker {
|
||||
border-top-left-radius: 0.125rem;
|
||||
border-bottom-left-radius: 0.125rem;
|
||||
.ar-DatePicker__picker {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ar-DatePicker__clear-button {
|
||||
visibility: hidden;
|
||||
padding: 0 0.75rem;
|
||||
|
||||
&.has-value {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
80
src/DatePicker.tsx
Executable file
80
src/DatePicker.tsx
Executable file
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ArPopoverPositions,
|
||||
ArPopoverSlots,
|
||||
CalendarDate,
|
||||
DatePickerProps,
|
||||
FunctionType,
|
||||
} from "@armco/types"
|
||||
import { Helper } from "@armco/utils"
|
||||
import { Icon } from "@armco/icon"
|
||||
import { Popover } from "@armco/shared-components"
|
||||
import Calendar from "."
|
||||
import "./DatePicker.component.scss"
|
||||
|
||||
const DatePicker = (props: DatePickerProps): JSX.Element => {
|
||||
const { allowEventSetting, classes, id, label, onChange } = props
|
||||
const [date, setDate] = useState<CalendarDate>()
|
||||
const [pickerDisplayed, showPicker] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
!allowEventSetting && date && showPicker(false)
|
||||
}, [allowEventSetting, date])
|
||||
|
||||
const localId = id || "ar-DatePicker__input"
|
||||
|
||||
const onLocalChange = (date: CalendarDate) => {
|
||||
setDate(date)
|
||||
onChange && (onChange as FunctionType)(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`ar-DatePicker${classes ? " " + classes : ""}`}>
|
||||
<Popover
|
||||
contentClasses="high-shadow"
|
||||
position={ArPopoverPositions.BOTTOMRIGHT}
|
||||
version="v1"
|
||||
isOpen={pickerDisplayed}
|
||||
>
|
||||
<div
|
||||
className="position-relative cursor-pointer"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={() => !allowEventSetting && showPicker(true)}
|
||||
>
|
||||
{label && (
|
||||
<label className="fw-bold me-3" htmlFor={localId}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={localId}
|
||||
className="ar-DatePicker__input px-3 py-2"
|
||||
type="text"
|
||||
placeholder="dd/mm/yyyy"
|
||||
value={
|
||||
date &&
|
||||
Helper.pad(date.day) +
|
||||
"/" +
|
||||
Helper.pad(date.month + 1) +
|
||||
"/" +
|
||||
date.year
|
||||
}
|
||||
/>
|
||||
<Icon
|
||||
attributes={{ classes: "position-absolute top-point5 end-point5" }}
|
||||
icon="fa.FaRegCalendarAlt"
|
||||
/>
|
||||
</div>
|
||||
<Calendar
|
||||
allowEventSetting={allowEventSetting}
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
onDateSelected={onLocalChange}
|
||||
miniMode
|
||||
isSingleSelect
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatePicker
|
||||
5
src/DateRangePicker.component.scss
Executable file
5
src/DateRangePicker.component.scss
Executable file
@@ -0,0 +1,5 @@
|
||||
.ar-DateRangePicker {
|
||||
.ar-DateRangePicker__input {
|
||||
min-width: 15rem;
|
||||
}
|
||||
}
|
||||
101
src/DateRangePicker.tsx
Executable file
101
src/DateRangePicker.tsx
Executable file
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ArPopoverPositions,
|
||||
ArPopoverSlots,
|
||||
CalendarDate,
|
||||
DateRangePickerProps,
|
||||
} from "@armco/types"
|
||||
import { Icon } from "@armco/icon"
|
||||
import { DateFormatter, Helper, toDate } from "@armco/utils"
|
||||
import { Popover } from "@armco/shared-components"
|
||||
import Calendar from "."
|
||||
import "./DateRangePicker.component.scss"
|
||||
|
||||
const DateRangePicker = (props: DateRangePickerProps): JSX.Element => {
|
||||
const { closeOnEndSelect, id, label, mask } = props
|
||||
const [startDate, setStartDate] = useState<CalendarDate>()
|
||||
const [endDate, setEndDate] = useState<CalendarDate>()
|
||||
const [pickerDisplayed, showPicker] = useState<boolean>()
|
||||
let value
|
||||
const localId = id || "ar-DateRangePicker__input"
|
||||
|
||||
useEffect(() => {
|
||||
if (endDate !== null && closeOnEndSelect) {
|
||||
showPicker(false)
|
||||
}
|
||||
}, [closeOnEndSelect, endDate])
|
||||
|
||||
if (startDate && endDate) {
|
||||
if (mask) {
|
||||
value =
|
||||
DateFormatter(toDate(startDate), mask) +
|
||||
" - " +
|
||||
DateFormatter(toDate(endDate), mask)
|
||||
} else {
|
||||
value =
|
||||
Helper.pad(startDate.day) +
|
||||
"/" +
|
||||
Helper.pad(startDate.month + 1) +
|
||||
"/" +
|
||||
startDate.year
|
||||
value += " - "
|
||||
value +=
|
||||
Helper.pad(endDate.day) +
|
||||
"/" +
|
||||
Helper.pad(endDate.month + 1) +
|
||||
"/" +
|
||||
endDate.year
|
||||
}
|
||||
}
|
||||
|
||||
const setDate = (dates: {
|
||||
startDate: CalendarDate
|
||||
endDate?: CalendarDate
|
||||
}) => {
|
||||
setStartDate(dates.startDate)
|
||||
if (dates.endDate) {
|
||||
setEndDate(dates.endDate)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ar-DateRangePicker">
|
||||
<Popover
|
||||
contentClasses="high-shadow"
|
||||
position={ArPopoverPositions.BOTTOMRIGHT}
|
||||
version="v1"
|
||||
isOpen={pickerDisplayed}
|
||||
>
|
||||
<div
|
||||
className="position-relative cursor-pointer"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={() => showPicker(true)}
|
||||
>
|
||||
{label && (
|
||||
<label className="fw-bold me-3" htmlFor={localId}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={localId}
|
||||
className="ar-DateRangePicker__input px-3 py-2"
|
||||
type="text"
|
||||
placeholder="dd/mm/yyyy - dd/mm/yyyy"
|
||||
value={value}
|
||||
/>
|
||||
<Icon
|
||||
icon="fa.FaRegCalendarAlt"
|
||||
attributes={{ classes: "position-absolute top-point5 end-point5" }}
|
||||
/>
|
||||
</div>
|
||||
<Calendar
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
onDateSelected={setDate}
|
||||
miniMode
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateRangePicker
|
||||
285
src/EventForm.tsx
Normal file
285
src/EventForm.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ArButtonVariants,
|
||||
ArEventStates,
|
||||
ArEventTypes,
|
||||
ArSchedules,
|
||||
ArSizes,
|
||||
Event,
|
||||
EventFormProps,
|
||||
FunctionType,
|
||||
ObjectType,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
Button,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
TextArea,
|
||||
TextInput,
|
||||
TimeEntry,
|
||||
Toggle,
|
||||
toDate,
|
||||
} from "../.."
|
||||
import { EventListItem } from "./EventList"
|
||||
|
||||
const durationOptions = [
|
||||
{
|
||||
name: "15min",
|
||||
label: "15 minutes",
|
||||
value: "15min",
|
||||
data: { minutes: 15 },
|
||||
},
|
||||
{
|
||||
name: "30min",
|
||||
label: "30 minutes",
|
||||
value: "30min",
|
||||
data: { minutes: 30 },
|
||||
},
|
||||
{
|
||||
name: "45min",
|
||||
label: "45 minutes",
|
||||
value: "45min",
|
||||
data: { minutes: 45 },
|
||||
},
|
||||
{
|
||||
name: "1hour",
|
||||
label: "1 hour",
|
||||
value: "1hour",
|
||||
data: { hours: 1 },
|
||||
},
|
||||
{
|
||||
name: "2hours",
|
||||
label: "2 hours",
|
||||
value: "2hours",
|
||||
data: { hours: 2 },
|
||||
},
|
||||
{ name: "custom", label: "Custom", value: "custom" },
|
||||
]
|
||||
|
||||
export const segments = [
|
||||
{ name: "task", label: "Task", icon: "md.MdAddTask" },
|
||||
{
|
||||
name: "event",
|
||||
label: "Event",
|
||||
icon: "md.MdOutlineEventAvailable",
|
||||
},
|
||||
{
|
||||
name: "birthday",
|
||||
label: "Birthday",
|
||||
icon: "fa.FaBirthdayCake",
|
||||
},
|
||||
{
|
||||
name: "meeting",
|
||||
label: "Meeting",
|
||||
icon: "md.MdOutlineMeetingRoom",
|
||||
},
|
||||
]
|
||||
|
||||
const validateEvent = (event: Event) => {
|
||||
return (
|
||||
event &&
|
||||
event.eventType &&
|
||||
event.title &&
|
||||
event.schedule &&
|
||||
event.schedule.starts &&
|
||||
event.schedule.ends
|
||||
)
|
||||
}
|
||||
|
||||
const EventForm = (props: EventFormProps) => {
|
||||
const { miniMode, onSubmit, selectedDate } = props
|
||||
const [localEvent, setLocalEvent] = useState<Event>({
|
||||
title: "",
|
||||
state: ArEventStates.DRAFT,
|
||||
schedule: {},
|
||||
})
|
||||
|
||||
const onChange = (
|
||||
key: string,
|
||||
value: string | boolean | ArSchedules | ObjectType,
|
||||
) => {
|
||||
const eventClone = { ...localEvent }
|
||||
if (key === "isWholeDay" || key === "starts" || key === "ends") {
|
||||
if (!eventClone.schedule) {
|
||||
eventClone.schedule = {}
|
||||
}
|
||||
} else if (key !== "duration") {
|
||||
;(eventClone[key as keyof Event] as string | boolean | ArSchedules) =
|
||||
value as string
|
||||
}
|
||||
if (key === "isWholeDay" && eventClone.schedule) {
|
||||
eventClone.schedule.isWholeDay = value as boolean
|
||||
if (value) {
|
||||
eventClone.schedule.starts = toDate(selectedDate)
|
||||
eventClone.schedule.ends = toDate({
|
||||
...selectedDate,
|
||||
day: selectedDate.day + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
if ((key === "starts" || key === "ends") && eventClone.schedule) {
|
||||
const timeDetails = (value as ObjectType).raw as ObjectType
|
||||
let date
|
||||
try {
|
||||
date =
|
||||
timeDetails &&
|
||||
timeDetails.hour &&
|
||||
toDate(
|
||||
selectedDate,
|
||||
+(timeDetails.hour || 0),
|
||||
+(timeDetails.minute || 0),
|
||||
timeDetails.ampm as string,
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn("Failed attempt to convert string to number")
|
||||
}
|
||||
date && (eventClone.schedule[key] = date)
|
||||
}
|
||||
if (key === "eventType" && value === "birthday") {
|
||||
if (!eventClone.schedule) {
|
||||
eventClone.schedule = {}
|
||||
}
|
||||
eventClone.schedule.isWholeDay = true
|
||||
eventClone.schedule.starts = toDate(selectedDate)
|
||||
eventClone.schedule.ends = toDate({
|
||||
...selectedDate,
|
||||
day: selectedDate.day + 1,
|
||||
})
|
||||
}
|
||||
if (key === "duration" && eventClone.schedule) {
|
||||
const durationData = ((value as ObjectType).data as ObjectType)?.data
|
||||
const currentHours = (eventClone.schedule.starts as Date)?.getHours()
|
||||
const currentMinutes = (eventClone.schedule.starts as Date)?.getMinutes()
|
||||
const currentAmPm = (eventClone.schedule.starts as Date)
|
||||
?.toLocaleString("en-US", { hour12: true })
|
||||
.split(" ")[1]
|
||||
const addHours = (durationData as ObjectType)?.hours
|
||||
const addMinutes = (durationData as ObjectType)?.minutes
|
||||
let endHours = currentHours,
|
||||
endMinutes = currentMinutes,
|
||||
ampm = currentAmPm,
|
||||
endsNextDay = false
|
||||
if (
|
||||
addMinutes &&
|
||||
currentMinutes &&
|
||||
currentMinutes + (addMinutes as number) > 59
|
||||
) {
|
||||
endMinutes = currentMinutes + (addMinutes as number) - 60
|
||||
endHours += 1
|
||||
}
|
||||
if (addHours && endHours + (addHours as number) >= 12) {
|
||||
endHours = endHours + (addHours as number) - 12
|
||||
if (ampm === "PM") {
|
||||
endsNextDay = true
|
||||
}
|
||||
ampm = ampm === "AM" ? "PM" : "AM"
|
||||
}
|
||||
eventClone.schedule.ends = toDate(
|
||||
endsNextDay
|
||||
? { ...selectedDate, day: selectedDate.day + 1 }
|
||||
: selectedDate,
|
||||
endHours,
|
||||
endMinutes,
|
||||
)
|
||||
}
|
||||
setLocalEvent(eventClone)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-EventForm p-2 mb-3 overflow-auto ${
|
||||
miniMode ? "w-100" : "border-right"
|
||||
}`}
|
||||
>
|
||||
<SegmentedControl
|
||||
classes="mb-3"
|
||||
segments={segments}
|
||||
onChange={(segment) => {
|
||||
onChange("eventType", segment.name)
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
classes="mb-3"
|
||||
label="Title"
|
||||
size={ArSizes.SMALL}
|
||||
onChange={(e) =>
|
||||
onChange("title", (e.target as HTMLInputElement).value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
<TextArea
|
||||
classes="mb-3"
|
||||
label="Details"
|
||||
size={ArSizes.SMALL}
|
||||
onChange={(e) =>
|
||||
onChange("description", (e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
/>
|
||||
<Toggle
|
||||
classes="mb-3"
|
||||
label="Whole Day?"
|
||||
onChange={(checked: boolean) => onChange("isWholeDay", checked)}
|
||||
size={ArSizes.SMALL}
|
||||
isOn={localEvent?.schedule?.isWholeDay as boolean}
|
||||
hideStatus
|
||||
/>
|
||||
<TimeEntry
|
||||
classes="mb-3"
|
||||
label="Starts"
|
||||
size={ArSizes.SMALL}
|
||||
isDisabled={localEvent?.schedule?.isWholeDay as boolean}
|
||||
onChange={
|
||||
((value: ObjectType) => onChange("starts", value)) as FunctionType
|
||||
}
|
||||
required
|
||||
/>
|
||||
{localEvent?.eventType &&
|
||||
localEvent?.eventType !== ArEventTypes.MEETING && (
|
||||
<TimeEntry
|
||||
classes="mb-4"
|
||||
label="Ends"
|
||||
size={ArSizes.SMALL}
|
||||
isDisabled={localEvent?.schedule?.isWholeDay as boolean}
|
||||
onChange={
|
||||
((value: ObjectType) => onChange("ends", value)) as FunctionType
|
||||
}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{localEvent?.eventType === ArEventTypes.MEETING && (
|
||||
<Select
|
||||
classes="mb-3"
|
||||
options={durationOptions}
|
||||
onSelectionChanged={(obj) => onChange("duration", obj)}
|
||||
size={ArSizes.SMALL}
|
||||
isDisabled={!localEvent.schedule || !localEvent.schedule.starts}
|
||||
/>
|
||||
)}
|
||||
<EventListItem
|
||||
classes="mb-4"
|
||||
event={localEvent}
|
||||
selectedEventType={segments.find(
|
||||
(segment) => segment.name === localEvent.eventType,
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
size={ArSizes.SMALL}
|
||||
content="OK"
|
||||
variant={ArButtonVariants.SUCCESS}
|
||||
onClick={() => {
|
||||
if (localEvent && validateEvent(localEvent)) {
|
||||
localEvent.state = ArEventStates.SCHEDULED
|
||||
onSubmit && onSubmit(localEvent)
|
||||
setLocalEvent({
|
||||
title: "",
|
||||
state: ArEventStates.DRAFT,
|
||||
schedule: {},
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventForm
|
||||
107
src/EventList.tsx
Normal file
107
src/EventList.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { ReactNode } from "react"
|
||||
import { EventListProps, Event, SegmentData } from "@armco/types"
|
||||
import { LoadableIcon } from "../.."
|
||||
import { segments } from "./EventForm"
|
||||
|
||||
const noEventsMessage = (
|
||||
<div className="ar-EventManager__no-events fw-bold flex-center h-100 flex-column">
|
||||
<span>No events on this date yet.</span>
|
||||
<span>Use the form on the left to select type and add an event.</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const EventListItem = ({
|
||||
classes,
|
||||
event,
|
||||
selectedEventType,
|
||||
}: {
|
||||
classes?: string
|
||||
event: Event
|
||||
selectedEventType?: SegmentData
|
||||
}) => {
|
||||
const titleJSX = event && <span>{event.title}</span>
|
||||
const startAtJSX = event?.schedule?.starts && (
|
||||
<span>
|
||||
. Starts:{" "}
|
||||
{event.schedule.starts?.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
const endsAtJSX = event?.schedule?.ends && (
|
||||
<span>
|
||||
, Ends:{" "}
|
||||
{event.schedule.ends?.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
|
||||
let preview: ReactNode = event?.title ? (
|
||||
<>
|
||||
{titleJSX}
|
||||
{event.schedule?.starts && (
|
||||
<>
|
||||
{startAtJSX}
|
||||
{event.schedule?.ends && endsAtJSX}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
|
||||
if (event?.eventType) {
|
||||
preview = selectedEventType?.icon ? (
|
||||
<span>
|
||||
<LoadableIcon classes="me-2" icon={selectedEventType.icon} />
|
||||
{preview}
|
||||
</span>
|
||||
) : (
|
||||
preview
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={`ar-EventListItem small d-inline-block fw-bold${
|
||||
classes ? " " + classes : ""
|
||||
}`}
|
||||
>
|
||||
{preview}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const EventList = (props: EventListProps) => {
|
||||
const { events } = props
|
||||
return (
|
||||
<div className="ar-EventList flex-grow-1 p-3">
|
||||
{events && events.length > 0 ? (
|
||||
<ul className="list-unstyled">
|
||||
{events.map((event) => (
|
||||
<li>
|
||||
<EventListItem
|
||||
event={event}
|
||||
selectedEventType={segments.find(
|
||||
(segment) => segment.name === event.eventType,
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
noEventsMessage
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventList
|
||||
26
src/EventManager.component.scss
Normal file
26
src/EventManager.component.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.ar-EventManager {
|
||||
.ar-EventManager__body {
|
||||
height: calc(100% - 2rem - 1px);
|
||||
&:not(.is-mini) .ar-EventForm {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
.ar-EventList {
|
||||
line-height: 1.3rem;
|
||||
|
||||
.ar-EventListItem {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.super-small {
|
||||
width: 3rem !important;
|
||||
}
|
||||
|
||||
.ar-EventManager__no-events {
|
||||
font-style: italic;
|
||||
color: var(--ar-color-disabled);
|
||||
}
|
||||
}
|
||||
61
src/EventManager.tsx
Normal file
61
src/EventManager.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ArButtonVariants,
|
||||
ArCalViews,
|
||||
ArSizes,
|
||||
EventManagerProps,
|
||||
Event,
|
||||
} from "@armco/types"
|
||||
import EventForm from "./EventForm"
|
||||
import EventList from "./EventList"
|
||||
import { Button, Helper } from "../.."
|
||||
import "./EventManager.component.scss"
|
||||
|
||||
const EventManager = (props: EventManagerProps) => {
|
||||
const { events, miniMode, selectedDate, setCurrentView, onEventSubmit } =
|
||||
props
|
||||
const [localEvents, setLocalEvents] = useState<Array<Event>>()
|
||||
|
||||
useEffect(() => setLocalEvents(events), [events])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-EventManager position-absolute h-100 w-100${
|
||||
miniMode ? " is-mini" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="ar-EventManager__header border-bottom py-2 px-3 flex-v-center">
|
||||
<Button
|
||||
classes="me-3 super-small"
|
||||
preIcon="io5/IoArrowBackCircleOutline"
|
||||
content=""
|
||||
variant={ArButtonVariants.SECONDARY}
|
||||
size={ArSizes.XSMALL}
|
||||
onClick={() => setCurrentView(ArCalViews.CALENDAR)}
|
||||
/>
|
||||
<span className="fw-bold">
|
||||
{Helper.pad(selectedDate.day) +
|
||||
" - " +
|
||||
Helper.pad(selectedDate.month + 1) +
|
||||
" - " +
|
||||
selectedDate.year}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ar-EventManager__body d-flex">
|
||||
<EventForm
|
||||
selectedDate={selectedDate}
|
||||
miniMode={miniMode}
|
||||
onSubmit={(newEvent: Event) => {
|
||||
const localEventsClone = localEvents ? [...localEvents] : []
|
||||
localEventsClone.push(newEvent)
|
||||
setLocalEvents(localEventsClone)
|
||||
onEventSubmit && onEventSubmit(newEvent)
|
||||
}}
|
||||
/>
|
||||
{!miniMode && <EventList events={localEvents} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventManager
|
||||
63
src/JustCalendar.component.scss
Normal file
63
src/JustCalendar.component.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
.ar-JustCalendar {
|
||||
.ar-JustCalendar__day {
|
||||
width: calc(100% / 7);
|
||||
.ar-JustCalendar__day__date {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.ar-JustCalendar__day__event-btn {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.3s;
|
||||
left: calc(50% + 1rem);
|
||||
&.show {
|
||||
max-width: 2rem;
|
||||
}
|
||||
}
|
||||
&:not(.ar-JustCalendar__weekday):hover {
|
||||
background-color: var(--ar-bg-hover);
|
||||
.ar-JustCalendar__day__date {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
&.ar-JustCalendar__sibling-month {
|
||||
color: var(--ar-color-disabled);
|
||||
&.selected {
|
||||
.ar-JustCalendar__day__date {
|
||||
color: var(--ar-color-disabled-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
.ar-JustCalendar__day__date {
|
||||
background-color: var(--ar-bg-selected-4-dark);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&.selectable {
|
||||
background-color: var(--ar-bg-hover);
|
||||
}
|
||||
&__badge {
|
||||
top: 0.1rem;
|
||||
right: 0;
|
||||
}
|
||||
// &.today {
|
||||
// background-color: var(--ar-bg-selected-3-faded);
|
||||
// }
|
||||
}
|
||||
&.is-mini {
|
||||
width: 18rem;
|
||||
.ar-JustCalendar__day {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
.ar-JustCalendar__day__event-btn {
|
||||
left: calc(50% + 0.3rem);
|
||||
top: calc(50% - 0.8rem);
|
||||
&.show {
|
||||
max-width: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/JustCalendar.tsx
Normal file
174
src/JustCalendar.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ArBadgeType,
|
||||
ArSizes,
|
||||
CalendarDate,
|
||||
DateItemProps,
|
||||
JustCalendarProps,
|
||||
} from "@armco/types"
|
||||
import { Badge, LoadableIcon } from "../.."
|
||||
|
||||
const weekDays = [
|
||||
{
|
||||
name: "Monday",
|
||||
short: "Mo",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
name: "Tuesday",
|
||||
short: "Tu",
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
name: "Wednesday",
|
||||
short: "We",
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
name: "Thursday",
|
||||
short: "Th",
|
||||
index: 3,
|
||||
},
|
||||
{
|
||||
name: "Friday",
|
||||
short: "Fr",
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
name: "Saturday",
|
||||
short: "Sa",
|
||||
index: 5,
|
||||
},
|
||||
{
|
||||
name: "Sunday",
|
||||
short: "Su",
|
||||
index: 6,
|
||||
},
|
||||
]
|
||||
const areSame = (date: Date | CalendarDate, calDate: CalendarDate) => {
|
||||
if ("day" in date) {
|
||||
date = date as CalendarDate
|
||||
return (
|
||||
calDate.day === date.day &&
|
||||
calDate.month === date.month &&
|
||||
calDate.year === date.year
|
||||
)
|
||||
} else {
|
||||
date = date as Date
|
||||
return (
|
||||
calDate.day === date.getDate() &&
|
||||
calDate.month === date.getMonth() &&
|
||||
calDate.year === date.getFullYear()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const DateItem = (props: DateItemProps) => {
|
||||
const {
|
||||
allowEventSetting,
|
||||
calHelper,
|
||||
date,
|
||||
hovered: conditionalHovered,
|
||||
isSingleSelect,
|
||||
isToday,
|
||||
miniMode,
|
||||
onDateSelected,
|
||||
setHovered: setConditionalHovered,
|
||||
} = props
|
||||
const [hovered, setHovered] = useState<boolean>()
|
||||
return (
|
||||
<li
|
||||
className={`ar-JustCalendar__day flex-center cursor-pointer position-relative${
|
||||
date.siblingMonth ? " ar-JustCalendar__sibling-month" : ""
|
||||
}${calHelper?.isDateSelected(date) ? " selected" : ""}${
|
||||
isToday ? " today" : ""
|
||||
}${
|
||||
calHelper?.isDateSelected(date, conditionalHovered) ? " selectable" : ""
|
||||
}`}
|
||||
onClick={() => onDateSelected(date)}
|
||||
onMouseOver={() => {
|
||||
setHovered(true)
|
||||
if (!!calHelper?.startDate && !calHelper.endDate && !isSingleSelect) {
|
||||
setConditionalHovered(date)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<span className="ar-JustCalendar__day__date flex-center position-relative">
|
||||
{isToday && (
|
||||
<Badge
|
||||
type={ArBadgeType.COMPLETE}
|
||||
size={ArSizes.SMALL}
|
||||
classes="ar-JustCalendar__day__badge position-absolute"
|
||||
/>
|
||||
)}
|
||||
{date.day}
|
||||
</span>
|
||||
{allowEventSetting && (
|
||||
<span
|
||||
className={`ar-JustCalendar__day__event-btn position-absolute${
|
||||
hovered ? " show" : ""
|
||||
}`}
|
||||
onClick={() => onDateSelected(date, allowEventSetting && hovered)}
|
||||
>
|
||||
<LoadableIcon
|
||||
icon="io.IoIosAddCircle"
|
||||
color="#5cb85cb3"
|
||||
hoverColor="#5cb85c"
|
||||
size={miniMode ? "0.8rem" : "1.5rem"}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const JustCalendar = (props: JustCalendarProps) => {
|
||||
const {
|
||||
allowEventSetting,
|
||||
calendar,
|
||||
calHelper,
|
||||
endDate,
|
||||
hovered,
|
||||
isSingleSelect,
|
||||
miniMode,
|
||||
setHovered,
|
||||
onDateSelected,
|
||||
startDate,
|
||||
} = props
|
||||
return (
|
||||
<ul
|
||||
className={`ar-JustCalendar list-unstyled d-flex flex-wrap flex-1 position-relative mb-0 ${
|
||||
miniMode ? "is-mini p-1" : "p-3"
|
||||
}`}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
>
|
||||
{weekDays.map((weekday) => (
|
||||
<li className="ar-JustCalendar__day ar-Calendar__weekday fw-bold flex-center">
|
||||
<span className="ar-JustCalendar__day__short">{weekday.short}</span>
|
||||
</li>
|
||||
))}
|
||||
{calendar?.map((date) => {
|
||||
const isToday = date && areSame(new Date(), date)
|
||||
const liElement = date ? (
|
||||
<DateItem
|
||||
allowEventSetting={allowEventSetting}
|
||||
calHelper={calHelper}
|
||||
date={date}
|
||||
hovered={hovered}
|
||||
isSingleSelect={isSingleSelect}
|
||||
isToday={isToday}
|
||||
miniMode={miniMode}
|
||||
onDateSelected={onDateSelected}
|
||||
setHovered={setHovered}
|
||||
/>
|
||||
) : (
|
||||
<li />
|
||||
)
|
||||
return liElement
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default JustCalendar
|
||||
72
src/MonthNavigator.component.scss
Normal file
72
src/MonthNavigator.component.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.ar-MonthNavigator {
|
||||
&__month {
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
&__nav-button {
|
||||
transition: width 0.3s;
|
||||
width: calc(100% / 5);
|
||||
padding: 0.5rem 0;
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover);
|
||||
}
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
background-color: var(--ar-color-disabled-3);
|
||||
color: var(--ar-color-disabled-2);
|
||||
}
|
||||
}
|
||||
&.is-mini {
|
||||
.ar-MonthNavigator {
|
||||
&__month {
|
||||
width: 48%;
|
||||
}
|
||||
&__nav-button:not(.ar-MonthNavigator__month) {
|
||||
width: 13%;
|
||||
}
|
||||
}
|
||||
&.to-month-year {
|
||||
.ar-MonthNavigator {
|
||||
&__month {
|
||||
width: 70%;
|
||||
}
|
||||
&__nav-button:first-child, &__nav-button:last-child {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
// &.to-month-selector {
|
||||
// .ar-MonthNavigator {
|
||||
// &__nav-button {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
&.to-month-year {
|
||||
.ar-MonthNavigator {
|
||||
&__nav-button {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
&__nav-button:first-child, &__nav-button:last-child {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
// &.to-month-selector {
|
||||
// .ar-MonthNavigator {
|
||||
// &__nav-button {
|
||||
// width: 100%;
|
||||
// }
|
||||
// &__nav-button:nth-child(2), &__nav-button:nth-child(4) {
|
||||
// width: 0;
|
||||
// overflow: hidden;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
163
src/MonthNavigator.tsx
Normal file
163
src/MonthNavigator.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
MonthNavigatorProps,
|
||||
ArCalViews,
|
||||
ArMonthSelectorViews,
|
||||
ArThemes,
|
||||
} from "@armco/types"
|
||||
import { LoadableIcon, CalHelper, MONTH_INDEX } from "../.."
|
||||
|
||||
const getSelectedDecade = (refDate: Date, selectedDecade?: number) => {
|
||||
const useYear = selectedDecade || refDate.getFullYear()
|
||||
const start = Math.floor(useYear / 10) * 10
|
||||
const end = start + 10
|
||||
|
||||
return start + 1 + " - " + end
|
||||
}
|
||||
|
||||
const getSelectedCentury = (refDate: Date, selectedDecade?: number) => {
|
||||
const useDecade = selectedDecade || refDate.getFullYear()
|
||||
const start = Math.floor(useDecade / 100) * 100
|
||||
const end = start + 100
|
||||
|
||||
return start + 1 + " - " + end
|
||||
}
|
||||
|
||||
const MonthNavigator = (props: MonthNavigatorProps) => {
|
||||
const {
|
||||
currentDecade,
|
||||
currentMonthNavView,
|
||||
currentView,
|
||||
currentYear,
|
||||
miniMode,
|
||||
refDate,
|
||||
setCurrentDecade,
|
||||
setCurrentMonthNavView,
|
||||
setCurrentView,
|
||||
setCurrentYear,
|
||||
setRefDate,
|
||||
theme,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-MonthNavigator border-bottom d-flex${
|
||||
miniMode ? " is-mini px-0 py-1" : " px-3 py-2"
|
||||
}${
|
||||
currentView === ArCalViews.MONTH_YEAR_SELECTOR ? " to-month-year" : ""
|
||||
}${
|
||||
ArMonthSelectorViews.MONTH === currentMonthNavView
|
||||
? " to-month-selector"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="d-flex w-100">
|
||||
<div
|
||||
className="ar-MonthNavigator__nav-button flex-center cursor-pointer"
|
||||
onClick={() => setRefDate(CalHelper.getPreviousYear(refDate))}
|
||||
>
|
||||
<LoadableIcon
|
||||
icon="ai/AiOutlineDoubleLeft"
|
||||
color={theme === ArThemes.DARK1 ? "white" : "black"}
|
||||
size="1rem"
|
||||
strokeWidth={miniMode ? "30" : "100"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ar-MonthNavigator__nav-button flex-center cursor-pointer"
|
||||
onClick={() => {
|
||||
if (currentView === ArCalViews.CALENDAR) {
|
||||
setRefDate(CalHelper.getPreviousMonth(refDate))
|
||||
} else {
|
||||
currentMonthNavView === ArMonthSelectorViews.MONTH
|
||||
? setCurrentYear((currentYear || refDate.getFullYear()) - 1)
|
||||
: setCurrentDecade(
|
||||
(currentDecade || 2000) -
|
||||
(currentMonthNavView === ArMonthSelectorViews.DECADE
|
||||
? 100
|
||||
: 10),
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LoadableIcon
|
||||
icon="ai/AiOutlineLeft"
|
||||
color={theme === ArThemes.DARK1 ? "white" : "black"}
|
||||
size="1rem"
|
||||
strokeWidth={miniMode ? "30" : "100"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`ar-MonthNavigator__nav-button ar-MonthNavigator__month d-inline-flex flex-center cursor-pointer overflow-auto${
|
||||
currentMonthNavView === ArMonthSelectorViews.DECADE
|
||||
? " disabled"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (currentView === ArCalViews.CALENDAR) {
|
||||
setCurrentView(ArCalViews.MONTH_YEAR_SELECTOR)
|
||||
}
|
||||
if (currentMonthNavView === ArMonthSelectorViews.MONTH) {
|
||||
setCurrentMonthNavView(ArMonthSelectorViews.YEAR)
|
||||
} else if (currentMonthNavView === ArMonthSelectorViews.YEAR) {
|
||||
setCurrentMonthNavView(ArMonthSelectorViews.DECADE)
|
||||
} else {
|
||||
setCurrentMonthNavView(ArMonthSelectorViews.MONTH)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentView === ArCalViews.MONTH_YEAR_SELECTOR ? (
|
||||
currentMonthNavView === ArMonthSelectorViews.MONTH ? (
|
||||
currentYear || refDate.getFullYear()
|
||||
) : currentMonthNavView === ArMonthSelectorViews.YEAR ? (
|
||||
getSelectedDecade(refDate, currentDecade)
|
||||
) : (
|
||||
getSelectedCentury(refDate, currentDecade)
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span className="me-1">{MONTH_INDEX[refDate.getMonth()]}</span>
|
||||
<span>{refDate.getFullYear()}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="ar-MonthNavigator__nav-button flex-center cursor-pointer"
|
||||
onClick={() => {
|
||||
if (currentView === ArCalViews.CALENDAR) {
|
||||
setRefDate(CalHelper.getNextMonth(refDate))
|
||||
} else {
|
||||
currentMonthNavView === ArMonthSelectorViews.MONTH
|
||||
? setCurrentYear((currentYear || refDate.getFullYear()) + 1)
|
||||
: setCurrentDecade(
|
||||
(currentDecade || 2000) +
|
||||
(currentMonthNavView === ArMonthSelectorViews.DECADE
|
||||
? 100
|
||||
: 10),
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LoadableIcon
|
||||
icon="ai.AiOutlineRight"
|
||||
color={theme === ArThemes.DARK1 ? "white" : "black"}
|
||||
size="1rem"
|
||||
strokeWidth={miniMode ? "30" : "100"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ar-MonthNavigator__nav-button flex-center cursor-pointer"
|
||||
onClick={() => setRefDate(CalHelper.getNextYear(refDate))}
|
||||
>
|
||||
<LoadableIcon
|
||||
icon="ai.AiOutlineDoubleRight"
|
||||
color={theme === ArThemes.DARK1 ? "white" : "black"}
|
||||
size="1rem"
|
||||
strokeWidth={miniMode ? "30" : "100"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MonthNavigator
|
||||
18
src/MonthSelector.component.scss
Executable file
18
src/MonthSelector.component.scss
Executable file
@@ -0,0 +1,18 @@
|
||||
.ar-MonthSelector {
|
||||
& > * {
|
||||
transition: left 0.3s;
|
||||
}
|
||||
&__decade-selector, &__year-selector, &__month-selector {
|
||||
&__decade-padder, &__year-padder, &__month-padder {
|
||||
width: calc(100% / 4);
|
||||
}
|
||||
&__decade:hover, &__year:hover, &__month:hover {
|
||||
background-color: var(--ar-bg-hover);
|
||||
}
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
&__decade-selector__decade-padder, &__year-selector__year-padder, &__month-selector__month-padder {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/MonthSelector.tsx
Normal file
143
src/MonthSelector.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
ArCalViews,
|
||||
ArMonthSelectorViews,
|
||||
MonthSelectorProps,
|
||||
} from "@armco/types"
|
||||
import { MONTH_INDEX, sub } from "../.."
|
||||
import "./MonthSelector.component.scss"
|
||||
|
||||
const generateDecades = (currentDecade: number) => {
|
||||
let startDecade = +(("" + currentDecade).substring(0, 2) + "00")
|
||||
const tillDecade = startDecade + 100
|
||||
const decades = []
|
||||
while (startDecade < tillDecade) {
|
||||
decades.push({ start: startDecade + 1, end: startDecade + 10 })
|
||||
startDecade += 10
|
||||
}
|
||||
return decades
|
||||
}
|
||||
|
||||
const generateYears = (currentYear: number) => {
|
||||
let startYear = currentYear + 1
|
||||
const tillYear = startYear + 10
|
||||
const years = []
|
||||
while (startYear < tillYear) {
|
||||
years.push(startYear)
|
||||
startYear++
|
||||
}
|
||||
return years
|
||||
}
|
||||
|
||||
const MonthSelector = (props: MonthSelectorProps) => {
|
||||
const {
|
||||
currentDecade,
|
||||
currentMonthNavView,
|
||||
currentYear,
|
||||
miniMode,
|
||||
onMonthSelect,
|
||||
setCurrentDecade,
|
||||
setCurrentMonthNavView,
|
||||
setCurrentView,
|
||||
setCurrentYear,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="ar-MonthSelector position-absolute h-100 d-flex w-100 overflow-hidden">
|
||||
<div
|
||||
className={`ar-MonthSelector__decade-selector position-absolute h-100 w-100 d-flex flex-wrap${
|
||||
currentMonthNavView === ArMonthSelectorViews.DECADE
|
||||
? " start-0"
|
||||
: " start-100"
|
||||
}`}
|
||||
>
|
||||
{currentDecade &&
|
||||
generateDecades(currentDecade).map((decade) => {
|
||||
return (
|
||||
<div
|
||||
className={`ar-MonthSelector__decade-selector__decade-padder d-flex ${
|
||||
miniMode ? "p-0" : "p-3"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentDecade(decade.start - 1)
|
||||
setCurrentMonthNavView(ArMonthSelectorViews.YEAR)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`ar-MonthSelector__decade-selector__decade flex-center flex-1 cursor-pointer${
|
||||
!miniMode ? " border border-radius" : ""
|
||||
}`}
|
||||
>
|
||||
{decade.start + " - " + sub(decade.end)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`ar-MonthSelector__year-selector position-absolute w-100 h-100 d-flex flex-wrap${
|
||||
currentMonthNavView === ArMonthSelectorViews.YEAR
|
||||
? " start-0"
|
||||
: currentMonthNavView === ArMonthSelectorViews.MONTH
|
||||
? " start-100"
|
||||
: " start-n100"
|
||||
}`}
|
||||
>
|
||||
{currentDecade &&
|
||||
generateYears(currentDecade).map((year) => {
|
||||
return (
|
||||
<div
|
||||
className={`ar-MonthSelector__year-selector__year-padder d-flex ${
|
||||
miniMode ? "p-0" : "p-3"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentYear(year)
|
||||
setCurrentMonthNavView(ArMonthSelectorViews.MONTH)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`ar-MonthSelector__year-selector__year flex-center flex-1 cursor-pointer${
|
||||
!miniMode ? " border border-radius" : ""
|
||||
}`}
|
||||
>
|
||||
{year}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`ar-MonthSelector__month-selector position-absolute w-100 h-100 d-flex flex-wrap${
|
||||
currentMonthNavView === ArMonthSelectorViews.MONTH
|
||||
? " start-0"
|
||||
: " start-n100"
|
||||
}`}
|
||||
>
|
||||
{MONTH_INDEX.map((month, index) => (
|
||||
<div
|
||||
className={`ar-MonthSelector__month-selector__month-padder d-flex ${
|
||||
miniMode ? "p-0" : "p-3"
|
||||
}`}
|
||||
onClick={() => {
|
||||
const date = new Date()
|
||||
date.setMonth(index)
|
||||
date.setFullYear(+(currentYear || date.getFullYear()))
|
||||
onMonthSelect(date)
|
||||
setCurrentMonthNavView(null)
|
||||
setCurrentView(ArCalViews.CALENDAR)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`ar-MonthSelector__month-selector__month flex-center flex-1 cursor-pointer${
|
||||
!miniMode ? " border border-radius" : ""
|
||||
}`}
|
||||
>
|
||||
{month}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MonthSelector
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useRoutes } from "react-router-dom"
|
||||
import * as pages from "./pages"
|
||||
import Helper from "./utils/helper"
|
||||
import ROUTES from "./routes"
|
||||
|
||||
Helper.populateComponentsInRoutes(ROUTES, pages)
|
||||
|
||||
interface RouterProps {}
|
||||
|
||||
const Router = (props: RouterProps): JSX.Element | null => useRoutes(ROUTES)
|
||||
|
||||
export default Router
|
||||
@@ -1,6 +0,0 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import type { RootState, AppDispatch } from "./store"
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-Home {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface HomeState {}
|
||||
|
||||
const initialState: HomeState = {}
|
||||
|
||||
export const homeSlice = createSlice({
|
||||
name: "home",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = homeSlice.actions
|
||||
|
||||
export default homeSlice.reducer
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
import "./Home.module.scss"
|
||||
|
||||
interface HomeProps {}
|
||||
|
||||
const Home = props => {
|
||||
return (
|
||||
<div className="c-Home">
|
||||
In Page Home
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -1,3 +0,0 @@
|
||||
import Home from "./Home.jsx"
|
||||
|
||||
export default Home
|
||||
@@ -1,7 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Home from "./Home"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Home,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const ROUTES = [
|
||||
{
|
||||
path: "/",
|
||||
class: "landing",
|
||||
element: "Home",
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
@@ -1,18 +0,0 @@
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
|
||||
import counterReducer from "../features/counter/counterSlice"
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
counter: counterReducer,
|
||||
},
|
||||
})
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>
|
||||
6
src/app/types/route.d.ts
vendored
6
src/app/types/route.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
interface RouteConfig {
|
||||
path: String
|
||||
class?: String
|
||||
element: String | JSX.Element | null
|
||||
children?: Array<RouteConfig>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
class Helper {
|
||||
static populateComponentsInRoutes(routes: RouteConfig[], components: any) {
|
||||
routes &&
|
||||
routes.forEach((route) => {
|
||||
const Component: JSX.ElementType =
|
||||
components[route.element as keyof object]
|
||||
route.element = <Component />
|
||||
if (route.children) {
|
||||
Helper.populateComponentsInRoutes(route.children, components)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Helper
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "../../app/hooks"
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
incrementIfOdd,
|
||||
selectCount,
|
||||
} from "./counterSlice"
|
||||
|
||||
export function Counter() {
|
||||
const count = useAppSelector(selectCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const [incrementAmount, setIncrementAmount] = useState("2")
|
||||
|
||||
const incrementValue = Number(incrementAmount) || 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="value">{count}</span>
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<input
|
||||
className="textbox"
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className="asyncButton"
|
||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||
>
|
||||
Add If Odd
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// A mock function to mimic making an async request for data
|
||||
export function fetchCount(amount = 1) {
|
||||
return new Promise<{ data: number }>((resolve) =>
|
||||
setTimeout(() => resolve({ data: amount }), 500),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import counterReducer, {
|
||||
CounterState,
|
||||
increment,
|
||||
decrement,
|
||||
incrementByAmount,
|
||||
} from "./counterSlice"
|
||||
|
||||
describe("counter reducer", () => {
|
||||
const initialState: CounterState = {
|
||||
value: 3,
|
||||
status: "idle",
|
||||
}
|
||||
it("should handle initial state", () => {
|
||||
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
|
||||
value: 0,
|
||||
status: "idle",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle increment", () => {
|
||||
const actual = counterReducer(initialState, increment())
|
||||
expect(actual.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should handle decrement", () => {
|
||||
const actual = counterReducer(initialState, decrement())
|
||||
expect(actual.value).toEqual(2)
|
||||
})
|
||||
|
||||
it("should handle incrementByAmount", () => {
|
||||
const actual = counterReducer(initialState, incrementByAmount(2))
|
||||
expect(actual.value).toEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { RootState, AppThunk } from "../../app/store"
|
||||
import { fetchCount } from "./counterAPI"
|
||||
|
||||
export interface CounterState {
|
||||
value: number
|
||||
status: "idle" | "loading" | "failed"
|
||||
}
|
||||
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
status: "idle",
|
||||
}
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched. Thunks are
|
||||
// typically used to make async requests.
|
||||
export const incrementAsync = createAsyncThunk(
|
||||
"counter/fetchCount",
|
||||
async (amount: number) => {
|
||||
const response = await fetchCount(amount)
|
||||
// The value we return becomes the `fulfilled` action payload
|
||||
return response.data
|
||||
},
|
||||
)
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState,
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn"t actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// including actions generated by createAsyncThunk or in other slices.
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(incrementAsync.pending, (state) => {
|
||||
state.status = "loading"
|
||||
})
|
||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
state.status = "idle"
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(incrementAsync.rejected, (state) => {
|
||||
state.status = "failed"
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they"re used instead of
|
||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||
export const selectCount = (state: RootState) => state.counter.value
|
||||
|
||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||
// Here"s an example of conditionally dispatching actions based on current state.
|
||||
export const incrementIfOdd =
|
||||
(amount: number): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
const currentValue = selectCount(getState())
|
||||
if (currentValue % 2 === 1) {
|
||||
dispatch(incrementByAmount(amount))
|
||||
}
|
||||
}
|
||||
|
||||
export default counterSlice.reducer
|
||||
8
src/index.ts
Executable file
8
src/index.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
export { default } from "./Calendar"
|
||||
export { default as DatePicker } from "./DatePicker"
|
||||
export { default as DateRangePicker } from "./DateRangePicker"
|
||||
export { default as JustCalendar } from "./JustCalendar"
|
||||
export { default as EventForm } from "./EventForm"
|
||||
export { default as EventManager } from "./EventManager"
|
||||
export { default as MonthNavigator } from "./MonthNavigator"
|
||||
export { default as MonthSelector } from "./MonthSelector"
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { store } from "./app/store"
|
||||
import Router from "./app/Router"
|
||||
import "./app/static/styles/global.scss"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<Router />
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user