diff --git a/CLAUDE.md b/CLAUDE.md
index 7f8b520..1998451 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -295,7 +295,8 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
**Frontend:**
- Tab navigation (Chat, Calendar) implemented with basic UI
- Calendar screen fully functional:
- - Month navigation with grid display
+ - Month navigation with grid display and Ionicons (chevron-back/forward)
+ - MonthSelector dropdown with infinite scroll (dynamically loads months)
- Events loaded from API via EventService.getByDateRange()
- Orange dot indicator for days with events
- Tap-to-open modal overlay showing EventCards for selected day
@@ -309,7 +310,7 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
-- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator
+- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator, secondaryBg
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[]
- `ChatStore`: Zustand store with addMessage(), updateMessage(), clearMessages() - persists messages across tab switches
- Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
diff --git a/apps/client/src/Themes.tsx b/apps/client/src/Themes.tsx
index 36497a8..b36892d 100644
--- a/apps/client/src/Themes.tsx
+++ b/apps/client/src/Themes.tsx
@@ -2,6 +2,7 @@ type Theme = {
chatBot: string;
primeFg: string;
primeBg: string;
+ secondaryBg: string;
messageBorderBg: string;
placeholderBg: string;
calenderBg: string;
@@ -20,6 +21,7 @@ const defaultLight: Theme = {
chatBot: "#DE6C20",
primeFg: "#3B3329",
primeBg: "#FFEEDE",
+ secondaryBg: "#FFFFFF",
messageBorderBg: "#FFFFFF",
placeholderBg: "#D9D9D9",
calenderBg: "#FBD5B2",
diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx
index 3208ef1..9d016c4 100644
--- a/apps/client/src/app/(tabs)/calendar.tsx
+++ b/apps/client/src/app/(tabs)/calendar.tsx
@@ -18,13 +18,49 @@ import React, {
useState,
} from "react";
import { useFocusEffect } from "expo-router";
+import { Ionicons } from "@expo/vector-icons";
import currentTheme from "../../Themes";
import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list";
import { EventService } from "../../services";
import { useEventsStore } from "../../stores";
-// TODO: month selection dropdown menu
+// MonthSelector types and helpers
+type MonthItem = {
+ id: string; // Format: "YYYY-MM"
+ year: number;
+ monthIndex: number; // 0-11
+ label: string; // e.g. "January 2024"
+};
+
+const generateMonths = (
+ centerYear: number,
+ centerMonth: number,
+ range: number,
+): MonthItem[] => {
+ const months: MonthItem[] = [];
+ for (let offset = -range; offset <= range; offset++) {
+ let year = centerYear;
+ let month = centerMonth + offset;
+
+ while (month < 0) {
+ month += 12;
+ year--;
+ }
+ while (month > 11) {
+ month -= 12;
+ year++;
+ }
+
+ months.push({
+ id: `${year}-${String(month + 1).padStart(2, "0")}`,
+ year,
+ monthIndex: month,
+ label: `${MONTHS[month]} ${year}`,
+ });
+ }
+ return months;
+};
const Calendar = () => {
const [monthIndex, setMonthIndex] = useState(new Date().getMonth());
@@ -149,6 +185,8 @@ const Calendar = () => {
changeMonth={changeMonth}
monthIndex={monthIndex}
currentYear={currentYear}
+ setMonthIndex={setMonthIndex}
+ setYear={setCurrentYear}
/>
void;
position: { top: number; left: number; width: number };
+ currentYear: number;
+ currentMonthIndex: number;
+ onSelectMonth: (year: number, monthIndex: number) => void;
};
const MonthSelector = ({
modalVisible,
onClose,
position,
+ currentYear,
+ currentMonthIndex,
+ onSelectMonth,
}: MonthSelectorProps) => {
const heightAnim = useRef(new Animated.Value(0)).current;
- type ItemType = { id: string; text: string };
- const listRef = useRef>>(null);
- const [monthSelectorData, setMonthSelectorData] = useState(() => {
- const initial = [];
- for (let i = 1; i <= 10; i++) {
- initial.push({ id: i.toString(), text: `number ${i}` });
- }
- return initial;
- });
+ const listRef = useRef>>(null);
+ const INITIAL_RANGE = 12; // 12 months before and after current
- const appendToTestData = (
- startIndex: number,
- numberOfEntries: number,
- appendToStart: boolean,
- ) => {
- // create new data
- const newData = [];
- for (let i = 0; i < numberOfEntries; i++) {
- const newIndex = startIndex + i + 1;
- const newEntry = {
- id: newIndex + "",
- text: `number ${newIndex}`,
- };
- if (appendToStart) {
- newData.unshift(newEntry);
- } else {
- newData.push(newEntry);
+ const [monthSelectorData, setMonthSelectorData] = useState(() =>
+ generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE),
+ );
+
+ // Reset data when current month changes
+ useEffect(() => {
+ setMonthSelectorData(
+ generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE),
+ );
+ }, [currentYear, currentMonthIndex]);
+
+ const appendMonths = (direction: "start" | "end", count: number) => {
+ setMonthSelectorData((prevData) => {
+ if (prevData.length === 0) return prevData;
+
+ const newMonths: MonthItem[] = [];
+ const referenceMonth =
+ direction === "start" ? prevData[0] : prevData[prevData.length - 1];
+
+ for (let i = 1; i <= count; i++) {
+ const offset = direction === "start" ? -i : i;
+ let year = referenceMonth.year;
+ let month = referenceMonth.monthIndex + offset;
+
+ while (month < 0) {
+ month += 12;
+ year--;
+ }
+ while (month > 11) {
+ month -= 12;
+ year++;
+ }
+
+ const newMonth: MonthItem = {
+ id: `${year}-${String(month + 1).padStart(2, "0")}`,
+ year,
+ monthIndex: month,
+ label: `${MONTHS[month]} ${year}`,
+ };
+
+ if (direction === "start") {
+ newMonths.unshift(newMonth);
+ } else {
+ newMonths.push(newMonth);
+ }
}
- }
- // add new data
- if (appendToStart) {
- setMonthSelectorData([...newData, ...monthSelectorData]);
- } else {
- setMonthSelectorData([...monthSelectorData, ...newData]);
- }
+
+ return direction === "start"
+ ? [...newMonths, ...prevData]
+ : [...prevData, ...newMonths];
+ });
};
useEffect(() => {
@@ -321,15 +384,29 @@ const MonthSelector = ({
useNativeDriver: false,
}).start();
} else {
- // reset on close
heightAnim.setValue(0);
}
- }, [modalVisible]);
+ }, [modalVisible, heightAnim]);
- const renderItem = ({ item }: { item: ItemType }) => (
-
-
- {item.text}
+ const renderItem = ({ item }: { item: MonthItem }) => (
+ {
+ onSelectMonth(item.year, item.monthIndex);
+ onClose();
+ }}
+ >
+
+
+ {item.label}
+
);
@@ -341,30 +418,31 @@ const MonthSelector = ({
animationType="none"
onRequestClose={onClose}
>
-
+
item.id}
data={monthSelectorData}
- initialScrollIndex={5}
+ initialScrollIndex={INITIAL_RANGE}
onEndReachedThreshold={0.5}
- onEndReached={() =>
- appendToTestData(monthSelectorData.length, 10, false)
- }
+ onEndReached={() => appendMonths("end", 12)}
onStartReachedThreshold={0.5}
- onStartReached={() =>
- appendToTestData(monthSelectorData.length, 10, true)
- }
+ onStartReached={() => appendMonths("start", 12)}
renderItem={renderItem}
/>
@@ -377,6 +455,8 @@ type CalendarHeaderProps = {
changeMonth: (delta: number) => void;
monthIndex: number;
currentYear: number;
+ setMonthIndex: (index: number) => void;
+ setYear: (year: number) => void;
};
const CalendarHeader = (props: CalendarHeaderProps) => {
@@ -400,54 +480,82 @@ const CalendarHeader = (props: CalendarHeaderProps) => {
return (
-
+
-
+
{MONTHS[props.monthIndex]} {props.currentYear}
- v
+
setModalVisible(false)}
position={dropdownPosition}
+ currentYear={props.currentYear}
+ currentMonthIndex={props.monthIndex}
+ onSelectMonth={(year, month) => {
+ props.setYear(year);
+ props.setMonthIndex(month);
+ }}
/>
- "} />
+
);
};
type ChangeMonthButtonProps = {
onPress: () => void;
- title: string;
+ icon: "chevron-back" | "chevron-forward";
};
const ChangeMonthButton = (props: ChangeMonthButtonProps) => (
- {props.title}
+
);