diff --git a/.gitignore b/.gitignore index feb5266a..2d79e064 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ backend/yarn.lock node_modules/.yarn-integrity .vscode/ docs/ +backend/ATT75011.env \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 96367f2f..72bab5db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,21 +9,27 @@ "@mui/material": "^5.8.0", "@mui/styles": "^5.8.0", "@mui/x-date-pickers": "^5.0.0-alpha.3", + "bootstrap": "^5.3.3", "cross-env": "^7.0.3", "dagre": "^0.8.5", "date-fns": "^2.23.0", "lodash": "^4.17.20", + "lucide-react": "^0.471.1", "material-ui-phone-number": "^3.0.0", "notistack": "^2.0.5", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.0", "react-bootstrap": "^1.6.1", + "react-date-picker": "^11.0.0", + "react-datepicker": "^7.3.0", "react-dom": "^18.2.0", "react-redux": "^7.2.1", "react-router-dom": "^6.3.0", "react-star-rating-component": "^1.4.1", "reactflow": "^11.8.3", - "redux": "^4.0.5" + "reactstrap": "^9.2.2", + "redux": "^4.0.5", + "styled-components": "^6.1.12" }, "devDependencies": { "react-scripts": "5.0.1", diff --git a/frontend/src/components/calendar/calendar.js b/frontend/src/components/calendar/calendar.js new file mode 100644 index 00000000..4eb53576 --- /dev/null +++ b/frontend/src/components/calendar/calendar.js @@ -0,0 +1,337 @@ +import React, { useState, useEffect, Fragment, useRef } from "react"; +import PropTypes from "prop-types"; +import * as Styled from "../../styles/calendarStyles"; +import calendar, { + isDate, + isSameDay, + isSameMonth, + getDateISO, + getNextMonth, + getPreviousMonth, + WEEK_DAYS, + CALENDAR_MONTHS, +} from "../../helpers/calendarHelper"; +import "bootstrap/dist/css/bootstrap.min.css"; +import { fetchMultipleGeneric, deleteSingleGeneric, updateSingleGeneric } from "../../api/genericDataApi"; +import { Link } from "../shared"; +import AppointmentModal from "./calendarModal"; + +export default function Calendar({ date, onDateChanged }) { + const [dateState, setDateState] = useState({ + current: new Date(), + month: new Date().getMonth() + 1, + year: new Date().getFullYear(), + }); + + const [today] = useState(new Date()); + const [appointments, setAppointments] = useState([]); + const [selectedDateAppointments, setSelectedDateAppointments] = useState([]); + const [editingAppointment, setEditingAppointment] = useState(null); + const [selectedAppointment, setSelectedAppointment] = useState(null); + const [modalPosition, setModalPosition] = useState({ x: 0, y: 0 }); + const [expandedDate, setExpandedDate] = useState(null); + + useEffect(() => { + addDateToState(date); + fetchAppointments(); + }, [date]); + + const addDateToState = (date) => { + const isDateObject = isDate(date); + const _date = isDateObject ? date : new Date(); + setDateState({ + current: isDateObject ? date : null, + month: _date.getMonth() + 1, + year: _date.getFullYear(), + }); + }; + + const fetchAppointments = async () => { + try { + const response = await fetchMultipleGeneric('appointment'); + console.log("Raw appointment data:", response.data); + + const appointmentData = response.data.map(appointment => { + let datetime, dateType; + const processedAppointment = { ...appointment, characteristicOccurrences: {} }; + + if (appointment.characteristicOccurrences) { + for (const occ of appointment.characteristicOccurrences) { + processedAppointment.characteristicOccurrences[occ.occurrenceOf?.name] = occ.dataStringValue || occ.dataDateValue || occ.objectValue; + + if (occ.occurrenceOf?.name === 'Date and Time') { + datetime = occ.dataDateValue; + dateType = 'DateTime'; + } else if (occ.occurrenceOf?.name === 'Date') { + datetime = occ.dataDateValue; + dateType = 'Date'; + } + } + } + + const parsedDate = parseDate(datetime); + console.log(`Parsed date for appointment ${appointment._id}:`, parsedDate); + + return { + ...processedAppointment, + date: parsedDate, + dateType + }; + }); + + console.log("Processed appointment data:", appointmentData); + + setAppointments(appointmentData.filter(app => app.date !== null)); + } catch (error) { + console.error("Error fetching appointments:", error); + } + }; + + const handleEdit = (appointment) => { + setEditingAppointment(appointment); + }; + + const handleSave = async (updatedAppointment) => { + try { + await updateSingleGeneric('appointment', updatedAppointment._id, updatedAppointment); + setEditingAppointment(null); + fetchAppointments(); + } catch (error) { + console.error("Error updating appointment:", error); + } + }; + + const handleDelete = async (appointmentId) => { + if (window.confirm("Are you sure you want to delete this appointment?")) { + try { + await deleteSingleGeneric('appointment', appointmentId); + fetchAppointments(); + } catch (error) { + console.error("Error deleting appointment:", error); + } + } + }; + + const handleAppointmentClick = (e, appointment) => { + e.stopPropagation(); // Prevent cell click handler from firing + const rect = e.currentTarget.getBoundingClientRect(); + setModalPosition({ x: rect.right, y: rect.top }); + setSelectedAppointment(appointment); + }; + + const handleMoreClick = (e, date) => { + e.stopPropagation(); // Prevent cell click handler + setExpandedDate(expandedDate === getDateISO(date) ? null : getDateISO(date)); + }; + + const parseDate = (dateString) => { + if (!dateString) return null; + + const parsedDate = new Date(dateString); + + if (isNaN(parsedDate.getTime())) { + console.error(`Failed to parse date: ${dateString}`); + return null; + } + + return parsedDate; + }; + + const getCalendarDates = () => { + const { current, month, year } = dateState; + const calendarMonth = month || (current ? current.getMonth() + 1 : today.getMonth() + 1); + const calendarYear = year || (current ? current.getFullYear() : today.getFullYear()); + return calendar(calendarMonth, calendarYear); + }; + + const gotoDate = (date) => { + const { current } = dateState; + if (!(current && isSameDay(date, current))) { + setDateState(prevState => ({ + ...prevState, + current: date, + month: date.getMonth() + 1, + year: date.getFullYear() + })); + if (typeof onDateChanged === 'function') { + onDateChanged(date); + } + + // const selectedAppointments = appointments.filter(app => isSameDay(app.date, date)); + // setSelectedDateAppointments(selectedAppointments); + } + }; + + const gotoPreviousMonth = () => { + const { month, year } = dateState; + const previousMonth = getPreviousMonth(month, year); + setDateState(prevState => ({ + ...prevState, + month: previousMonth.month, + year: previousMonth.year + })); + }; + + const gotoNextMonth = () => { + const { month, year } = dateState; + const nextMonth = getNextMonth(month, year); + setDateState(prevState => ({ + ...prevState, + month: nextMonth.month, + year: nextMonth.year + })); + }; + + const renderMonthAndYear = () => { + const { month, year } = dateState; + const monthname = CALENDAR_MONTHS[Math.max(0, Math.min(month - 1, 11))]; + return ( + + + + {monthname} {year} + + + + ); + }; + + const renderDayLabel = (day, index) => { + const daylabel = WEEK_DAYS[day].toUpperCase(); + return ( + + {daylabel} + + ); + }; + + const renderCalendarDate = (date, index) => { + const _date = new Date(date.join("-")); + const { current, month, year } = dateState; + + const isToday = isSameDay(_date, today); + const isCurrent = current && isSameDay(_date, current); + const inMonth = month && year && isSameMonth(_date, new Date([year, month, 1].join("-"))); + const onClick = () => gotoDate(_date); + + const dateAppointments = appointments.filter(app => isSameDay(app.date, _date)); + const maxDisplayAppointments = 3; + const isExpanded = expandedDate === getDateISO(_date); + + const getAppointmentName = (appointment) => { + const characteristics = appointment.characteristicOccurrences; + return characteristics['Appointment Name'] || 'Untitled Appointment'; + }; + + return ( + + + {_date.getDate()} + + + + {(isExpanded ? dateAppointments : dateAppointments.slice(0, maxDisplayAppointments)).map((app) => ( + handleAppointmentClick(e, app)} + > + {getAppointmentName(app)} + + ))} + + {!isExpanded && dateAppointments.length > maxDisplayAppointments && ( + handleMoreClick(e, _date)} + > + {dateAppointments.length - maxDisplayAppointments} more + + )} + + + ); + }; + + const renderAppointmentDetails = (appointment) => { + if (editingAppointment && editingAppointment._id === appointment._id) { + return ( +
+

