First commit with sources

This commit is contained in:
2024-09-18 17:54:00 +05:30
parent 32021178e7
commit 6eb002ff11
37 changed files with 1681 additions and 344 deletions

View File

@@ -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
View 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);
}
}
}

View File

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

View File

@@ -0,0 +1,5 @@
.ar-DateRangePicker {
.ar-DateRangePicker__input {
min-width: 15rem;
}
}

101
src/DateRangePicker.tsx Executable file
View 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
View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
.c-Home {
}

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
import Home from "./Home.jsx"
export default Home

View File

@@ -1,7 +0,0 @@
/* PLOP_INJECT_IMPORT */
import Home from "./Home"
export {
/* PLOP_INJECT_EXPORT */
Home,
}

View File

@@ -1,9 +0,0 @@
const ROUTES = [
{
path: "/",
class: "landing",
element: "Home",
},
]
export default ROUTES

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
interface RouteConfig {
path: String
class?: String
element: String | JSX.Element | null
children?: Array<RouteConfig>
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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"

View File

@@ -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>,
)