-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: add calendar with events. [dev only]
- Loading branch information
1 parent
581c9fb
commit 6a92cd1
Showing
24 changed files
with
809 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
OPENWEATHER_APIKEY=YOUR_API_KEY | ||
NEXT_PUBLIC_SUPABASE_URL=your-project-url | ||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,3 +39,6 @@ next-env.d.ts | |
|
||
# editors | ||
.idea/ | ||
|
||
# extension build | ||
/extension/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import clsx from "clsx"; | ||
import { ChevronLeft, ChevronRight } from "lucide-react"; | ||
import { getDayClassName } from "./utils"; | ||
import { FC, ReactNode } from "react"; | ||
import { useDatePicker } from "@rehookify/datepicker"; | ||
import { Button } from "@/components/ui/button"; | ||
import { IEvent } from "@/types"; | ||
import { Popover, PopoverContent } from "../ui/popover"; | ||
import { PopoverTrigger } from "@radix-ui/react-popover"; | ||
|
||
interface RowProps { | ||
className?: string; | ||
children?: ReactNode; | ||
} | ||
|
||
export const Row: FC<RowProps> = ({ className, children }) => { | ||
return ( | ||
<div className={clsx("grid grid-cols-7 gap-0 pt-4", className)}> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
interface CalendarProps { | ||
selectedDates: Date[]; | ||
onDatesChange: (dates: Date[]) => void; | ||
events: IEvent[]; | ||
} | ||
|
||
export const Calendar: FC<CalendarProps> = ({ | ||
selectedDates, | ||
onDatesChange, | ||
events, | ||
}) => { | ||
const { | ||
data: { calendars, weekDays }, | ||
propGetters: { dayButton, addOffset, subtractOffset }, | ||
} = useDatePicker({ | ||
selectedDates, | ||
onDatesChange, | ||
calendar: { | ||
mode: "static", | ||
startDay: 1, | ||
}, | ||
}); | ||
|
||
const { month, year, days } = calendars[0]; | ||
|
||
function dayIsInTheEvents(day: Date): IEvent[] { | ||
if (!events || events.length == 0) return []; | ||
const eventsThatDay = events.filter( | ||
(e) => | ||
e.status !== "cancelled" && | ||
new Date(e.start.dateTime).getDate() === day.getDate() && | ||
new Date(e.start.dateTime).getMonth() === day.getMonth() && | ||
new Date(e.start.dateTime).getFullYear() === day.getFullYear() | ||
); | ||
return eventsThatDay; | ||
} | ||
|
||
function isToday(day: Date) { | ||
const today = new Date(); | ||
return ( | ||
day.getDate() === today.getDate() && | ||
day.getMonth() === today.getMonth() && | ||
day.getFullYear() === today.getFullYear() | ||
); | ||
} | ||
|
||
return ( | ||
<section className="flex flex-col items-center justify-center w-full h-full"> | ||
<div className="flex items-center justify-between w-11/12"> | ||
<Button | ||
variant="outline" | ||
size="icon" | ||
className="flex items-center justify-center w-8 h-8 text-center" | ||
{...subtractOffset({ months: 1 })} | ||
> | ||
<ChevronLeft | ||
size={24} | ||
className="w-5 text-muted-foreground" | ||
/> | ||
</Button> | ||
<p className="text-base text-center"> | ||
{month} {year} | ||
</p> | ||
<Button | ||
variant="outline" | ||
size="icon" | ||
className="flex items-center justify-center w-8 h-8 text-center" | ||
{...addOffset({ months: 1 })} | ||
> | ||
<ChevronRight | ||
size={24} | ||
className="w-5 text-muted-foreground" | ||
/> | ||
</Button> | ||
</div> | ||
<Row className="flex items-center justify-center w-full font-medium h-fit text-muted-foreground"> | ||
{weekDays.map((d, idx) => ( | ||
<p key={idx} className="w-5/6 text-sm text-center h-fit"> | ||
{d.slice(0, 2)} | ||
</p> | ||
))} | ||
</Row> | ||
<Row className="w-full"> | ||
{/* // TODO - create a separate component for each day so that the dayIsInTheEvents can be fixed so it isnt called everywhere */} | ||
{/* //!! VERY RO-BUST */} | ||
{days.map((d) => ( | ||
<Popover key={d.$date.toString()}> | ||
<PopoverTrigger asChild> | ||
<Button | ||
variant="ghost" | ||
className={clsx( | ||
"w-5/6 text-xs aspect-square relative", | ||
isToday(d.$date) && "border border-border" | ||
)} | ||
// {...dayButton(d)} | ||
onClick={() => { | ||
onDatesChange([d.$date]); | ||
}} | ||
> | ||
{dayIsInTheEvents(d.$date).length > 0 && ( | ||
<div className="absolute w-5 h-1 rounded-full bottom-1.5 bg-primary" /> | ||
)} | ||
{d.day} | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent | ||
className={clsx( | ||
"p-0", | ||
dayIsInTheEvents(d.$date).length == 0 && | ||
"bg-transparent border-transparent" | ||
)} | ||
> | ||
{dayIsInTheEvents(d.$date) && | ||
dayIsInTheEvents(d.$date).map((e, index) => ( | ||
<div | ||
key={index} | ||
className="flex items-center justify-center gap-2 p-2" | ||
> | ||
<div className="flex-1 w-1 h-full text-transparent rounded-full grow bg-primary"> | ||
a | ||
</div> | ||
<div className="flex flex-col items-start justify-center w-full"> | ||
<p className="text-sm font-medium text-left"> | ||
{e.summary} | ||
</p> | ||
{e.description && ( | ||
<p className="w-full mt-1 overflow-hidden text-xs text-left text-muted-foreground"> | ||
{e.description.slice( | ||
0, | ||
100 | ||
)} | ||
</p> | ||
)} | ||
</div> | ||
</div> | ||
))} | ||
</PopoverContent> | ||
</Popover> | ||
))} | ||
</Row> | ||
</section> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { DPDay, DPMonth, DPYear } from "@rehookify/datepicker"; | ||
import clsx from "clsx"; | ||
|
||
export const getDayClassName = ( | ||
className: string, | ||
{ selected, disabled, inCurrentMonth, now }: DPDay | ||
) => | ||
clsx(className, { | ||
"bg-slate-700 text-white hover:bg-slate-700 opacity-100": selected, | ||
"opacity-25 cursor-not-allowed": disabled, | ||
"opacity-50": !inCurrentMonth, | ||
"border border-slate-500": now, | ||
}); | ||
|
||
export const getMonthClassName = ( | ||
className: string, | ||
{ selected, now, disabled }: DPMonth | ||
) => | ||
clsx(className, { | ||
"bg-slate-700 text-white hover:bg-slate-700 opacity-100": selected, | ||
"border border-slate-500": now, | ||
"opacity-25 cursor-not-allowed": disabled, | ||
}); | ||
|
||
export const getYearsClassName = ( | ||
className: string, | ||
{ selected, now, disabled }: DPYear | ||
) => | ||
clsx(className, { | ||
"bg-slate-700 text-white hover:bg-slate-700 opacity-100": selected, | ||
"border border-slate-500": now, | ||
"opacity-25 cursor-not-allowed": disabled, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { useEffect, useState } from "react"; | ||
import { Card, CardContent } from "@/components/ui/card"; | ||
import { Calendar } from "./calendar"; | ||
import { IEvent } from "@/types"; | ||
import { Button } from "@/components/ui/button"; | ||
import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; | ||
|
||
type Props = {}; | ||
|
||
// TODO - create a separate hook for events | ||
|
||
function CalendarWidget({}: Props) { | ||
const supabase = useSupabaseClient(); | ||
const session = useSession(); | ||
|
||
const [selectedDates, onDatesChange] = useState<Date[]>([]); | ||
|
||
const [events, setEvents] = useState<IEvent[]>([]); | ||
|
||
const handleGoogleSignIn = async () => { | ||
const environment = process.env.NODE_ENV; | ||
|
||
if (environment === "development") { | ||
const { data, error } = await supabase.auth.signInWithOAuth({ | ||
provider: "google", | ||
options: { | ||
scopes: "https://www.googleapis.com/auth/calendar", | ||
}, | ||
}); | ||
|
||
if (error) { | ||
console.log(error); | ||
return; | ||
} | ||
} else { | ||
// handle login with google in production | ||
// for example, trigger an event to be picked up by the content script (which doesn't exist yet) | ||
// I've tried this, but i can't get it to work. PLS HEEELP | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
const getEvents = async () => { | ||
const res = await fetch( | ||
"https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=2500&timeMin=2021-01-01T00%3A00%3A00%2B00%3A00", | ||
{ | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${session?.provider_token}`, | ||
}, | ||
} | ||
); | ||
|
||
const data = await res.json(); | ||
|
||
const filteredEvents = data.items | ||
? data.items.filter( | ||
(event: IEvent) => | ||
new Date(event.start.dateTime).getFullYear() === | ||
new Date().getFullYear() | ||
) | ||
: []; | ||
|
||
setEvents(filteredEvents); | ||
}; | ||
|
||
getEvents(); | ||
}, [session]); | ||
|
||
return ( | ||
<Card | ||
id="calendar-widget" | ||
className="grid grid-cols-1 col-span-2 grid-rows-1 row-span-4 pt-2.5 2xl:pt-5 2xl:row-span-3 place-items-center rounded-xl" | ||
> | ||
<CardContent className="w-full h-full"> | ||
<Calendar | ||
selectedDates={selectedDates} | ||
onDatesChange={onDatesChange} | ||
events={events} | ||
/> | ||
{/* later down the line, remove the check for NODE_ENV caues the production auth will work....I hope 🫠 */} | ||
{!session && process.env.NODE_ENV === "development" && ( | ||
<Button | ||
variant="default" | ||
className="mt-4" | ||
onClick={async () => await handleGoogleSignIn()} | ||
> | ||
Sign in with Google | ||
</Button> | ||
)} | ||
</CardContent> | ||
</Card> | ||
); | ||
} | ||
|
||
export default CalendarWidget; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as React from "react" | ||
import * as PopoverPrimitive from "@radix-ui/react-popover" | ||
|
||
import { cn } from "@/lib/utils" | ||
|
||
const Popover = PopoverPrimitive.Root | ||
|
||
const PopoverTrigger = PopoverPrimitive.Trigger | ||
|
||
const PopoverContent = React.forwardRef< | ||
React.ElementRef<typeof PopoverPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | ||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | ||
<PopoverPrimitive.Portal> | ||
<PopoverPrimitive.Content | ||
ref={ref} | ||
align={align} | ||
sideOffset={sideOffset} | ||
className={cn( | ||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
</PopoverPrimitive.Portal> | ||
)) | ||
PopoverContent.displayName = PopoverPrimitive.Content.displayName | ||
|
||
export { Popover, PopoverTrigger, PopoverContent } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/a037abb17e9548c6.css" as="style" crossorigin=""/><link rel="stylesheet" href="/_next/static/css/a037abb17e9548c6.css" crossorigin="" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" crossorigin="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-5146130448d8adf7.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/framework-fda0a023b274c574.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/main-842c943bbed6ece7.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_app-236e1a6797f4b5de.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_error-95043b19f41f9a58.js" defer="" crossorigin=""></script><script src="/_next/static/UDRF1liZERRBqH7hmegr4/_buildManifest.js" defer="" crossorigin=""></script><script src="/_next/static/UDRF1liZERRBqH7hmegr4/_ssgManifest.js" defer="" crossorigin=""></script></head><body class="min-h-screen bg-background text-foreground"><div id="__next"><script>!function(){try{var d=document.documentElement,c=d.classList;c.remove('light','dark');var e=localStorage.getItem('theme');if('system'===e||(!e&&true)){var t='(prefers-color-scheme: dark)',m=window.matchMedia(t);if(m.media!==t||m.matches){d.style.colorScheme = 'dark';c.add('dark')}else{d.style.colorScheme = 'light';c.add('light')}}else if(e){c.add(e|| '')}if(e==='light'||e==='dark')d.style.colorScheme=e}catch(e){}}()</script><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json" crossorigin="">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"UDRF1liZERRBqH7hmegr4","runtimeConfig":{"OpenWeatherApiKey":"c4d2bf29784c20816ff3debb069c1e00"},"nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html> | ||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>404: This page could not be found</title><meta name="next-head-count" content="3"/><link rel="preload" href="/_next/static/css/b63e26e08eb0884f.css" as="style" crossorigin=""/><link rel="stylesheet" href="/_next/static/css/b63e26e08eb0884f.css" crossorigin="" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" crossorigin="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script><script src="/_next/static/chunks/webpack-702ead4cd452a163.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/framework-fda0a023b274c574.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/main-842c943bbed6ece7.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_app-41d8ad1de1836265.js" defer="" crossorigin=""></script><script src="/_next/static/chunks/pages/_error-95043b19f41f9a58.js" defer="" crossorigin=""></script><script src="/_next/static/QFWR0fAMSIyIXuz40ZyJB/_buildManifest.js" defer="" crossorigin=""></script><script src="/_next/static/QFWR0fAMSIyIXuz40ZyJB/_ssgManifest.js" defer="" crossorigin=""></script></head><body class="min-h-screen bg-background text-foreground"><div id="__next"><script>!function(){try{var d=document.documentElement,c=d.classList;c.remove('light','dark');var e=localStorage.getItem('theme');if('system'===e||(!e&&true)){var t='(prefers-color-scheme: dark)',m=window.matchMedia(t);if(m.media!==t||m.matches){d.style.colorScheme = 'dark';c.add('dark')}else{d.style.colorScheme = 'light';c.add('light')}}else if(e){c.add(e|| '')}if(e==='light'||e==='dark')d.style.colorScheme=e}catch(e){}}()</script><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json" crossorigin="">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"QFWR0fAMSIyIXuz40ZyJB","runtimeConfig":{"OpenWeatherApiKey":"c4d2bf29784c20816ff3debb069c1e00"},"nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html> |
Oops, something went wrong.