Editing Appointment: {appointment._id}

+ {/* Add form fields for editing appointment details */} + + +
+ ); + } + return ( +
+

Appointment ID: {appointment._id}

+

Date: {appointment.date.toLocaleDateString()}

+

Time: {appointment.dateType === 'DateTime' ? appointment.date.toLocaleTimeString() : 'N/A'}

+ {/*
Characteristics:
*/} + +
+ Edit + +
+
+
+ ); + }; + + return ( + + {renderMonthAndYear()} + + + + {Object.keys(WEEK_DAYS).map(renderDayLabel)} + + + {getCalendarDates().map(renderCalendarDate)} + + + + // Appointment details + {/* {selectedDateAppointments.length > 0 && ( + +

Appointments for {dateState.current.toDateString()}

+ {selectedDateAppointments.map(renderAppointmentDetails)} +
+ )} */} + + {selectedAppointment && ( + setSelectedAppointment(null)} + onEdit={() => handleEdit(selectedAppointment)} + onDelete={() => handleDelete(selectedAppointment._id)} + />)} + +
+ ); +} + +Calendar.propTypes = { + date: PropTypes.instanceOf(Date), + onDateChanged: PropTypes.func, +}; + +Calendar.defaultProps = { + date: new Date(), + onDateChanged: () => {}, +}; \ No newline at end of file diff --git a/frontend/src/components/calendar/calendarModal.js b/frontend/src/components/calendar/calendarModal.js new file mode 100644 index 00000000..434388e0 --- /dev/null +++ b/frontend/src/components/calendar/calendarModal.js @@ -0,0 +1,83 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Edit2, Trash2, X } from 'lucide-react'; +import * as Styled from '../../styles/calendarModalStyles'; +import { Link } from "../shared"; +import GoogleCalendarSync from './googleCalendarSync'; + +const AppointmentDetails = ({ appointment, position, onClose, onEdit, onDelete }) => { + const popupRef = useRef(null); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + + useEffect(() => { + if (popupRef.current) { + const popupRect = popupRef.current.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let top = position.y; + let left = position.x; + + // Adjust if popup would go off screen + if (left + popupRect.width > viewportWidth) { + left = position.x - popupRect.width; + } + if (top + popupRect.height > viewportHeight) { + top = position.y - popupRect.height; + } + + setPopupPosition({ top, left }); + } + }, [position]); + + const formatDateTime = (dateTime) => { + const date = new Date(dateTime); + return { + date: date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }), + time: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) + }; + }; + + const { date, time } = formatDateTime(appointment.date); + + const handleClickOutside = (e) => { + if (popupRef.current && !popupRef.current.contains(e.target)) { + onClose(); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( + + + + + + + + + + + + + {appointment.characteristicOccurrences['Appointment Name']} + + + + {date} • {time} + + + + + + + ); +}; + +export default AppointmentDetails; \ No newline at end of file diff --git a/frontend/src/components/calendar/googleCalendarSync.js b/frontend/src/components/calendar/googleCalendarSync.js new file mode 100644 index 00000000..c2f88f6e --- /dev/null +++ b/frontend/src/components/calendar/googleCalendarSync.js @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { googleCalendarService } from '../../helpers/googleCalendarHelper'; + +const GoogleCalendarSync = ({ appointment }) => { + const [syncing, setSyncing] = useState(false); + const [error, setError] = useState(null); + + const handleSync = async () => { + try { + setSyncing(true); + setError(null); + + if (!googleCalendarService.isAuthenticated) { + await googleCalendarService.authenticate(); + } + + await googleCalendarService.syncAppointment(appointment); + alert('Successfully synced with Google Calendar!'); + } catch (err) { + setError(err.message); + } finally { + setSyncing(false); + } + }; + + return ( + <> + + {error &&
{error}
} + + ); +}; + +export default GoogleCalendarSync; \ No newline at end of file diff --git a/frontend/src/components/layouts/TopNavbar.js b/frontend/src/components/layouts/TopNavbar.js index 2f2c826b..6fea73f5 100644 --- a/frontend/src/components/layouts/TopNavbar.js +++ b/frontend/src/components/layouts/TopNavbar.js @@ -237,6 +237,13 @@ function TopNavBar() { Persons + + + + + Calendar + + ) : null} diff --git a/frontend/src/helpers/calendarHelper.js b/frontend/src/helpers/calendarHelper.js new file mode 100644 index 00000000..c71c20ef --- /dev/null +++ b/frontend/src/helpers/calendarHelper.js @@ -0,0 +1,163 @@ +// Render the custom calendar, prviding functionality for date selection +import "bootstrap/dist/css/bootstrap.min.css"; + +// (int) The current year +export const THIS_YEAR = +(new Date().getFullYear()); + +// (int) The current month starting from 1 - 12 +// 1 => January, 12 => December +export const THIS_MONTH = +(new Date().getMonth()) + 1; + +// Week days names and shortnames +export const WEEK_DAYS = { + Sunday: "Sun", + Monday: "Mon", + Tuesday: "Tue", + Wednesday: "Wed", + Thursday: "Thu", + Friday: "Fri", + Saturday: "Sat" +} + +// Calendar months names and short names +export const CALENDAR_MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; + +// Weeks displayed on calendar +export const CALENDAR_WEEKS = 6; + +// Pads a string value with leading zeroes(0) until length is reached +// For example: zeroPad(5, 2) => "05" +export const zeroPad = (value, length) => { + return `${value}`.padStart(length, '0'); +} + +// (int) Number days in a month for a given year from 28 - 31 +export const getMonthDays = (month = THIS_MONTH, year = THIS_YEAR) => { + const months30 = [4, 6, 9, 11]; + const leapYear = year % 4 === 0; + return month === 2 + ? leapYear + ? 29 + : 28 + : months30.includes(month) + ? 30 + : 31; +} + +// (int) First day of the month for a given year from 1 - 7 +// 1 => Sunday, 7 => Saturday +export const getMonthFirstDay = (month = THIS_MONTH, year = THIS_YEAR) => { + return +(new Date(`${year}-${zeroPad(month, 2)}-01`).getDay()) + 1; +} + +// (bool) Checks if a value is a date - this is just a simple check +export const isDate = date => { + const isDate = Object.prototype.toString.call(date) === '[object Date]'; + const isValidDate = date && !Number.isNaN(date.valueOf()); + + return isDate && isValidDate; + } + + // (bool) Checks if two date values are of the same month and year + export const isSameMonth = (date, basedate = new Date()) => { + if (!(isDate(date) && isDate(basedate))) return false; + const basedateMonth = +(basedate.getMonth()) + 1; + const basedateYear = basedate.getFullYear(); + const dateMonth = +(date.getMonth()) + 1; + const dateYear = date.getFullYear(); + return (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); + } + + // (bool) Checks if two date values are the same day + export const isSameDay = (date, basedate = new Date()) => { + if (!(isDate(date) && isDate(basedate))) return false; + const basedateDate = basedate.getDate(); + const basedateMonth = +(basedate.getMonth()) + 1; + const basedateYear = basedate.getFullYear(); + const dateDate = date.getDate(); + const dateMonth = +(date.getMonth()) + 1; + const dateYear = date.getFullYear(); + return (+basedateDate === +dateDate) && (+basedateMonth === +dateMonth) && (+basedateYear === +dateYear); + } + + // (string) Formats the given date as YYYY-MM-DD + // Months and Days are zero padded + export const getDateISO = (date = new Date) => { + if (!isDate(date)) return null; + return [ + date.getFullYear(), + zeroPad(+date.getMonth() + 1, 2), + zeroPad(+date.getDate(), 2) + ].join('-'); + } + + // ({month, year}) Gets the month and year before the given month and year + // For example: getPreviousMonth(1, 2000) => {month: 12, year: 1999} + // while: getPreviousMonth(12, 2000) => {month: 11, year: 2000} + export const getPreviousMonth = (month, year) => { + const prevMonth = (month > 1) ? month - 1 : 12; + const prevMonthYear = (month > 1) ? year : year - 1; + return { month: prevMonth, year: prevMonthYear }; + } + + // ({month, year}) Gets the month and year after the given month and year + // For example: getNextMonth(1, 2000) => {month: 2, year: 2000} + // while: getNextMonth(12, 2000) => {month: 1, year: 2001} + export const getNextMonth = (month, year) => { + const nextMonth = (month < 12) ? month + 1 : 1; + const nextMonthYear = (month < 12) ? year : year + 1; + return { month: nextMonth, year: nextMonthYear }; + } + + // Calendar builder for a month in the specified year +// Returns an array of the calendar dates. +// Each calendar date is represented as an array => [YYYY, MM, DD] +export default (month = THIS_MONTH, year = THIS_YEAR) => { + // Get number of days in the month and the month's first day + const monthDays = getMonthDays(month, year); + const monthFirstDay = getMonthFirstDay(month, year); + // Get number of days to be displayed from previous and next months + // These ensure a total of 42 days (6 weeks) displayed on the calendar + + const daysFromPrevMonth = monthFirstDay - 1; + const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays); + // Get the previous and next months and years + + const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(month, year); + const { month: nextMonth, year: nextMonthYear } = getNextMonth(month, year); + // Get number of days in previous month + const prevMonthDays = getMonthDays(prevMonth, prevMonthYear); + // Builds dates to be displayed from previous month + + const prevMonthDates = [...new Array(daysFromPrevMonth)].map((n, index) => { + const day = index + 1 + (prevMonthDays - daysFromPrevMonth); + return [ prevMonthYear, zeroPad(prevMonth, 2), zeroPad(day, 2) ]; + }); + // Builds dates to be displayed from current month + + const thisMonthDates = [...new Array(monthDays)].map((n, index) => { + const day = index + 1; + return [year, zeroPad(month, 2), zeroPad(day, 2)]; + }); + // Builds dates to be displayed from next month + + const nextMonthDates = [...new Array(daysFromNextMonth)].map((n, index) => { + const day = index + 1; + return [nextMonthYear, zeroPad(nextMonth, 2), zeroPad(day, 2)]; + }); + // Combines all dates from previous, current and next months + return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ]; + } \ No newline at end of file diff --git a/frontend/src/helpers/googleCalendarHelper.js b/frontend/src/helpers/googleCalendarHelper.js new file mode 100644 index 00000000..21daed1c --- /dev/null +++ b/frontend/src/helpers/googleCalendarHelper.js @@ -0,0 +1,106 @@ +const GOOGLE_API_KEY = 'YOUR_GOOGLE_API_KEY'; +const GOOGLE_CLIENT_ID = 'YOUR_CLIENT_ID'; +const SCOPES = 'https://www.googleapis.com/auth/calendar'; + +export class GoogleCalendarService { + constructor() { + this.isAuthenticated = false; + this.tokenClient = null; + } + + async initialize() { + // Load the Google API client library + await this.loadGoogleApi(); + + this.tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: GOOGLE_CLIENT_ID, + scope: SCOPES, + callback: this.handleAuthResponse, + }); + } + + async loadGoogleApi() { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://apis.google.com/js/api.js'; + script.onload = () => { + gapi.load('client', async () => { + try { + await gapi.client.init({ + apiKey: GOOGLE_API_KEY, + discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'], + }); + resolve(); + } catch (error) { + reject(error); + } + }); + }; + script.onerror = reject; + document.body.appendChild(script); + }); + } + + handleAuthResponse = (response) => { + if (response.error) { + throw new Error('Authentication failed'); + } + this.isAuthenticated = true; + }; + + async authenticate() { + if (!this.tokenClient) { + await this.initialize(); + } + this.tokenClient.requestAccessToken(); + } + + async syncAppointment(appointment) { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const event = { + summary: appointment.characteristicOccurrences['Appointment Name'], + start: { + dateTime: appointment.date.toISOString(), + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + end: { + dateTime: new Date(appointment.date.getTime() + 60 * 60 * 1000).toISOString(), // Default 1 hour + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + description: `First Name: ${appointment.characteristicOccurrences['First Name']}\nLast Name: ${appointment.characteristicOccurrences['Last Name']}`, + }; + + try { + const response = await gapi.client.calendar.events.insert({ + calendarId: 'primary', + resource: event, + }); + return response.result; + } catch (error) { + console.error('Error syncing with Google Calendar:', error); + throw error; + } + } + + async syncMultipleAppointments(appointments) { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const results = []; + for (const appointment of appointments) { + try { + const result = await this.syncAppointment(appointment); + results.push({ success: true, appointment, result }); + } catch (error) { + results.push({ success: false, appointment, error }); + } + } + return results; + } +} + +export const googleCalendarService = new GoogleCalendarService(); \ No newline at end of file diff --git a/frontend/src/routes.js b/frontend/src/routes.js index f2035445..66882ed3 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -91,6 +91,7 @@ import VisualizeClientAssessment from "./components/clientAssessment/VisualizeCl import VisualizeReferral from "./components/referrals/VisualizeReferral"; import Matching from "./components/clients/Matching"; import VisualizeNotification from './components/notifications/visualizeNotification'; +import Calendar from "./components/calendar/calendar"; const routes = ( @@ -239,6 +240,8 @@ const routes = ( }/> }/> + }/> + }/> }/> diff --git a/frontend/src/styles/calendarModalStyles.js b/frontend/src/styles/calendarModalStyles.js new file mode 100644 index 00000000..aa8288d4 --- /dev/null +++ b/frontend/src/styles/calendarModalStyles.js @@ -0,0 +1,56 @@ +import styled from "styled-components"; + +export const Popup = styled.div` + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + padding: 16px; + min-width: 280px; + max-width: 400px; + z-index: 1000; + font-family: 'Google Sans', Roboto, Arial, sans-serif; +`; + +export const PopupHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +`; + +export const HeaderActions = styled.div` + display: flex; + gap: 8px; +`; + +export const ActionIcon = styled.button` + border: none; + background: none; + padding: 8px; + cursor: pointer; + border-radius: 50%; + color: #5f6368; + + &:hover { + background: #f1f3f4; + } +`; + +export const Title = styled.div` + font-size: 22px; + font-weight: 400; + color: #3c4043; + margin-bottom: 8px; +`; + +export const DateTime = styled.div` + font-size: 14px; + color: #3c4043; +`; + +export const SyncContainer = styled.div` + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #dadce0; +`; \ No newline at end of file diff --git a/frontend/src/styles/calendarStyles.js b/frontend/src/styles/calendarStyles.js new file mode 100644 index 00000000..76bd6281 --- /dev/null +++ b/frontend/src/styles/calendarStyles.js @@ -0,0 +1,175 @@ +import styled from "styled-components"; +import "bootstrap/dist/css/bootstrap.min.css"; + +export const CalendarContainer = styled.div` + font-family: 'Google Sans', Roboto, Arial, sans-serif; + border: 1px solid #dadce0; + border-radius: 8px; + overflow: hidden; + background: white; + width: 100%; +`; + +export const CalendarHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 20px; + border-bottom: 1px solid #dadce0; +`; + +export const CalendarMonth = styled.div` + font-size: 22px; + font-weight: 400; + color: #3c4043; + letter-spacing: 0; +`; + +export const CalendarGrid = styled.div` + display: grid; + grid-template-columns: repeat(7, 1fr); + background: white; +`; + +export const CalendarDay = styled.div` + color: #70757a; + font-size: 11px; + font-weight: 500; + letter-spacing: 0; + text-align: center; + padding: 15px 0; + border-bottom: 1px solid #dadce0; +`; + +export const CalendarCell = styled.div` + aspect-ratio: ${props => props.expanded ? 'auto' : '1/1'}; + border-right: 1px solid #dadce0; + border-bottom: 1px solid #dadce0; + padding: 8px; + position: relative; + background: white; + color: ${props => props.inMonth ? '#3c4043' : '#70757a'}; + + &:last-child { + border-right: none; + } + + &:hover { + background-color: #f8f9fa; + } +`; + +export const DateNumber = styled.div` +font-size: 12px; +margin-bottom: 4px; +text-align: right; +color: ${props => props.isToday ? '#1a73e8' : 'inherit'}; +font-weight: ${props => props.isToday ? '500' : '400'}; + +${props => props.isToday && ` + background-color: #1a73e8; + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; +`} +`; + +export const AppointmentPreview = styled.div` + display: flex; + align-items: center; + font-size: 11px; + padding: 0 4px; + height: 18px; + line-height: 18px; + color: #3c4043; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #1a73e8; + margin-right: 4px; + flex-shrink: 0; + } + + &:hover { + background: #f1f3f4; + border-radius: 3px; + } +`; + +export const MoreAppointments = styled.div` + font-size: 11px; + color: #70757a; + padding: 4px 4px 4px 12px; // Extra left padding to align with appointments + cursor: pointer; + border-radius: 3px; + + &:hover { + color: #1a73e8; + background: #f1f3f4; + } +`; + +export const Arrow = styled.button` + border: none; + background: transparent; + padding: 8px; + cursor: pointer; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #70757a; + + &:hover { + background: #f8f9fa; + } +`; + +export const ArrowLeft = styled(Arrow)` + &::before { + content: '‹'; + font-size: 24px; + } +`; + +export const ArrowRight = styled(Arrow)` + &::before { + content: '›'; + font-size: 24px; + } +`; + +export const AppointmentList = styled.div` + display: flex; + flex-direction: column; + gap: 2px; + max-height: ${props => props.expanded ? '200px' : '100%'}; + overflow-y: auto; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; + } +`; \ No newline at end of file