feat: calendar screen
This commit is contained in:
1
src/pages/calendar.module.scss
Normal file
1
src/pages/calendar.module.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
211
src/pages/calendar.tsx
Normal file
211
src/pages/calendar.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { FunctionComponent, h } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
type MarkedDateType = "event" | "holiday" | "important" | string;
|
||||||
|
type MarkedDates = Record<string, MarkedDateType>;
|
||||||
|
|
||||||
|
interface BigCalendarProps {
|
||||||
|
onDateSelect?: (date: Date) => void;
|
||||||
|
markedDates?: MarkedDates;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BigCalendar: FunctionComponent<BigCalendarProps> = ({
|
||||||
|
onDateSelect,
|
||||||
|
markedDates = {},
|
||||||
|
className = "",
|
||||||
|
}: BigCalendarProps) => {
|
||||||
|
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const monthNames: string[] = [
|
||||||
|
"Январь",
|
||||||
|
"Февраль",
|
||||||
|
"Март",
|
||||||
|
"Апрель",
|
||||||
|
"Май",
|
||||||
|
"Июнь",
|
||||||
|
"Июль",
|
||||||
|
"Август",
|
||||||
|
"Сентябрь",
|
||||||
|
"Октябрь",
|
||||||
|
"Ноябрь",
|
||||||
|
"Декабрь",
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNames: string[] = ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"];
|
||||||
|
|
||||||
|
const getDaysInMonth = (year: number, month: number): number => {
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstDayOfMonth = (year: number, month: number): number => {
|
||||||
|
return new Date(year, month, 1).getDay();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevMonth = (): void => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextMonth = (): void => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateClick = (date: Date, isCurrentMonth: boolean): void => {
|
||||||
|
if (!isCurrentMonth) {
|
||||||
|
setCurrentDate(new Date(date.getFullYear(), date.getMonth(), 1));
|
||||||
|
}
|
||||||
|
setSelectedDate(date);
|
||||||
|
onDateSelect?.(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDateMarked = (date: Date): MarkedDateType | undefined => {
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
return markedDates[dateStr];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDays = (): h.JSX.Element[] => {
|
||||||
|
const year: number = currentDate.getFullYear();
|
||||||
|
const month: number = currentDate.getMonth();
|
||||||
|
|
||||||
|
const daysInMonth: number = getDaysInMonth(year, month);
|
||||||
|
const firstDayOfMonth: number = getFirstDayOfMonth(year, month);
|
||||||
|
const lastDayOfMonth: number = new Date(year, month, daysInMonth).getDay();
|
||||||
|
|
||||||
|
const days: h.JSX.Element[] = [];
|
||||||
|
|
||||||
|
// Дни предыдущего месяца
|
||||||
|
const prevMonthDays = getDaysInMonth(year, month - 1);
|
||||||
|
for (let i = firstDayOfMonth - 1; i >= 0; i--) {
|
||||||
|
const day = prevMonthDays - i;
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`prev-${day}`}
|
||||||
|
className={`relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70 ${isSelected ? "bg-gray-200" : ""} `}
|
||||||
|
onClick={() => handleDateClick(date, false)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={`mb-1 truncate rounded p-1 text-xs ${markType === "event" ? "bg-green-100 text-green-800" : ""} ${markType === "holiday" ? "bg-red-100 text-red-800" : ""} ${markType === "important" ? "bg-yellow-100 text-yellow-800" : ""} `}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дни текущего месяца
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const date = new Date(year, month, day);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
const isToday = new Date().toISOString().split("T")[0] === dateStr;
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`current-${day}`}
|
||||||
|
className={`relative flex h-24 flex-col border border-gray-200 p-2 ${isSelected ? "border-blue-400 bg-blue-100" : ""} ${isToday ? "border-yellow-400" : ""} cursor-pointer hover:bg-gray-50`}
|
||||||
|
onClick={() => handleDateClick(date, true)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex h-8 w-8 items-center justify-center self-end rounded-full ${isSelected ? "bg-blue-600 text-white" : ""} ${isToday && !isSelected ? "bg-yellow-100 text-yellow-800" : ""} `}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={`mb-1 truncate rounded p-1 text-xs ${markType === "event" ? "bg-green-100 text-green-800" : ""} ${markType === "holiday" ? "bg-red-100 text-red-800" : ""} ${markType === "important" ? "bg-yellow-100 text-yellow-800" : ""} `}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дни следующего месяца
|
||||||
|
const daysToAdd = 6 - lastDayOfMonth;
|
||||||
|
for (let day = 1; day <= daysToAdd; day++) {
|
||||||
|
const date = new Date(year, month + 1, day);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`next-${day}`}
|
||||||
|
className={`relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70 ${isSelected ? "bg-gray-200" : ""} `}
|
||||||
|
onClick={() => handleDateClick(date, false)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={`mb-1 truncate rounded p-1 text-xs ${markType === "event" ? "bg-green-100 text-green-800" : ""} ${markType === "holiday" ? "bg-red-100 text-red-800" : ""} ${markType === "important" ? "bg-yellow-100 text-yellow-800" : ""} `}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col ${className}`}>
|
||||||
|
<div className="mb-4 flex items-center justify-between px-2">
|
||||||
|
<button
|
||||||
|
onClick={handlePrevMonth}
|
||||||
|
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
|
||||||
|
aria-label="Previous month"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2 className="text-xl font-bold text-gray-800">
|
||||||
|
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleNextMonth}
|
||||||
|
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
|
||||||
|
aria-label="Next month"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2 grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
|
||||||
|
{dayNames.map((day) => (
|
||||||
|
<div key={day} className="flex h-10 items-center justify-center">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid flex-1 grid-cols-7 gap-1">{renderDays()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BigCalendar;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Menu from "@/components/menu";
|
import Menu from "@/components/menu";
|
||||||
import { FunctionComponent } from "preact";
|
import { FunctionComponent } from "preact";
|
||||||
import { Route, Router, useLocation } from "preact-iso";
|
import { lazy, Route, Router, useLocation } from "preact-iso";
|
||||||
|
|
||||||
const ProfilePage: FunctionComponent = () => {
|
const ProfilePage: FunctionComponent = () => {
|
||||||
const { route } = useLocation();
|
const { route } = useLocation();
|
||||||
@@ -82,7 +82,7 @@ const ProfilePage: FunctionComponent = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route path="/tasks" component={() => <p class="text-2xl">Tasks</p>} />
|
<Route path="/tasks" component={() => <p class="text-2xl">Tasks</p>} />
|
||||||
<Route path="/calendar" component={() => <p class="text-2xl">Calendar</p>} />
|
<Route path="/calendar" component={lazy(() => import("./calendar"))} />
|
||||||
<Route
|
<Route
|
||||||
default
|
default
|
||||||
component={() => {
|
component={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user