Skip to content

Commit

Permalink
feature: add calendar with events. [dev only]
Browse files Browse the repository at this point in the history
  • Loading branch information
bartosz-skejcik committed Jan 21, 2024
1 parent 581c9fb commit 6a92cd1
Show file tree
Hide file tree
Showing 24 changed files with 809 additions and 269 deletions.
2 changes: 2 additions & 0 deletions .env.example
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ next-env.d.ts

# editors
.idea/

# extension build
/extension/
166 changes: 166 additions & 0 deletions components/calendar/calendar.tsx
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>
);
};
33 changes: 33 additions & 0 deletions components/calendar/utils.ts
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,
});
96 changes: 96 additions & 0 deletions components/calendar/widget.tsx
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;
4 changes: 2 additions & 2 deletions components/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ArticlesWidget from "@/components/articles/widget";
import useArticles from "@/hooks/use-articles";
import SpotifyWidget from "@/components/spotify/widget";
import WeatherWidget from "@/components/weather/widget";
import CalendarWidget from "@/components/calendar/widget";
import ClockWidget from "@/components/clock/widget";
import useStore from "@/hooks/use-store";
import { useUserPreferences } from "@/stores/user-preferences";
Expand All @@ -20,8 +21,6 @@ function Home({}: Props) {
if (!hasTakenTour) {
const config = driverObj(setHasTakenTour);
driver(config).drive();
} else {
console.log("Tour already taken");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasTakenTour]);
Expand All @@ -30,6 +29,7 @@ function Home({}: Props) {
<main className="grid w-full h-full grid-cols-8 grid-rows-6 gap-3 overflow-y-auto 2xl:gap-5 grow">
<WeatherWidget />
{!loading && <ArticlesWidget articles={articles} />}
<CalendarWidget />
<ClockWidget />
<SpotifyWidget />
</main>
Expand Down
29 changes: 29 additions & 0 deletions components/ui/popover.tsx
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 }
2 changes: 1 addition & 1 deletion extension/404.html
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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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>
Loading

0 comments on commit 6a92cd1

Please sign in to comment.