Major reshuffle post demodularization

This commit is contained in:
2025-11-12 11:13:01 +05:30
commit 9cec2370bc
37 changed files with 11162 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
# Testing
coverage
# Production
build
# Miscellaneous
*.local
.DS_Store
# Component Modules built for local dev using lerna should be ignored
Calendar
DatePicker
DateRangePicker
EventForm
EventList
EventManager
JustCalendar
MonthNavigator
MonthSelector

6
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,6 @@
@Library('jenkins-shared') _
kanikoPipeline(
repoName: 'calendar',
branch: env.BRANCH_NAME ?: 'main',
isNpmLib: true
)

1
README.md Normal file
View File

@@ -0,0 +1 @@
# Armco Template for the tech stack: React, TS, Dart Sass, Redux Tookkit, react-redux, react browser routing, TS based plop generator

36
build-tools/build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi
echo "[BUILD:SH] Running post processor scripts..."
# Run Post processors: Update style imports in .js files, create component modules
node "$SCRIPT_DIR/post-processor.js" build/cjs $DEV_FLAG
node "$SCRIPT_DIR/post-processor.js" build/es $DEV_FLAG

View File

@@ -0,0 +1,35 @@
import { promises as fs } from "fs"
import { dirname, resolve } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
async function generateModule(fileName, isDev) {
if (
fileName.endsWith(".js") &&
fileName.indexOf("-chunk") === -1 &&
fileName !== "index.js"
) {
const dir = fileName.slice(0, -3)
const name = `@armco/calendar/${dir}`
const packageJsonContent = {
name,
main: `../${isDev ? "build/" : ""}cjs/${dir}.js`,
module: `../${isDev ? "build/" : ""}es/${dir}.js`,
types: `../${isDev ? "build/" : ""}types/${dir}.d.ts`,
}
const dirPath = resolve(__dirname, `../${isDev ? "" : "build/"}${dir}`)
try {
await fs.mkdir(dirPath, { recursive: true })
await fs.writeFile(
resolve(dirPath, "package.json"),
JSON.stringify(packageJsonContent, null, 2),
)
} catch (error) {
console.error(`Error processing directory ${dirPath}:`, error)
}
}
}
export default generateModule

View File

@@ -0,0 +1,24 @@
import { readdir } from "fs/promises"
import generateModule from "./generate-module.js"
async function postProcessor(dir, isDev) {
try {
const files = await readdir(dir)
await Promise.all(
files.map(async (file) => {
await generateModule(file, isDev)
}),
)
} catch (error) {
console.error(`Error processing directory ${dir}:`, error)
}
}
const targetDir = process.argv[2]
if (targetDir) {
postProcessor(targetDir, process.argv.includes("--dev"))
} else {
console.error("Please provide the build directory to run post processor on.")
process.exit(1)
}

8032
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "@armco/calendar",
"version": "0.0.7",
"type": "module",
"main": "build/cjs/index.js",
"module": "build/es/index.js",
"types": "build/types/index.d.ts",
"scripts": {
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev",
"format": "prettier --write .",
"lint": "eslint .",
"publish:sh": "./publish.sh",
"publish:local": "./publish-local.sh"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": "error",
"react/jsx-no-target-blank": "off"
}
},
"prettier": "prettier-config-nick",
"repository": {
"type": "git",
"url": "git+https://github.com/ReStruct-Corporate-Advantage/calendar.git"
},
"keywords": [
"components",
"atomic",
"building-blocks",
"foundation"
],
"license": "ISC",
"bugs": {
"url": "https://github.com/ReStruct-Corporate-Advantage/calendar/issues"
},
"homepage": "https://github.com/ReStruct-Corporate-Advantage/calendar#readme",
"dependencies": {
"@armco/configs": "^0.0.15",
"@armco/icon": "^0.0.13",
"@armco/shared-components": "^0.0.59",
"@armco/utils": "^0.0.31"
},
"devDependencies": {
"@armco/types": "^0.0.22",
"@vitejs/plugin-react": "^5.1.0",
"glob": "^11.0.3",
"sass-embedded": "^1.93.3",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-externalize-deps": "^0.10.0",
"vite-plugin-lib-inject-css": "^2.2.2",
"vitest": "^4.0.8"
}
}

16
publish-local.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
semver=${1:-patch}
set -e
npm run build
cp package.json build/
sed -i '' -E 's/"build"/"*"/' build/package.json
sed -i '' 's#"build/cjs/Icon.js"#"cjs/Icon.js"#' build/package.json
sed -i '' 's#"build/es/Icon.js"#"es/Icon.js"#' build/package.json
sed -i '' 's#"build/types/Icon.d.ts"#"types/Icon.d.ts"#' build/package.json
cd build
npm pack --pack-destination ~/__Projects__/Common

