diff --git a/src/components/Home/HomeLandingSections.tsx b/src/components/Home/HomeLandingSections.tsx new file mode 100644 index 0000000..aa13cc7 --- /dev/null +++ b/src/components/Home/HomeLandingSections.tsx @@ -0,0 +1,386 @@ +import * as React from 'react'; +import { + IconBrandGoogle, + IconLock, + IconMapPin, + IconMapPinFilled, + IconUsers, +} from '@tabler/icons-react'; +import Button from '../utils/components/Button'; +import imgHeroCalendar from '../utils/components/TutorialModal/DaySelect-dark.png'; +import heroVidCreate from '../utils/components/TutorialModal/ymeets-create-event-vid.mp4'; +import heroVidShare from '../utils/components/TutorialModal/ymeets-copy-link-vid.mp4'; +import heroVidImport from '../utils/components/TutorialModal/ymeets-import-cals-and-autofill-avails.mp4'; +import heroVidSchedule from '../utils/components/TutorialModal/ymeets-view-availabilities.mp4'; +import imgHowItWorksStep3 from './assets/how-it-works-step-3.png'; +import gcalLogo from '../SideBySideView/gcal-logo.png'; + +type NavigateFn = (path: string) => void; + +const sectionShell = 'max-w-6xl mx-auto px-4 sm:px-6 lg:px-8'; + +const heroTutorialClips: { + src: string; + chromeTitle: string; + videoAlt: string; +}[] = [ + { + src: heroVidCreate, + chromeTitle: 'ymeets — New event', + videoAlt: 'Screen recording: creating an event in ymeets', + }, + { + src: heroVidShare, + chromeTitle: 'ymeets — Share invite', + videoAlt: 'Screen recording: sharing an event link', + }, + { + src: heroVidImport, + chromeTitle: 'ymeets — Calendar sync', + videoAlt: 'Screen recording: importing Google Calendar in ymeets', + }, + { + src: heroVidSchedule, + chromeTitle: 'ymeets — Availability', + videoAlt: 'Screen recording: viewing group availability', + }, +]; + +export function LandingHero({ + navigate, + onOpenTutorial, +}: { + navigate: NavigateFn; + onOpenTutorial: () => void; +}) { + const [heroClipIndex, setHeroClipIndex] = React.useState(0); + const [prefersReducedMotion, setPrefersReducedMotion] = React.useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ); + + React.useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + const onChange = () => setPrefersReducedMotion(mq.matches); + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, []); + + const clip = heroTutorialClips[heroClipIndex]; + + return ( +
+
+
+

+ A cleaner, faster way to schedule meetings on Yale's campus. +

+
    +
  • + + GCal integration +
  • +
  • + + + + Vote on a preferred campus meeting place + + + Vote on a preferred meeting place + + +
  • +
  • + + + Lock in the best time and place to gather with a selection + +
  • +
+
+ +
+

+ + New to ymeets? + {' '} + +

+
+ +
+
+
+
+
+ + + + + + + {clip.chromeTitle} + +
+
+ {prefersReducedMotion ? ( + ymeets calendar: pick days and times for your event + ) : ( + + )} +
+ {!prefersReducedMotion && ( +
+

+ Quick walkthrough{' '} + + • + {' '} + {heroClipIndex + 1} of {heroTutorialClips.length} +

+
+ {heroTutorialClips.map((_, index) => { + const isCurrent = index === heroClipIndex; + return ( + + ); + })} +
+
+ )} +
+
+
+
+
+ ); +} + +const featureCardBase = + 'rounded-2xl border border-outline/25 dark:border-gray-600/70 bg-secondary_background dark:bg-secondary_background-dark p-6 sm:p-8 shadow-sm hover:shadow-md transition-shadow duration-300'; + +export function LandingFeatureHighlights() { + const items = [ + { + icon: IconBrandGoogle, + title: 'Google Calendar sync', + body: 'Auto-fill availability from Google Calendar to save time.', + }, + { + icon: IconUsers, + title: 'Group scheduling', + body: 'Compare times across your group without endless messages.', + }, + { + icon: IconMapPin, + title: 'Campus place voting', + body: 'Let everyone vote on the best place to meet on campus.', + }, + ]; + + return ( +
+
+ {items.map(({ icon: Icon, title, body }) => ( +
+
+ +
+

+ {title} +

+

+ {body} +

+
+ ))} +
+
+ ); +} + +/** Same frame for all steps: tight border, height from content (image or GCal panel). */ +const howItWorksVisualFrame = + 'w-full shrink-0 overflow-hidden rounded-xl border border-outline/20 dark:border-gray-600/50 bg-secondary_background dark:bg-gray-800/90 shadow-inner p-0'; + +export function LandingHowItWorks() { + const steps = [ + { + title: 'Create an event', + body: 'Name it, set your time window, then share the event—copy the link or invite people by email.', + visual: ( + ymeets new event form and calendar + ), + }, + { + title: 'Pull in availability with Google Calendar', + body: 'Connect GCal so your busy times show up instantly—and compare with the rest of the group.', + visual: ( +
+
+ +
+
+

+ Calendar-connected availability +

+

+ Optional sync—or mark free times by hand. Your grid updates for + the group either way. +

+
+
+ ), + }, + { + title: 'Vote and finalize time & place', + body: 'See overlapping availability, vote on Yale campus locations, and lock the meeting details everyone agreed on.', + visual: ( + ymeets: group availability and campus location preferences + ), + }, + ]; + + return ( +
+

+ How it works +

+

+ Three steps from “we should meet” to a booked time and place. +

+
+ {steps.map((step, i) => ( +
+
+ + {i + 1} + +

+ {step.title} +

+
+

+ {step.body} +

+
+ {step.visual} +
+
+ ))} +
+
+ ); +} + +export function LandingFinalCta({ navigate }: { navigate: NavigateFn }) { + return ( +
+
+

+ Ready to stop scheduling in the group chat? +

+

+ Create your first event in under a minute—built for Yale, free to + use. +

+
+ +
+
+
+ ); +} diff --git a/src/components/Home/HomePage.tsx b/src/components/Home/HomePage.tsx index c77c92b..7bf8f30 100644 --- a/src/components/Home/HomePage.tsx +++ b/src/components/Home/HomePage.tsx @@ -1,177 +1,37 @@ -/* eslint-disable */ import * as React from 'react'; -// import background from '../landingpage/landingbackground.jpg' import { useNavigate } from 'react-router-dom'; -import { checkIfLoggedIn, getEventById } from '../../backend/events'; -import graphic from './calendargraphic.png'; -import LoginPopup from '../utils/components/LoginPopup'; import Footer from '../utils/components/Footer'; -import Button from '../utils/components/Button'; import TutorialModal from '../utils/components/TutorialModal/TutorialModal'; - -// import { SiGooglecalendar } from 'react-icons/si'; -// import { FaLock } from 'react-icons/fa'; -// import { CiLocationOn } from 'react-icons/ci'; - import { - IconMapPinFilled, - IconMapPin, - IconBrandGoogle, - IconLockAccessOff, - IconLock, -} from '@tabler/icons-react'; -import { sendAvailabilityUpdatedEmail } from '../../emails/sendEmailHelpers'; + LandingHero, + LandingFeatureHighlights, + LandingHowItWorks, + LandingFinalCta, +} from './HomeLandingSections'; export default function HomePage() { const navigate = useNavigate(); - const [showInput, setShowInput] = React.useState(true); - const [eventCode, setEventCode] = React.useState(''); - const [showLoginPopup, setShowLoginPopup] = React.useState(false); - const [showFormValidation, setShowFormValidation] = - React.useState(false); - const [formErrorMessage, setFormErrorMessage] = React.useState(''); - const [emailStatus, setEmailStatus] = React.useState< - 'idle' | 'sending' | 'sent' | 'error' - >('idle'); - - function formValidationPopup(message: string) { - setShowFormValidation(true); - setFormErrorMessage(message); - } - - const showEventInput = () => { - setShowInput(!showInput); - }; - const updateEventCode = (event: React.BaseSyntheticEvent) => { - setEventCode(event.target.value); - }; - const handleKeyPress = (e: any) => { - if (e.key == 'Enter') { - validateAndGoToEvent(); - } - }; - - const goToEvent = () => { - getEventById(eventCode) - .then((result) => { - navigate('/dashboard/' + eventCode); - }) - .catch((err) => { - formValidationPopup('Code is invalid.'); - }); - }; - - const validateAndGoToEvent = () => { - if (eventCode.length != 6) { - formValidationPopup('Codes are 6 characters long.'); - } else { - goToEvent(); - } - }; - - const handleLoginPopupClose = (successFlag?: boolean) => { - setShowLoginPopup(false); - if (successFlag) { - // instead of checkIfLoggedIn because login is async - goToEvent(); - } - }; - - const [showTutorial, setTutorial] = React.useState(false); + const [showTutorial, setShowTutorial] = React.useState(false); return (
-
- {showTutorial && ( setTutorial(false)} + isOpen={showTutorial} + onClose={() => setShowTutorial(false)} /> )} -
-
-
-
-

