create schedule meeting component, modifided homepage and so on

This commit is contained in:
Juthatip McDevitt 2024-04-30 17:21:03 -05:00
parent 15f7330617
commit 367a6f5ae3
14 changed files with 4554 additions and 31 deletions

View file

@ -10,12 +10,17 @@ import UserFormInfo from "./UserFormInfo";
import { collection, doc, getDocs, getFirestore, query, setDoc, where } from "firebase/firestore";
import { app } from "@/config/FirebaseConfig";
import { toast } from "sonner";
import Plunk from "@plunk/node";
import { render } from '@react-email/render';
import Email from "@/emails";
import { useRouter } from "next/navigation";
const MeetingEventSelection = ({meetingEventInfo, businessInfo}) => {
const [date, setDate] = useState(new Date());
const [timeSlots, setTimeSlots] = useState();
const [unavialableTime, setUnavialableTime] = useState(false);
const [selectedTime, setSelectedTime] = useState();
const router = useRouter();
//set user schedule data
const [userName, setUserName] = useState();
@ -30,6 +35,9 @@ const MeetingEventSelection = ({meetingEventInfo, businessInfo}) => {
const db = getFirestore(app);
//send an email using React Email and the Plunk Node.js SDK
const plunk = new Plunk(process.env.NEXT_PUBLIC_PLUNK_API_KEY);
//create time slot
const createTimeSlot = (interval) => {
const startTime = 8 * 60;
@ -77,13 +85,16 @@ const MeetingEventSelection = ({meetingEventInfo, businessInfo}) => {
userEmail: userEmail,
selectedTime: selectedTime,
selectedDate: date,
formatedDate:format(date, 'PPP'),
formatedTimeStamp:format(date, 't'),
userMessage: userMessage
}).then(res => {
toast('Your schedule is created successfully');
sendEmail(userName)
})
}
//fetch data to block out selection days and time
//fetch data to blockout selection days and time
const eventBooking = async (datebooking) => {
const q = query(collection(db, 'ScheduleMeeting'), where('selectedDate', '==', datebooking), where('eventId', '==', meetingEventInfo.id));
const querySnapshot = await getDocs(q);
@ -93,6 +104,26 @@ const MeetingEventSelection = ({meetingEventInfo, businessInfo}) => {
})
}
//create an email using Plunk
const sendEmail = (user) => {
const emailHtml = render(<Email
userFirstName={user}
businessName={businessInfo?.businessName}
meetingDate={format(date, 'PPP').toString()}
meetingTime={selectedTime}
duration={meetingEventInfo?.timeDuration}
meetingUrl={meetingEventInfo?.urlMeeting}/>);
plunk.emails.send({
to: userEmail,
subject: "Meeting Schedule Detail",
body: emailHtml,
}).then(res => {
console.log(res)
router.replace('/confirmation')
});
}
useEffect(() => {
meetingEventInfo?.timeDuration && createTimeSlot(meetingEventInfo?.timeDuration)
}, [meetingEventInfo])

View file

@ -0,0 +1,14 @@
import Image from 'next/image'
import React from 'react'
const Confirmation = () => {
return (
<div className='max-w-sm mx-auto h-screen flex flex-col items-center px-5 text-center'>
<Image src='/check.png' width={80} height={80} alt='check' className='mt-40 mb-5'/>
<p className='font-bold text-2xl text-[#1F1717] mb-2'>This meeting is scheduled</p>
<p className='text-sm text-gray-500'>We emailed you a confirmation and meeting details to you on your email.</p>
</div>
)
}
export default Confirmation

View file

@ -21,8 +21,6 @@ const DashboardHeader = () => {
<DropdownMenuContent className='border border-gray-200 mt-1 bg-white'>
<DropdownMenuLabel>{user.given_name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Setting</DropdownMenuItem>
<DropdownMenuItem><LogoutLink>Logout</LogoutLink></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -4,7 +4,8 @@ import { app } from '@/config/FirebaseConfig'
import { useKindeBrowserClient } from '@kinde-oss/kinde-auth-nextjs'
import { collection, deleteDoc, doc, getDoc, getDocs, getFirestore, orderBy, query, where } from 'firebase/firestore'
import { GoClock } from "react-icons/go";
import { PiMapPinLight, PiCopyLight, PiShareFatLight } from 'react-icons/pi'
import { PiMapPinLight, PiCopyLight } from 'react-icons/pi'
import { IoTrashOutline } from "react-icons/io5";
import { IoSettingsOutline } from "react-icons/io5";
import { toast } from 'sonner'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"
@ -65,21 +66,8 @@ const MeetingEventList = () => {
</div>
<hr/>
<div className='flex justify-between'>
<div className='flex gap-2'>
<button onClick={() => {onCopyHandle(event)}} className='text-lg'><PiCopyLight/></button>
<button className="text-lg"><PiShareFatLight/></button>
</div>
<div className='flex'>
<DropdownMenu>
<DropdownMenuTrigger>
<IoSettingsOutline className='cursor-pointer'/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDeleteEvent(event)}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<button onClick={() => onDeleteEvent(event)}><IoTrashOutline/></button>
</div>
</div>
)): <p className='font-semibold text-green-800'>Loading...</p>