28
publish.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
semver=${1:-patch}
set -e
npm --no-git-tag-version version ${semver}
npm run build
cp package.json build/
# Use Node.js for portable package.json normalization
# Pass the target path via env var to avoid Node treating it as a module/script argument
PKG_PATH="$(pwd)/build/package.json" node - <<'EOF'
const fs = require('fs');
const path = process.env.PKG_PATH;
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
pkg.private = false;
delete pkg.scripts;
delete pkg.devDependencies;
if (!pkg.files) pkg.files = ['*'];
else pkg.files = pkg.files.map(x => x === 'build' ? '*' : x);
['main','module','types'].forEach(k => {
if (pkg[k]) pkg[k] = pkg[k].replace(/^build\//, '');
});
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
EOF
cd build
npm publish --access public --loglevel verbose

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

288
src/Calendar.tsx Executable file
View File

@@ -0,0 +1,288 @@
import { ReactNode, useEffect, useState } from "react"
import { ArButtonVariants, ArSizes } from "@armco/shared-components/enums"
import Button from "@armco/shared-components/Button"
import { ArCalViews, ArMonthSelectorViews } from "./enums"
import { CalendarDate, CalendarProps, Event } from "./types"
import { CalHelper, isWithinSchedule } from "./dateHelper"
import MonthSelector from "./MonthSelector"
import JustCalendar from "./JustCalendar"
import EventManager from "./EventManager"
import MonthNavigator from "./MonthNavigator"
import "./Calendar.component.scss"
const calHelper = new CalHelper({ siblingMonths: true, weekStart: 1 })
const today = new Date()
const Calendar = (props: CalendarProps): ReactNode => {
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;
}
}
}

72
src/DatePicker.tsx Executable file
View File

@@ -0,0 +1,72 @@
import { ReactNode, useEffect, useState } from "react"
import { FunctionType } from "@armco/types"
import { pad } from "@armco/utils/helper"
import { ArPopoverPositions, ArPopoverSlots } from "@armco/shared-components/enums"
import Popover from "@armco/shared-components/Popover"
import Icon from "@armco/icon"
import { CalendarDate, DatePickerProps } from "./types"
import Calendar from "./Calendar"
import "./DatePicker.component.scss"
const DatePicker = (props: DatePickerProps): ReactNode => {
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 &&
pad(date.day) + "/" + 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;
}
}

95
src/DateRangePicker.tsx Executable file
View File

@@ -0,0 +1,95 @@
import { ReactNode, useEffect, useState } from "react"
import { ArPopoverPositions, ArPopoverSlots } from "@armco/shared-components/enums"
import Icon from "@armco/icon"
import { pad } from "@armco/utils/helper"
import Popover from "@armco/shared-components/Popover"
import { DateRangePickerProps, CalendarDate } from "./types"
import { toDate } from "./dateHelper"
import { DateFormatter } from "./dateformat"
import Calendar from "./Calendar"
import "./DateRangePicker.component.scss"
const DateRangePicker = (props: DateRangePickerProps): ReactNode => {
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 =
pad(startDate.day) +
"/" +
pad(startDate.month + 1) +
"/" +
startDate.year
value += " - "
value +=
pad(endDate.day) + "/" + 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

273
src/EventForm.tsx Normal file
View File

@@ -0,0 +1,273 @@
import { useState } from "react"
import { FunctionType, ObjectType } from "@armco/types"
import { ArButtonVariants, ArSizes } from "@armco/shared-components/enums"
import Button from "@armco/shared-components/Button"
import SegmentedControl from "@armco/shared-components/SegmentedControl"
import Select from "@armco/shared-components/Select"
import TextArea from "@armco/shared-components/TextArea"
import TextInput from "@armco/shared-components/TextInput"
import TimeEntry from "@armco/shared-components/TimeEntry"
import Toggle from "@armco/shared-components/Toggle"
import { ArEventStates, ArEventTypes, ArSchedules } from "./enums"
import { Event, EventFormProps } from "./types"
import { toDate } from "./dateHelper"
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
)
}
export 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>
)
}

105
src/EventList.tsx Normal file
View File

@@ -0,0 +1,105 @@
import { ReactNode } from "react"
import { SegmentData } from "@armco/shared-components/entity"
import Icon from "@armco/icon"
import { EventListProps, Event } from "./types"
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>
<Icon attributes={{ classes: "me-2" }} icon={selectedEventType.icon} />
{preview}
</span>
) : (
preview
)
}
return (
<span
className={`ar-EventListItem small d-inline-block fw-bold${classes ? " " + classes : ""
}`}
>
{preview}
</span>
)
}
export 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>
)
}

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

57
src/EventManager.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { useEffect, useState } from "react"
import { pad } from "@armco/utils/helper"
import { ArButtonVariants, ArSizes } from "@armco/shared-components/enums"
import Button from "@armco/shared-components/Button"
import { EventManagerProps, Event } from "./types"
import { ArCalViews } from "./enums"
import { EventForm } from "./EventForm"
import { EventList } from "./EventList"
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">
{pad(selectedDate.day) +
" - " +
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;
}
}
}
}

169
src/JustCalendar.tsx Normal file
View File

@@ -0,0 +1,169 @@
import { useState } from "react"
import { ArBadgeType, ArSizes } from "@armco/shared-components/enums"
import Badge from "@armco/shared-components/Badge"
import Icon from "@armco/icon"
import { CalendarDate, DateItemProps, JustCalendarProps } from "./types"
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)}
>
<Icon
icon="io.IoIosAddCircle"
attributes={{
colors: {
fillColor: "#5cb85cb3",
hoverFillColor: "#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;
// }
// }
// }
}

174
src/MonthNavigator.tsx Normal file
View File

@@ -0,0 +1,174 @@
import { ArThemes } from "@armco/utils"
import Icon from "@armco/icon"
import { MonthNavigatorProps } from "./types"
import { ArCalViews, ArMonthSelectorViews } from "./enums"
import { CalHelper } from "./dateHelper"
import { MONTH_INDEX } from "./configs"
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))}
>
<Icon
icon="ai/AiOutlineDoubleLeft"
attributes={{
colors: {
fillColor: 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),
)
}
}}
>
<Icon
icon="ai/AiOutlineLeft"
attributes={{
colors: {
fillColor: 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),
)
}
}}
>
<Icon
icon="ai.AiOutlineRight"
attributes={{
colors: {
fillColor: 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))}
>
<Icon
icon="ai.AiOutlineDoubleRight"
attributes={{
colors: {
fillColor: 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);
}
}
}

132
src/MonthSelector.tsx Normal file
View File

@@ -0,0 +1,132 @@
import { MonthSelectorProps } from "./types"
import { ArCalViews, ArMonthSelectorViews } from "./enums"
import { MONTH_INDEX } from "./configs"
import { sub } from "./dateHelper"
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

14
src/configs.ts Normal file
View File

@@ -0,0 +1,14 @@
export const MONTH_INDEX = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]

547
src/dateHelper.ts Normal file
View File

@@ -0,0 +1,547 @@
import { pad } from "@armco/utils/helper"
import { ArDateFormats } from "./enums"
import { CalendarDate, Event } from "./types"
import { MONTH_INDEX } from "./configs"
export interface CalendarOptions {
/**
* Date object indicating the selected start date
*/
startDate?: CalendarDate | null
/**
* Date object indicating the selected end date
*/
endDate?: CalendarDate | null
/**
* Calculate dates from sibling months (before and after the current month, based on weekStart)
*/
siblingMonths?: boolean
/**
* Calculate the week days
*/
weekNumbers?: boolean
/**
* Day of the week to start the calendar, respects `Date.prototype.getDay` (defaults to `0`, Sunday)
*/
weekStart?: number
}
export const sub = (num: number, separator?: string) => {
return ("" + num).substring(2) + (separator ? separator : "")
}
export const str = (num: number, trim?: boolean, separator?: string) => {
const month = MONTH_INDEX[num]
return (trim ? month.substring(0, 4) : month) + (separator ? separator : "")
}
export type CalendarInstance = Calendar
/**
* Calendar object
*/
class Calendar {
startDate: CalendarDate | null
endDate: CalendarDate | null
siblingMonths: boolean
weekNumbers: boolean
weekStart: number
/**
* Calendar constructor
*
* @param options Calendar options
*/
constructor({
startDate = null,
endDate = null,
siblingMonths = false,
weekNumbers = false,
weekStart = 0,
}: CalendarOptions = {}) {
this.startDate = startDate
this.endDate = endDate
this.siblingMonths = siblingMonths
this.weekNumbers = weekNumbers
this.weekStart = weekStart
}
/**
* Calculate a calendar month
*
* @param year Year
* @param month Month [0-11]
* @return Calendar days
*/
getCalendar(year: number, month: number) {
const date = new Date(Date.UTC(year, month, 1, 0, 0, 0, 0))
year = date.getFullYear()
month = date.getMonth()
const calendar: (CalendarDate | false)[] = []
const firstDay = date.getDay()
const firstDate = -((7 - this.weekStart + firstDay) % 7)
const lastDate = Calendar.daysInMonth(year, month)
const lastDay = (lastDate - firstDate) % 7
const lastDatePreviousMonth = Calendar.daysInMonth(year, month - 1)
let i = firstDate
let currentDay
let currentDate
let currentDateObject: CalendarDate | false = false
let currentWeekNumber = null
let otherMonth
let otherYear
const max = lastDate - i + (lastDay !== 0 ? 7 - lastDay : 0) + firstDate
while (i < max) {
currentDate = i + 1
currentDay = ((i < 1 ? 7 + i : i) + firstDay) % 7
if (currentDate < 1 || currentDate > lastDate) {
if (this.siblingMonths) {
if (currentDate < 1) {
otherMonth = month - 1
otherYear = year
if (otherMonth < 0) {
otherMonth = 11
otherYear--
}
currentDate = lastDatePreviousMonth + currentDate
} else if (currentDate > lastDate) {
otherMonth = month + 1
otherYear = year
if (otherMonth > 11) {
otherMonth = 0
otherYear++
}
currentDate = i - lastDate + 1
}
if (otherMonth !== undefined && otherYear !== undefined) {
currentDateObject = {
day: currentDate,
weekDay: currentDay,
month: otherMonth,
year: otherYear,
siblingMonth: true,
}
}
} else {
currentDateObject = false
}
} else {
currentDateObject = {
day: currentDate,
weekDay: currentDay,
month: month,
year: year,
}
}
if (currentDateObject && this.weekNumbers) {
if (currentWeekNumber === null) {
currentWeekNumber = Calendar.calculateWeekNumber(currentDateObject)
} else if (currentDay === 1 && currentWeekNumber === 52) {
currentWeekNumber = 1
} else if (currentDay === 1) {
currentWeekNumber++
}
currentDateObject.weekNumber = currentWeekNumber
}
if (currentDateObject && this.startDate) {
currentDateObject.selected = this.isDateSelected(currentDateObject)
}
calendar.push(currentDateObject)
i++
}
return calendar
}
/**
* Checks if a date is selected
*
* @param date Date object
* @return Selected status of the date
*/
isDateSelected(date: CalendarDate, endDate?: CalendarDate | null) {
// Hack to disregard this.endDate in further calculations for finding selectable candidates (hovered)
// by explicitly sending a "null" endDate, if endDate is not sent at all (undefined) then only function behaves as
// initially intended and considers endDate for calculations
if (!this.startDate || endDate === null) {
return false
}
let startDate = this.startDate
if (endDate) {
if (Calendar.compare(this.startDate, endDate) === 1) {
startDate = endDate
endDate = this.startDate
}
} else {
endDate = this.endDate
}
if (
date.year === startDate.year &&
date.month === startDate.month &&
date.day === startDate.day
) {
return true
}
if (!endDate) {
return false
}
if (
date.year === startDate.year &&
date.month === startDate.month &&
date.day < startDate.day
) {
return false
}
if (
date.year === endDate.year &&
date.month === endDate.month &&
date.day > endDate.day
) {
return false
}
if (date.year === startDate.year && date.month < startDate.month) {
return false
}
if (date.year === endDate.year && date.month > endDate.month) {
return false
}
if (date.year < startDate.year) {
return false
}
if (date.year > endDate.year) {
return false
}
return true
}
/**
* Sets the selected period start
*
* @param date Date object
*/
setStartDate(date: CalendarDate | null) {
this.startDate = date
}
/**
* Sets the selected period end
*
* @param date Date object
*/
setEndDate(date: CalendarDate | null) {
this.endDate = date
}
/**
* Sets one selected date
*
* @param date Date object
*/
setDate(date: CalendarDate) {
return this.setStartDate(date)
}
/**
* Calculates the difference between two dates (date1 - date2), in days
*
* @param dateLeft Date object
* @param dateRight Date object
* @return Days between the dates
*/
static diff(dateLeft: CalendarDate, dateRight: CalendarDate) {
const dateLeftDate = new Date(
Date.UTC(dateLeft.year, dateLeft.month, dateLeft.day, 0, 0, 0, 0),
)
const dateRightDate = new Date(
Date.UTC(dateRight.year, dateRight.month, dateRight.day, 0, 0, 0, 0),
)
return Math.ceil(
(dateLeftDate.getTime() - dateRightDate.getTime()) / 86400000,
)
}
/**
* Calculates the interval between two dates
*
* @param dateLeft Date object
* @param dateRight Date object
* @return Number of days between dates
*/
static interval(dateLeft: CalendarDate, dateRight: CalendarDate) {
return Math.abs(Calendar.diff(dateLeft, dateRight)) + 1
}
/**
* Quickly compare two dates
*
* @param dateLeft Left `CalendarDate` object
* @param dateRight Right `CalendarDate` object
* @return Comparison result: -1 (left < right), 0 (equal) or 1 (left > right)
*/
static compare(dateLeft: CalendarDate, dateRight: CalendarDate) {
if (
typeof dateLeft !== "object" ||
typeof dateRight !== "object" ||
dateLeft === null ||
dateRight === null
) {
throw new TypeError("dates must be objects")
}
if (dateLeft.year < dateRight.year) {
return -1
}
if (dateLeft.year > dateRight.year) {
return 1
}
if (dateLeft.month < dateRight.month) {
return -1
}
if (dateLeft.month > dateRight.month) {
return 1
}
if (dateLeft.day < dateRight.day) {
return -1
}
if (dateLeft.day > dateRight.day) {
return 1
}
return 0
}
/**
* Calculates the number of days in a month
*
* @param year Year
* @param month Month [0-11]
* @return Length of the month
*/
static daysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate()
}
/**
* Calculates if a given year is a leap year
*
* @param year Year
* @return Leap year or not
*/
static isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}
/**
* Calculates the week number for a given date
*
* @param date Date object
* @return Week number
*/
// Adapted from http://techblog.procurios.nl/k/news/view/33796/14863/calculate-iso-8601-week-and-year-in-javascript.html
static calculateWeekNumber(date: CalendarDate) {
// Creates the requested date
const current = new Date(
Date.UTC(date.year, date.month, date.day, 0, 0, 0, 0),
)
// Create a copy of the object
const target = new Date(current.valueOf())
// ISO week date weeks start on monday so correct the day number
const dayNr = (current.getUTCDay() + 6) % 7
// ISO 8601 states that week 1 is the week with the first thursday of that
// year. Set the target date to the thursday in the target week.
target.setUTCDate(target.getUTCDate() - dayNr + 3)
// Store the millisecond value of the target date
const firstThursday = target.valueOf()
// Set the target to the first thursday of the year
// First set the target to january first
target.setUTCMonth(0, 1)
// Not a thursday? Correct the date to the next thursday
if (target.getUTCDay() !== 4) {
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7))
}
// The week number is the number of weeks between the first thursday of the
// year and the thursday in the target week.
// 604800000 = 7 * 24 * 3600 * 1000
return 1 + Math.ceil((firstThursday - target.getTime()) / 604800000)
}
static toString(
date: CalendarDate,
separator: string = "/",
format: string = "DDMMYYYY",
) {
const day = pad(date.day, separator)
const month = pad(date.month + 1, separator)
const year = date.year
switch (format) {
case ArDateFormats.MMDDYY:
return month + day + sub(year)
case ArDateFormats.DDMMYYYY:
return day + month + year
case ArDateFormats.MMDDYYYY:
return month + day + year
case ArDateFormats.DDMMMYY:
return day + str(date.month + 1, true, separator) + sub(year)
case ArDateFormats.DDMMMYYYY:
return day + str(date.month + 1, true, separator) + year
case ArDateFormats.MMMDDYY:
return str(date.month + 1, true, separator) + day + sub(year)
case ArDateFormats.MMMDDYYYY:
return str(date.month + 1, true, separator) + day + year
case ArDateFormats.YYMMDD:
return sub(year) + month + day
case ArDateFormats.YYMMMDD:
return sub(year) + str(date.month + 1, true, separator) + day
case ArDateFormats.YYYYMMDD:
return year + month + day
case ArDateFormats.YYYYMMMDD:
return year + str(date.month + 1, true, separator) + day
case ArDateFormats.DDMMYY:
default:
return day + month + sub(year)
}
}
static getPreviousMonth(date: Date) {
const month = date.getMonth()
const year = date.getFullYear()
const previousMonth = month === 0 ? 11 : month - 1
const previousYear = month === 0 ? year - 1 : year
const returnDate = new Date()
returnDate.setDate(date.getDate())
returnDate.setMonth(previousMonth)
returnDate.setFullYear(previousYear)
return returnDate
}
static getNextMonth(date: Date) {
const month = date.getMonth()
const year = date.getFullYear()
const previousMonth = month === 11 ? 0 : month + 1
const previousYear = month === 11 ? year + 1 : year
const returnDate = new Date()
returnDate.setDate(date.getDate())
returnDate.setMonth(previousMonth)
returnDate.setFullYear(previousYear)
return returnDate
}
static getPreviousYear(date: Date) {
const year = date.getFullYear()
const returnDate = new Date()
returnDate.setDate(date.getDate())
returnDate.setMonth(date.getMonth())
returnDate.setFullYear(year === 1 ? 1 : year - 1)
return returnDate
}
static getNextYear(date: Date) {
const year = date.getFullYear()
const returnDate = new Date()
returnDate.setDate(date.getDate())
returnDate.setMonth(date.getMonth())
returnDate.setFullYear(year + 1)
return returnDate
}
}
export const toDate = (
calDate: CalendarDate,
hour?: number,
minute?: number,
ampm: string = "AM",
) => {
if (hour) {
let adjustedHour = hour
if (ampm === "PM" && hour !== 12) {
adjustedHour += 12 // Add 12 hours for PM time, except when it's 12 PM
} else if (ampm === "AM" && hour === 12) {
adjustedHour = 0 // 12 AM is equivalent to 0 hours
}
return new Date(
calDate.year,
calDate.month,
calDate.day,
adjustedHour,
minute,
)
} else return new Date(calDate.year, calDate.month, calDate.day)
}
export const isWithinSchedule = (date: CalendarDate, event: Event) => {
const startDate = event.schedule?.starts
const endDate = event.schedule?.ends
const startCalDate = startDate && {
day: (startDate as Date).getDate(),
month: (startDate as Date).getMonth(),
year: (startDate as Date).getFullYear(),
}
const endCalDate = endDate && {
day: (endDate as Date).getDate(),
month: (endDate as Date).getMonth(),
year: (endDate as Date).getFullYear(),
}
if (startCalDate && endCalDate) {
const checkStart = Calendar.compare(date, startCalDate)
const checkEnd = Calendar.compare(date, endCalDate)
const amPm = (endDate as Date)
?.toLocaleString("en-US", { hour12: true })
.split(" ")[2]
const shouldExcludeEnd =
(endDate as Date).getHours() === 0 &&
amPm === "AM" &&
date.day === endCalDate.day
return checkStart >= 0 && checkEnd <= 0 && !shouldExcludeEnd
}
return false
}
/**
* Exports the Calendar
*/
export { Calendar as CalHelper }

