Compare commits

...

4 Commits

8 changed files with 230 additions and 47 deletions

View File

@ -10,8 +10,13 @@ body {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.border { .border {
display: flex;
flex-direction: column;
gap: 0.5rem;
> * {
background: #fff; background: #fff;
border-radius: 5px; border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
} }
} }
}

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { SingleDatePicker } from "../src/index" import { SingleDatePicker, SingleWeekPicker } from "../src/index"
import './app.scss' import './app.scss'
export default () => { export default () => {
@ -9,18 +9,9 @@ export default () => {
} }
return (<div className='app'> return (<div className='app'>
<div className="border"> <div className="border">
<SingleDatePicker <SingleWeekPicker />
value={{ <SingleDatePicker />
year: 2025,
month: 1,
day: 1
}}
onSelect={onSelect}
localization="zh-CN"
onClose={() => alert('close')}
/>
</div> </div>
</div>) </div>)
} }

View File

@ -15,8 +15,7 @@ export interface SingleDatePickerProps {
/** /**
* A callback function that will be called when a date is selected inside the panel. * A callback function that will be called when a date is selected inside the panel.
* @param date - The date user selected. * @param {{ year: number, month: number, day: number }} - The date user selected.
* @returns {{ year: number, month: number, day: number }} - The date user selected.
* @example { year: 2025, month: 1, day: 1 } // User selected 1 Jan 2025 * @example { year: 2025, month: 1, day: 1 } // User selected 1 Jan 2025
*/ */
onSelect?: (date: { onSelect?: (date: {
@ -74,6 +73,7 @@ export interface SingleDatePickerProps {
} }
/** /**
* SingleDatePicker
* A panel that allows users to select a date. * A panel that allows users to select a date.
* *
* @component * @component
@ -123,16 +123,6 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
}) })
} , [mainColor, accentColor, reversedColor, hoverColor, borderColor]) } , [mainColor, accentColor, reversedColor, hoverColor, borderColor])
useEffect(() => {
applyColor(uniqueId, {
mainColor: mainColor,
accentColor: accentColor,
reversedColor: reversedColor,
hoverColor: hoverColor,
borderColor: borderColor
})
}, [])
function selectDate(date: Date) { function selectDate(date: Date) {
setSelectedDate(date) setSelectedDate(date)
onSelect?.({ onSelect?.({
@ -205,7 +195,7 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
</button> </button>
<button className='stepper' onClick={skipToNextMonth} aria-label={`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></button> <button className='stepper' onClick={skipToNextMonth} aria-label={`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></button>
</div> </div>
<div className='calendar-view-body' aria-live="polite"> <div className='calendar-view-body grid' aria-live="polite">
{l10nDays.map((day, index) => <div className='item day-indicator' key={index}>{day}</div>)} {l10nDays.map((day, index) => <div className='item day-indicator' key={index}>{day}</div>)}
{dates.map(date => <button {dates.map(date => <button

View File

@ -0,0 +1,156 @@
import { useEffect, useState } from "react"
import { generateUniqueId, applyColor, getL10Weekday, getCalendarDates, calculateWeekNum } from "../utils"
export interface SingleWeekPickerProps {
/**
* The language code that will be used to localize the panel.
* Accept standard ISO 639-1 language code, such as 'zh-CN', 'en-US', 'ja-JP', etc. Note
* that it will not effect to the screen reader, but the screen reader will still read the
* date in the users language.
* @default navigator.language
*/
localization?: string
/**
* The main color of the panel, including the text color and the border color.
*@default '#000000'
*/
mainColor?: string
/**
* The accent color of the panel, including the background color of the selected date.
*@default '#000000'
*/
accentColor?: string
/**
* The reversed color of the panel, including the text color of the selected date.
*@default '#ffffff'
*/
reversedColor?: string
/**
* The hover color of the panel, including the hover background color of the date.
*@default '#00000017'
*/
hoverColor?: string
/**
* The border color of the panel, including the divider color between the header and the body.
*@default '#e0e0e0'
*/
borderColor?: string
/**
* A callback function that will be called when a week is selected inside the panel. Note that
* Datenel will follow the ISO 8601 standard as well as the return rules of Luxon, which will
* treat the week number 53 as the first week of the next year if exist.
* @param {{ year: number, month: number, day: number }} - The date user selected.
* @example { year: 2025, month: 1, day: 1 } // User selected 1 Jan 2025
*/
onSelect?: (date: {
weekYear: number,
weekNum: number
}) => void
}
/**
* SingleWeekPicker
* A panel that allows users to select a week.
*
* @component
*
* @param
*/
export default ({ localization, mainColor = '#000000', accentColor = '#000000', reversedColor = '#ffffff', hoverColor = '#00000017', borderColor = '#e0e0e0' }: SingleWeekPickerProps) => {
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth())
const [currentYear, setCurrentYear] = useState(new Date().getFullYear())
const [selectedWeek, setSelectedWeek] = useState<{ weekYear: number, weekNum: number }>(calculateWeekNum(new Date()))
const [selectMonth, setSelectMonth] = useState(false)
const [calendarWeeks, setCalendarWeeks] = useState<Date[][]>([])
const [l10nDays, setL10nDays] = useState<string[]>([])
const uniqueId = generateUniqueId()
function skipToLastMonth() {
if (currentMonth === 0) {
setCurrentMonth(11)
setCurrentYear(currentYear - 1)
}
else setCurrentMonth(currentMonth - 1)
}
function skipToNextMonth() {
if (currentMonth === 11) {
setCurrentMonth(0)
setCurrentYear(currentYear + 1)
}
else setCurrentMonth(currentMonth + 1)
}
useEffect(() => {
applyColor(uniqueId, {
mainColor: mainColor,
accentColor: accentColor,
reversedColor: reversedColor,
hoverColor: hoverColor,
borderColor: borderColor
})
}, [mainColor, accentColor, reversedColor, hoverColor, borderColor])
useEffect(() => {
const i18n = localization || navigator.language
setL10nDays(getL10Weekday(i18n))
}, [localization])
useEffect(() => {
const dates = getCalendarDates(currentMonth, currentYear)
let weeks: Date[][] = []
for (let i = 0; i < dates.length; i += 7)
weeks.push(dates.slice(i, i + 7))
setCalendarWeeks(weeks)
}, [currentMonth, currentYear])
useEffect(() => {
const date = new Date(2033, 0, 1)
console.log(date)
console.log(calculateWeekNum(date))
}, [])
function selectWeek(date: Date) {
setSelectedWeek(calculateWeekNum(date))
}
if (selectMonth) {
return <div className='datenel-component' role="dialog" aria-label="Week selection panel, you are now at month and year quick-select" id={`__datenel-${uniqueId}`}></div>
} else {
return <div className='datenel-component' role="dialog" aria-label="Week selection panel" id={`__datenel-${uniqueId}`}>
<div className='header'>
<button className='stepper' onClick={skipToLastMonth} aria-label={`Go to last month, ${new Date(currentYear, currentMonth - 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"></path></svg></button>
<button className='indicator' onClick={() => setSelectMonth(true)} aria-label={`You are now at ${new Date(currentYear, currentMonth).toLocaleString(localization || navigator.language, { month: 'long', year: 'numeric' })}. Click here to quick-select month or year.`}>
{new Date(currentYear, currentMonth).toLocaleString(localization || navigator.language, { month: 'long', year: 'numeric' })}
</button>
<button className='stepper' onClick={skipToNextMonth} aria-label={`Go to next month, ${new Date(currentYear, currentMonth + 1).toLocaleString(localization || navigator.language, { month: 'long' })}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></button>
</div>
<div className='calendar-view-body flex' aria-live="polite">
<div className="listitem">
{Array.from({ length: 7 }).map((_, index) => <div className='item day-indicator' key={index}>{l10nDays[index]}</div>)}
</div>
{calendarWeeks.map((week, index) => {
const isSelected = selectedWeek.weekYear === calculateWeekNum(week[0]).weekYear && selectedWeek.weekNum === calculateWeekNum(week[0]).weekNum
return <button className={`listitem ${isSelected ? 'active' : ''}`} key={index} onClick={() => selectWeek(week[0])}>
{week.map(date => <div
className={`item date ${currentMonth !== date.getMonth() && 'extra-month'}`}
key={date.getDate()}
>
{date.getDate()}
{date.toDateString() === new Date().toDateString() && <svg xmlns="http://www.w3.org/2000/svg" className='today-indicator' viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path></svg>}
</div>)}
</button>
})}
</div>
</div>
}
}

View File

@ -1,3 +1,4 @@
import './style.scss' import './style.scss'
export {default as SingleDatePicker} from './components/SingleDatePicker' export {default as SingleDatePicker} from './components/SingleDatePicker'
export {default as SingleWeekPicker} from './components/SingleWeekPicker'

View File

@ -66,10 +66,44 @@
} }
.calendar-view-body { .calendar-view-body {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0.75rem; padding: 0.75rem;
gap: 0.125rem; gap: 0.125rem;
&.grid{
display: grid;
grid-template-columns: repeat(7, 1fr);
.item.date {
border-radius: 50%;
&.extra-month {
cursor: default;
}
&.active {
background: var(--datenel-accent-color);
color: var(--datenel-reversed-color);
}
}
}
&.flex {
display: flex;
flex-direction: column;
.listitem {
padding: 0;
display: grid;
grid-template-columns: repeat(7, 1fr);
border-radius: 1rem;
.item.date {
background: none;
}
&.active {
background: var(--datenel-accent-color);
color: var(--datenel-reversed-color);
.extra-month {
opacity: 0.5;
}
}
}
}
.item { .item {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
@ -77,20 +111,12 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 0.75rem; font-size: 0.75rem;
}
.item.day-indicator {
opacity: 0.5;
}
button.item.date {
border-radius: 50%;
position: relative; position: relative;
&.extra-month { &.extra-month {
opacity: 0.3; opacity: 0.3;
cursor: default;
} }
&:hover { &.day-indicator {
background: var(--datenel-hover-color); opacity: 0.5;
} }
.today-indicator { .today-indicator {
position: absolute; position: absolute;
@ -98,11 +124,6 @@
width: 0.25rem; width: 0.25rem;
height: 0.25rem; height: 0.25rem;
} }
&.active {
background: var(--datenel-accent-color);
color: var(--datenel-reversed-color);
}
} }
} }

View File

@ -0,0 +1,18 @@
export default (date: Date): { weekYear: number, weekNum: number } => {
const tempDate = new Date(date)
tempDate.setHours(0, 0, 0, 0)
tempDate.setDate(tempDate.getDate() + 3 - (tempDate.getDay() || 7))
const firstThursday = new Date(tempDate.getFullYear(), 0, 4)
firstThursday.setDate(firstThursday.getDate() + 3 - (firstThursday.getDay() || 7))
const diffInDays = Math.floor((tempDate.getTime() - firstThursday.getTime()) / (24 * 60 * 60 * 1000))
const weekNum = Math.ceil((diffInDays + 1) / 7)
return {
weekYear: weekNum === 53 ? tempDate.getFullYear() + 1 : tempDate.getFullYear(),
weekNum
}
}

View File

@ -2,3 +2,4 @@ export { default as getCalendarDates } from './getCalendarDates'
export { default as getL10Weekday } from './getL10Weekday' export { default as getL10Weekday } from './getL10Weekday'
export { default as generateUniqueId } from './generateUniqueId' export { default as generateUniqueId } from './generateUniqueId'
export { default as applyColor } from './applyColor' export { default as applyColor } from './applyColor'
export { default as calculateWeekNum } from './calculateWeekNum'