View file

@ -0,0 +1,39 @@
import React from 'react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"
import { PiCalendarCheck } from 'react-icons/pi'
import { GoClock } from 'react-icons/go'
import { AiOutlineLink } from "react-icons/ai";
import Link from 'next/link'
const ScheduleList = ({meetingList}) => {
return (
<div>
{meetingList && meetingList.map((meeting, index) => (
<Accordion key={index} type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>{meeting?.formatedDate}</AccordionTrigger>
<AccordionContent>
<div>
<div className='mt-2 flex flex-col gap-2'>
<p className='flex gap-1 items-center'><PiCalendarCheck/>{meeting.formatedDate}</p>
<p className='flex gap-1 items-center'><GoClock/>{meeting?.timeDuration} minutes
<span className="text-sm text-gray-500">({meeting.selectedTime})</span>
</p>
<div className='flex justify-between items-center'>
<p className='flex gap-1 items-center'>
<AiOutlineLink/>
<Link href={meeting?.urlMeeting?meeting?.urlMeeting:'#'} className="text-gray-500 underline text-sm">{meeting?.urlMeeting}</Link>
</p>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
))}
</div>
)
}
export default ScheduleList

View file

@ -0,0 +1,61 @@
"use client"
import React, { useEffect, useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import ScheduleList from './_components/ScheduleList'
import { collection, getDocs, getFirestore, query, where } from 'firebase/firestore'
import { app } from '@/config/FirebaseConfig'
import { useKindeBrowserClient } from '@kinde-oss/kinde-auth-nextjs'
import { format } from 'date-fns'
const ScheduleMeeting = () => {
const [meetingList, setMeetingList] = useState([]);
const {user} = useKindeBrowserClient();
const db = getFirestore(app);
const getSchedule = async () => {
setMeetingList([])
const q = query(collection(db, 'ScheduleMeeting'), where('businessEmail', '==', user.email))
const querySnapshot = await getDocs(q);
querySnapshot.forEach(doc => {
console.log(doc.data());
setMeetingList(prev => [...prev, doc.data()])
})
}
//filter meeting
const filterMeetingList = (type) => {
if(type == 'upcoming'){
return meetingList.filter(item => item.formatedTimeStamp >= format(new Date(), 't'))
} else{
return meetingList.filter(item => item.formatedTimeStamp < format(new Date(), 't'))
}
}
useEffect(() => {
user && getSchedule()
}, [user])
return (
<div className='px-5'>
<p className='font-semibold text-xl mb-5'>Schedule Meeting</p><hr/>
<Tabs defaultValue="upcoming" className="w-[400px] mt-5">
<TabsList>
<TabsTrigger value="upcoming">Upcoming</TabsTrigger>
<TabsTrigger value="history">Meeting History</TabsTrigger>
</TabsList>
<TabsContent value="upcoming">
<ScheduleList meetingList={filterMeetingList('upcoming')}/>
</TabsContent>
<TabsContent value="history">
<ScheduleList meetingList={filterMeetingList('history')}/>
</TabsContent>
</Tabs>
</div>
)
}
export default ScheduleMeeting

View file

@ -1,26 +1,36 @@
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { GoClock } from 'react-icons/go'
import { PiCalendarCheck, PiInfo, PiMapPinLight } from 'react-icons/pi'
const Hero = () => {
return (
<>
<div className='flex flex-col text-center mt-20 md:mt-40 overflow-hidden'>
<div>
<p className='uppercase text-5xl md:text-6xl font-semibold drop-shadow-xl font-outline-2 text-white mb-5'>Schedule meetings</p>
<p className='uppercase text-4xl md:text-5xl font-semibold drop-shadow-xl mb-5'>for <span>everyone</span></p>
<p className='text-lg sm:text-xl font-semibold'>Set your availability and share your link.</p>
<p className='text-lg sm:text-xl font-semibold'>Schedule.Me ensures you never get double booked!</p>
<p className='text-base sm:text-xl font-semibold min-[400px]:text-lg'>Set your availability and share your link.</p>
<p className='text-base sm:text-xl font-semibold min-[400px]:text-lg'>Schedule.Me ensures you never get double booked!</p>
</div>
{/*<div className='mt-10 sm:mt-20'>
<p className='mb-10 text-sm'>Sign up with <span className='font-semibold'>Google</span> and <span className='font-semibold'>Facebook</span></p>
<div className='flex flex-col md:flex md:flex-row gap-5 justify-center items-center text-center'>
<button className='flex items-center gap-1 mb-0 md:mb-5 px-4 py-2 border bg-[#101010] text-white hover:opacity-90'><Image src='/google.png' width={20} height={20} alt='google' className='w-[15px] h-[15px]'/>Sign up with Google</button>
<button className='flex items-center gap-1 mb-5 px-4 py-2 border border-[#101010] hover:bg-[gray] hover:border-[gray] hover:text-white'><Image src='/facebook.png' alt='facebook' width={20} height={20} className='w-[15px] h-[15px]'/>Sign up with Facebook</button>
</div>
<hr className='mb-5 mt-5 w-[350px] mx-auto'/>
<Link href="" className='text-sm text-[gray] hover:underline transition-all duration-500'>Sign up with your email</Link>
</div>*/}
<div className='max-w-xl bg-white grid grid-cols-2 mx-auto mt-2 border border-black border-t-8 rounded-md shadow-md min-[400px]:mt-5'>
<div className='flex flex-col gap-2 p-5 border-r'>
<p className='text-md uppercase font-semibold tracking-wide mb-2'>Schedule.Me</p>
<p className='flex w-5 h-5 rounded-full bg-gray-500 text-white justify-center items-center mb-2'>S</p>
<p className='flex gap-1 items-center text-xs'><PiInfo className='text-base'/>Stand-up meeting</p>
<p className='flex gap-1 items-center text-xs'><PiCalendarCheck className='text-base'/>April 30th, 2024</p>
<p className='flex gap-1 items-center text-xs'><GoClock className='text-base'/>30 minutes </p>
<p className='flex gap-1 items-center text-xs'><PiMapPinLight className='text-base'/>Zoom</p>
<Link href="https://zoom.us/" className="text-gray-500 underline text-xs">https://zoom.us</Link>
</div>
<div className='flex flex-col gap-2 px-5 mt-14 mb-5'>
<p className='text-xs font-semibold'>Select date & time</p>
<Image src='/calendar.png' width={200} height={200} alt='calendar'/>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,43 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -0,0 +1,43 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props} />
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props} />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props} />
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View file