338
src/dateformat.ts Normal file
View File

@@ -0,0 +1,338 @@
import { FunctionType } from "@armco/types"
import { ArDateMasks } from "./enums"
const token =
/d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g
const timezone =
/\b(?:[A-Z]{1,3}[A-Z][TC])(?:[-+]\d{4})?|((?:Australian )?(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time)\b/g
const timezoneClip = /[^-+\dA-Z]/g
/**
* @param {string | number | Date} date
* @param {string} mask
* @param {boolean} utc
* @param {boolean} gmt
*/
export function dateFormat(
date?: number | Date,
mask?: ArDateMasks | string,
utc?: boolean,
gmt?: boolean,
) {
// You can't provide utc if you skip other args (use the 'UTC:' mask prefix)
if (arguments.length === 1 && typeof date === "string" && !/\d/.test(date)) {
mask = date
date = undefined
}
date = date || date === 0 ? date : new Date()
if (isNaN(date as number)) {
throw TypeError("Invalid date")
}
if (!(date instanceof Date)) {
date = new Date(date)
}
mask = String(
ArDateMasksRecord[mask as string] || mask || ArDateMasksRecord["DEFAULT"],
)
// Allow setting the utc/gmt argument via the mask
const maskSlice = mask.slice(0, 4)
if (maskSlice === "UTC:" || maskSlice === "GMT:") {
mask = mask.slice(4)
utc = true
if (maskSlice === "GMT:") {
gmt = true
}
}
const _ = () => (utc ? "getUTC" : "get")
const d = () => (date as Date)[(_() + "Date") as "getDate" | "getUTCDate"]()
const D = () => (date as Date)[(_() + "Day") as "getDay" | "getUTCDay"]()
const m = () =>
(date as Date)[(_() + "Month") as "getMonth" | "getUTCMonth"]()
const y = () =>
(date as Date)[(_() + "FullYear") as "getFullYear" | "getUTCFullYear"]()
const H = () =>
(date as Date)[(_() + "Hours") as "getHours" | "getUTCHours"]()
const M = () =>
(date as Date)[(_() + "Minutes") as "getMinutes" | "getUTCMinutes"]()
const s = () =>
(date as Date)[(_() + "Seconds") as "getSeconds" | "getUTCSeconds"]()
const L = () =>
(date as Date)[
(_() + "Milliseconds") as "getMilliseconds" | "getUTCMilliseconds"
]()
const o = () => (utc ? 0 : (date as Date).getTimezoneOffset())
const W = () => date instanceof Date && getWeek(date)
const N = () => date instanceof Date && getDayOfWeek(date)
const flags: { [key: string]: FunctionType } = {
d: () => d(),
dd: () => pad(d()),
ddd: () => i18n.dayNames[D()],
DDD: () =>
getDayName({
y: y(),
m: m(),
d: d(),
_: _(),
dayName: i18n.dayNames[D()],
short: true,
}),
dddd: () => i18n.dayNames[D() + 7],
DDDD: () =>
getDayName({
y: y(),
m: m(),
d: d(),
_: _(),
dayName: i18n.dayNames[D() + 7],
}),
m: () => m() + 1,
mm: () => pad(m() + 1),
mmm: () => i18n.monthNames[m()],
mmmm: () => i18n.monthNames[m() + 12],
yy: () => String(y()).slice(2),
yyyy: () => pad(y(), 4),
h: () => H() % 12 || 12,
hh: () => pad(H() % 12 || 12),
H: () => H(),
HH: () => pad(H()),
M: () => M(),
MM: () => pad(M()),
s: () => s(),
ss: () => pad(s()),
l: () => pad(L(), 3),
L: () => pad(Math.floor(L() / 10)),
t: () => (H() < 12 ? i18n.timeNames[0] : i18n.timeNames[1]),
tt: () => (H() < 12 ? i18n.timeNames[2] : i18n.timeNames[3]),
T: () => (H() < 12 ? i18n.timeNames[4] : i18n.timeNames[5]),
TT: () => (H() < 12 ? i18n.timeNames[6] : i18n.timeNames[7]),
Z: () =>
gmt ? "GMT" : utc ? "UTC" : date instanceof Date && formatTimezone(date),
o: () =>
(o() > 0 ? "-" : "+") +
pad(Math.floor(Math.abs(o()) / 60) * 100 + (Math.abs(o()) % 60), 4),
p: () =>
(o() > 0 ? "-" : "+") +
pad(Math.floor(Math.abs(o()) / 60), 2) +
":" +
pad(Math.floor(Math.abs(o()) % 60), 2),
S: () =>
["th", "st", "nd", "rd"][
d() % 10 > 3 ? 0 : (+((d() % 100) - (d() % 10) !== 10) * d()) % 10
],
W: () => W(),
WW: () => {
const date = W()
date && pad(date)
},
N: () => N(),
}
return mask.replace(token, (match) => {
if (match in flags) {
return flags[match]()
}
return match.slice(1, match.length - 1)
})
}
const ArDateMasksRecord: Record<string, string> = {
DEFAULT: ArDateMasks.DEFAULT,
SHORTDATE: ArDateMasks.SHORTDATE,
PADDEDSHORTDATE: ArDateMasks.PADDEDSHORTDATE,
MEDIUMDATE: ArDateMasks.MEDIUMDATE,
LONGDATE: ArDateMasks.LONGDATE,
FULLDATE: ArDateMasks.FULLDATE,
SHORTTIME: ArDateMasks.SHORTTIME,
MEDIUMTIME: ArDateMasks.MEDIUMTIME,
LONGTIME: ArDateMasks.LONGTIME,
ISODATE: ArDateMasks.ISODATE,
ISOTIME: ArDateMasks.ISOTIME,
ISODATETIME: ArDateMasks.ISODATETIME,
ISOUTCDATETIME: ArDateMasks.ISOUTCDATETIME,
EXPIRESHEADERFORMAT: ArDateMasks.EXPIRESHEADERFORMAT,
}
// Internationalization strings
export let i18n = {
dayNames: [
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
monthNames: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
timeNames: ["a", "p", "am", "pm", "A", "P", "AM", "PM"],
}
const pad = (val: number, len = 2) => String(val).padStart(len, "0")
/**
* Get day name
* Yesterday, Today, Tomorrow if the date lies within, else fallback to Monday - Sunday
* @param {Object}
* @return {String}
*/
const getDayName = ({
y,
m,
d,
_,
dayName,
short = false,
}: {
y: number
m: number
d: number
_: string
dayName: string
short?: boolean
}) => {
const today = new Date()
const yesterday = new Date()
const tomorrow = new Date()
const dateAccessor = (_ + "Date") as "getDate" | "getUTCDate"
const monthAccessor = (_ + "Month") as "getMonth" | "getUTCMonth"
const yearAccessor = (_ + "FullYear") as "getFullYear" | "getUTCFullYear"
yesterday.setDate(yesterday[dateAccessor]() - 1)
tomorrow.setDate(tomorrow[dateAccessor]() + 1)
const today_d = () => today[dateAccessor]()
const today_m = () => today[monthAccessor]()
const today_y = () => today[yearAccessor]()
const yesterday_d = () => yesterday[dateAccessor]()
const yesterday_m = () => yesterday[monthAccessor]()
const yesterday_y = () => yesterday[yearAccessor]()
const tomorrow_d = () => tomorrow[dateAccessor]()
const tomorrow_m = () => tomorrow[monthAccessor]()
const tomorrow_y = () => tomorrow[yearAccessor]()
if (today_y() === y && today_m() === m && today_d() === d) {
return short ? "Tdy" : "Today"
} else if (
yesterday_y() === y &&
yesterday_m() === m &&
yesterday_d() === d
) {
return short ? "Ysd" : "Yesterday"
} else if (tomorrow_y() === y && tomorrow_m() === m && tomorrow_d() === d) {
return short ? "Tmw" : "Tomorrow"
}
return dayName
}
/**
* Get the ISO 8601 week number
* Based on comments from
* http://techblog.procurios.nl/k/n618/news/view/33796/14863/Calculate-ISO-8601-week-and-year-in-javascript.html
*
* @param {Date} `date`
* @return {Number}
*/
const getWeek = (date: Date) => {
// Remove time components of date
const targetThursday = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
)
// Change date to Thursday same week
targetThursday.setDate(
targetThursday.getDate() - ((targetThursday.getDay() + 6) % 7) + 3,
)
// Take January 4th as it is always in week 1 (see ISO 8601)
const firstThursday = new Date(targetThursday.getFullYear(), 0, 4)
// Change date to Thursday same week
firstThursday.setDate(
firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3,
)
// Check if daylight-saving-time-switch occurred and correct for it
const ds =
targetThursday.getTimezoneOffset() - firstThursday.getTimezoneOffset()
targetThursday.setHours(targetThursday.getHours() - ds)
// Number of weeks between target Thursday and first Thursday
const weekDiff =
(targetThursday.getTime() - firstThursday.getTime()) / (86400000 * 7)
return 1 + Math.floor(weekDiff)
}
/**
* Get ISO-8601 numeric representation of the day of the week
* 1 (for Monday) through 7 (for Sunday)
*
* @param {Date} `date`
* @return {Number}
*/
const getDayOfWeek = (date: Date) => {
let dow = date.getDay()
if (dow === 0) {
dow = 7
}
return dow
}
/**
* Get proper timezone abbreviation or timezone offset.
*
* This will fall back to `GMT+xxxx` if it does not recognize the
* timezone within the `timezone` RegEx above. Currently only common
* American and Australian timezone abbreviations are supported.
*
* @param {String | Date} date
* @return {String}
*/
export const formatTimezone = (date: Date) => {
const strDate = String(date)
const match = strDate.match(timezone) || [""]
return match
.pop()
?.replace(timezoneClip, "")
.replace(/GMT\+0000/g, "UTC")
}
export { dateFormat as DateFormatter }

67
src/enums.ts Normal file
View File

@@ -0,0 +1,67 @@
export enum ArDateFormats {
DDMMYYYY = "DDMMYYYY",
DDMMYY = "DDMMYY",
MMDDYYYY = "MMDDYYYY",
MMDDYY = "MMDDYY",
YYMMDD = "YYMMDD",
YYYYMMDD = "YYYYMMDD",
DDMMMYY = "DDMMMYY",
DDMMMYYYY = "DDMMMYYYY",
MMMDDYY = "MMMDDYY",
MMMDDYYYY = "MMMDDYYYY",
YYYYMMMDD = "YYYYMMMDD",
YYMMMDD = "YYMMMDD",
}
export enum ArDateMasks {
DEFAULT = "ddd mmm dd yyyy HH:MM:ss",
SHORTDATE = "m/d/yy",
PADDEDSHORTDATE = "mm/dd/yyyy",
MEDIUMDATE = "mmm d, yyyy",
LONGDATE = "mmmm d, yyyy",
FULLDATE = "dddd, mmmm d, yyyy",
SHORTTIME = "h:MM TT",
MEDIUMTIME = "h:MM:ss TT",
LONGTIME = "h:MM:ss TT Z",
ISODATE = "yyyy-mm-dd",
ISOTIME = "HH:MM:ss",
ISODATETIME = "yyyy-mm-dd'T'HH:MM:sso",
ISOUTCDATETIME = "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'",
EXPIRESHEADERFORMAT = "ddd, dd mmm yyyy HH:MM:ss Z",
}
export enum ArCalViews {
CALENDAR = "calendar",
MONTH_YEAR_SELECTOR = "month-year-selector",
EVENT_FORM = "event-form",
}
export enum ArMonthSelectorViews {
DECADE = "decade",
YEAR = "year",
MONTH = "month",
}
export enum ArEventStates {
DRAFT = "draft",
SCHEDULED = "scheduled",
EXPIRED = "expired",
CANCELLED = "cancelled",
DEFERRED = "deferred",
}
export enum ArEventTypes {
TASK = "task",
EVENT = "event",
BIRTHDAY = "birthday",
MEETING = "meeting",
}
export enum ArSchedules {
TODAY = "today",
TOMORROW = "tomorrow",
NEXTWEEK = "next-week",
NEXTMONTH = "next-month",
CUSTOM = "custom",
}

9
src/index.ts Executable file
View File

@@ -0,0 +1,9 @@
export { default } from "./Calendar"
export { default as DatePicker } from "./DatePicker"
export { default as DateRangePicker } from "./DateRangePicker"
export { default as JustCalendar } from "./JustCalendar"
export { EventList } from "./EventList"
export { EventForm } from "./EventForm"
export { default as EventManager } from "./EventManager"
export { default as MonthNavigator } from "./MonthNavigator"
export { default as MonthSelector } from "./MonthSelector"

71
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,71 @@
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production"
readonly PUBLIC_URL: string
}
}
declare module "*.avif" {
const src: string
export default src
}
declare module "*.bmp" {
const src: string
export default src
}
declare module "*.gif" {
const src: string
export default src
}
declare module "*.jpg" {
const src: string
export default src
}
declare module "*.jpeg" {
const src: string
export default src
}
declare module "*.png" {
const src: string
export default src
}
declare module "*.webp" {
const src: string
export default src
}
declare module "*.svg" {
import * as React from "react"
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>
const src: string
export default src
}
declare module "*.module.css" {
const classes: { readonly [key: string]: string }
export default classes
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string }
export default classes
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string }
export default classes
}

115
src/types.ts Normal file
View File

@@ -0,0 +1,115 @@
import { BaseProps, DateFormat, FunctionType } from "@armco/types"
import { FormInputProps } from "@armco/shared-components/types"
import { ArCalViews, ArDateFormats, ArDateMasks, ArEventStates, ArEventTypes, ArMonthSelectorViews, ArSchedules } from "./enums"
export interface CalendarProps extends BaseProps {
allowEventSetting?: boolean
events?: Array<Event>
customDayEventSetter?: FunctionType
endDate?: Date | ArDateFormats | CalendarDate
hasTimeControls?: boolean
isSingleSelect?: boolean
maxDate?: Date | DateFormat | CalendarDate
minDate?: Date | DateFormat | CalendarDate
miniMode?: boolean
onDateSelected?: FunctionType
startDate?: Date | DateFormat | CalendarDate
}
export interface CalendarDate {
day: number
month: number
selected?: boolean
siblingMonth?: boolean
weekDay?: number
weekNumber?: number
year: number
}
export interface Event {
title: string
eventType?: ArEventTypes
description?: string
state: ArEventStates
schedule?: Schedule
}
export interface Schedule {
isWholeDay?: boolean
starts?: Date | ArSchedules
ends?: Date | ArSchedules
}
export interface DatePickerProps extends FormInputProps {
allowEventSetting?: boolean
placeholder?: string
}
export interface DateRangePickerProps extends FormInputProps {
closeOnEndSelect?: boolean
mask?: ArDateMasks | string
}
export interface EventFormProps extends BaseProps {
miniMode?: boolean
onSubmit: FunctionType
selectedDate: CalendarDate
}
export interface EventListProps extends BaseProps {
events?: Array<Event>
}
export interface EventManagerProps extends BaseProps {
events?: Array<Event>
miniMode?: boolean
onEventSubmit: FunctionType
selectedDate: CalendarDate
setCurrentView: FunctionType
}
export interface JustCalendarProps extends BaseProps {
allowEventSetting?: boolean
calHelper?: any
calendar?: Array<CalendarDate | false>
endDate?: CalendarDate
hovered?: CalendarDate
isSingleSelect?: boolean
miniMode?: boolean
onDateSelected: FunctionType
setHovered: FunctionType
startDate?: CalendarDate
}
export interface DateItemProps extends JustCalendarProps {
date: CalendarDate
isToday: boolean
}
export interface MonthNavigatorProps extends BaseProps {
currentDecade?: number
currentMonthNavView?: ArMonthSelectorViews | null
currentView: ArCalViews
currentYear?: number
miniMode?: boolean
refDate: Date
setCurrentDecade: FunctionType
setCurrentMonthNavView: FunctionType
setCurrentView: FunctionType
setCurrentYear: FunctionType
setRefDate: FunctionType
}
export interface MonthSelectorProps extends BaseProps {
currentDecade?: number
currentMonthNavView?: ArMonthSelectorViews | null
currentView: ArCalViews
currentYear?: number
miniMode?: boolean
onMonthSelect: FunctionType
setCurrentDecade: FunctionType
setCurrentMonthNavView: FunctionType
setCurrentView: FunctionType
setCurrentYear: FunctionType
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"outDir": "build",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

41
vite-dev.config.ts Normal file
View File

@@ -0,0 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
import { externalizeDeps } from "vite-plugin-externalize-deps"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
libInjectCss(),
dts({ outDir: "build/types" }),
externalizeDeps(),
],
build: {
outDir: "build",
sourcemap: true,
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

40
vite.config.ts Normal file
View File

@@ -0,0 +1,40 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
import { externalizeDeps } from "vite-plugin-externalize-deps"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
libInjectCss(),
dts({ outDir: "build/types" }),
externalizeDeps(),
],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})