- A cleaner, faster way to schedule meetings on Yale's campus. -

-

-
-
- -

GCal integration

-
-
- -

- Vote on a preferred campus meeting place -

-

- Vote on a preferred meeting place -

-
-
- -

- Lock in the best time and place to gather with a selection -

-
-
-

-
- -
- - - -
- -

- New to ymeets? → - setTutorial(true)}> - Click here for a quick walkthrough! - -

- -
- -
- graphic -
-
- - {showLoginPopup && ( - - )} -
+
+ setShowTutorial(true)} + /> + + + +
- {/* Footer always at bottom */}
); diff --git a/src/components/Home/assets/how-it-works-step-3.png b/src/components/Home/assets/how-it-works-step-3.png new file mode 100644 index 0000000..98d3a96 Binary files /dev/null and b/src/components/Home/assets/how-it-works-step-3.png differ diff --git a/src/components/utils/components/TutorialModal/DaySelect-dark.png b/src/components/utils/components/TutorialModal/DaySelect-dark.png index 449acfe..0d5168f 100644 Binary files a/src/components/utils/components/TutorialModal/DaySelect-dark.png and b/src/components/utils/components/TutorialModal/DaySelect-dark.png differ diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 40e1f70..34c5719 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -10,4 +10,9 @@ declare global { declare module '*.mp4' { const src: string; export default src; +} + +declare module '*.png' { + const src: string; + export default src; } \ No newline at end of file