@ -0,0 +1,54 @@
import { Body, Button, Container, Column, Head, Heading, Hr, Html, Img, Link, Row, Section, Text, Tailwind, } from "@react-email/components";
import * as React from "react";
const baseUrl = "";
export const Email = ({
userFirstName,
businessName,
meetingUrl,
meetingDate,
meetingTime,
duration
}) => {
return (
<Html>
<Head />
<Tailwind>
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<p className="flex justify-center text-xl uppercase font-semibold tracking-wide">Schedule.Me</p>
</Section>
<Text className="text-black text-[14px] leading-[24px]">Hello <span className="capitalize">{userFirstName}</span>,</Text>
<Text className="text-black text-[14px] leading-[24px]">
You have scheduled meeting with {businessName}. Please find your meeting details below this email. Thank you for scheduling meeting with us.
</Text>
<Text>
<p className="text-[14px]"><b>Date:</b> {meetingDate}</p>
<p className="text-[14px]"><b>Time:</b> {meetingTime}</p>
<p className="text-[14px]"><b>Time duration:</b> {duration}</p>
</Text>
<Section className="text-center mt-[32px] mb-[32px]">
<Button className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3" href={meetingUrl}> Join Now </Button>
</Section>
<Text>
or copy and paste this URL into your browser:{meetingUrl}
</Text>
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
<Text className="text-[#666666] text-[12px] leading-[24px]">
This notification was intended for <span className="text-black capitalize">{userFirstName}</span>. If you were not scheduling this meeting, you can ignore this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default Email;

File diff suppressed because it is too large Load diff

View file

@ -6,13 +6,18 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"email": "email dev"
},
"dependencies": {
"@kinde-oss/kinde-auth-nextjs": "^2.2.4",
"@plunk/node": "^3.0.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@react-email/components": "0.0.17",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
@ -23,6 +28,7 @@
"react": "^18",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-email": "2.1.2",
"react-icons": "^5.1.0",
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB