diff --git a/src/components/TimeSelect/TimeSelectPage.tsx b/src/components/TimeSelect/TimeSelectPage.tsx
index adb0e8ab..dfa4fec6 100644
--- a/src/components/TimeSelect/TimeSelectPage.tsx
+++ b/src/components/TimeSelect/TimeSelectPage.tsx
@@ -198,6 +198,10 @@ function TimeSelectPage({
useEffect(() => {
if (!isGeneralDays) return;
+ if (calendarFramework.dates.length === 0) return;
+
+ const startDate = calendarFramework.dates[0]?.[0]?.date;
+ if (!startDate || startDate.getFullYear() !== 2000) return;
// you need to injet dates into each column so later on
const today = new Date();
@@ -244,7 +248,7 @@ function TimeSelectPage({
...prev,
dates: updatedDates,
}));
- }, [isGeneralDays]);
+ }, [isGeneralDays, calendarFramework.dates]);
const fetchGoogleCalEvents = async (
calIds: string[]
@@ -252,13 +256,15 @@ function TimeSelectPage({
if (!hasAccess || calIds.length === 0) return [];
const dates = calendarFramework.dates.flat();
- const timeMin = dates[0]?.date?.toISOString() ?? new Date().toISOString();
- const timeMax = new Date(dates[dates.length - 1].date as Date).setUTCHours(
- 23,
- 59,
- 59,
- 999
- );
+ const dateTimestamps = dates.map((d) => (d.date as Date).getTime());
+
+ const startDate = new Date(Math.min(...dateTimestamps));
+ startDate.setHours(0, 0, 0, 0);
+ const timeMin = startDate.toISOString();
+
+ const endDate = new Date(Math.max(...dateTimestamps));
+ endDate.setHours(23, 59, 59, 999);
+ const timeMax = endDate.toISOString();
const allEvents: calendar_v3.Schema$Event[] = [];
@@ -266,7 +272,7 @@ function TimeSelectPage({
const events = await getEvents(
calId,
timeMin,
- new Date(timeMax).toISOString(),
+ timeMax,
calendarFramework.timezone
);
@@ -295,6 +301,7 @@ function TimeSelectPage({
hasAccess,
shouldFillAvailability,
calendarFramework.timezone,
+ calendarFramework.dates,
]);
// Fetch the user's Google Calendars
diff --git a/src/components/navbar/NavBar.tsx b/src/components/navbar/NavBar.tsx
index b78509cc..d3d5c6e7 100644
--- a/src/components/navbar/NavBar.tsx
+++ b/src/components/navbar/NavBar.tsx
@@ -5,6 +5,7 @@ import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../../backend/firebase';
+import TutorialModal from '../utils/components/TutorialModal/TutorialModal';
import {
IconMenu2,
@@ -19,6 +20,7 @@ import {
IconMoonFilled,
IconMoon,
IconSun,
+ IconBook,
} from '@tabler/icons-react';
import { useAuth } from '../../backend/authContext';
import { useTheme } from '../../contexts/ThemeContext';
@@ -34,6 +36,8 @@ export default function NavBar() {
const { login, logout, currentUser } = useAuth();
+ const [showTutorial, setTutorial] = useState(false);
+
const handleMouseLeave = () => {
setIsOpen(false);
};
@@ -82,6 +86,13 @@ export default function NavBar() {
return (
<>
+ {showTutorial && (
+
setTutorial(false)}
+ />
+ )}
+
@@ -148,6 +159,20 @@ export default function NavBar() {
aria-orientation="vertical"
aria-labelledby="options-menu"
>
+
{
+ e.preventDefault();
+ setTutorial(true);
+ setMenuState('closed');
+ }}
+ >
+ Tutorial
+
+
+
+
void;
+}
+
+interface TutorialSlide {
+ id: string;
+ title: string;
+ description: string;
+ media: { type: 'video' | 'image'; src: string; alt: string };
+}
+
+const tutorialSlides: TutorialSlide[] = [
+ {
+ id: 'welcome',
+ title: 'Welcome to ymeets',
+ description:
+ 'The easiest way to find a time that works for everyone. Let us show you how it works.',
+ media: { type: 'video', src: stock_meeting_gif, alt: 'ymeets overview' },
+ },
+ {
+ id: 'create',
+ title: 'Create Your Event',
+ description:
+ 'Name your event, set the timezone, and pick your dates. Choose specific calendar days or recurring days of the week.',
+ media: { type: 'image', src: day_select_pic, alt: 'Date selection' },
+ },
+ {
+ id: 'share',
+ title: 'Share and Invite',
+ description:
+ 'Copy your event link to share. Participants can add availability manually or sync directly from Google Calendar.',
+ media: { type: 'video', src: copy_link_gif, alt: 'Sharing event' },
+ },
+ {
+ id: 'schedule',
+ title: 'Find the Perfect Time',
+ description:
+ 'See when everyone is free at a glance. Hover over names for details, then export your meeting to Google Calendar.',
+ media: { type: 'video', src: group_view_vid, alt: 'Viewing availability' },
+ },
+ {
+ id: 'done',
+ title: "You're All Set!",
+ description:
+ 'Access all your events from "My Events" in the navigation. Happy scheduling!',
+ media: { type: 'video', src: delete_vid, alt: 'My Events' },
+ },
+];
+
+export default function TutorialModal({ isOpen, onClose }: TutorialProps) {
+ const [isVisible, setIsVisible] = useState(false);
+ const [isAnimating, setIsAnimating] = useState(false);
+ const [currentSlide, setCurrentSlide] = useState(0);
+ const [mediaLoaded, setMediaLoaded] = useState(false);
+ const [isTransitioning, setIsTransitioning] = useState(false);
+
+ const totalSlides = tutorialSlides.length;
+ const isFirstSlide = currentSlide === 0;
+ const isLastSlide = currentSlide === totalSlides - 1;
+ const currentSlideData = tutorialSlides[currentSlide];
+
+ useEffect(() => {
+ if (isOpen) {
+ setIsVisible(true);
+ setCurrentSlide(0);
+ setMediaLoaded(false);
+ setIsTransitioning(false);
+ document.body.style.overflow = 'hidden';
+ setTimeout(() => setIsAnimating(true), 10);
+ } else if (isVisible) {
+ setIsAnimating(false);
+ const timer = setTimeout(() => {
+ setIsVisible(false);
+ document.body.style.overflow = '';
+ }, 300);
+ return () => clearTimeout(timer);
+ }
+ }, [isOpen, isVisible]);
+
+ useEffect(() => {
+ setMediaLoaded(false);
+ }, [currentSlide]);
+
+ const changeSlide = (newSlide: number) => {
+ if (isTransitioning) return;
+ setIsTransitioning(true);
+
+ setTimeout(() => {
+ setCurrentSlide(newSlide);
+ setTimeout(() => {
+ setIsTransitioning(false);
+ }, 50);
+ }, 150);
+ };
+
+ const handleNext = () => {
+ if (!isLastSlide) {
+ changeSlide(currentSlide + 1);
+ } else {
+ localStorage.setItem('hasSeenCreatorTutorial', 'true');
+ onClose();
+ }
+ };
+
+ const handlePrevious = () => {
+ if (!isFirstSlide) {
+ changeSlide(currentSlide - 1);
+ }
+ };
+
+ const handleDotClick = (index: number) => {
+ if (index !== currentSlide) {
+ changeSlide(index);
+ }
+ };
+
+ const handleClose = () => {
+ localStorage.setItem('hasSeenCreatorTutorial', 'true');
+ onClose();
+ };
+
+ if (!isVisible) return null;
+
+ return (
+
+ {/* Backdrop - click to dismiss */}
+
+
+ {/* Modal */}
+
+ {/* Media container */}
+
+ {/* Loading skeleton */}
+
+
+ {/* Media content with fade transition */}
+
+ {currentSlideData.media.type === 'video' ? (
+
+ ) : (
+

setMediaLoaded(true)}
+ />
+ )}
+
+
+
+ {/* Text content with fade transition */}
+
+
+ {currentSlideData.title}
+
+
+ {currentSlideData.description}
+
+
+
+ {/* Progress dots with glow */}
+
+ {tutorialSlides.map((_, index) => {
+ const isCompleted = index < currentSlide;
+ const isCurrent = index === currentSlide;
+
+ return (
+
+ );
+ })}
+
+
+ {/* Navigation */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/utils/components/TutorialModal/ViewAvailable.mp4 b/src/components/utils/components/TutorialModal/ViewAvailable.mp4
new file mode 100644
index 00000000..d0832e8f
Binary files /dev/null and b/src/components/utils/components/TutorialModal/ViewAvailable.mp4 differ
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
index 9bb86ecd..40e1f705 100644
--- a/src/react-app-env.d.ts
+++ b/src/react-app-env.d.ts
@@ -6,3 +6,8 @@ declare global {
FB: typeof FB
}
}
+
+declare module '*.mp4' {
+ const src: string;
+ export default src;
+}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index 7677bed8..f6936f90 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -14,11 +14,16 @@ module.exports = {
'scale-in': {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' }
+ },
+ 'shimmer': {
+ '0%': { backgroundPosition: '-200% 0' },
+ '100%': { backgroundPosition: '200% 0' }
}
},
animation: {
'fade-in': 'fade-in 0.2s ease-out',
- 'scale-in': 'scale-in 0.2s ease-out'
+ 'scale-in': 'scale-in 0.2s ease-out',
+ 'shimmer': 'shimmer 1.5s infinite'
},
fontFamily: {
roboto: ["Roboto", "sans"],