mirror of
https://github.com/fergalmoran/radio-otherway.git
synced 2025-12-22 09:50:29 +00:00
Fix calendar caching
This commit is contained in:
4
.github/workflows/scheduler-cache-events.yml
vendored
4
.github/workflows/scheduler-cache-events.yml
vendored
@@ -9,4 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cache outstanding events
|
||||
run: curl -X GET https://otherway.fergl.ie/api/cron/cache
|
||||
uses: wei/curl@master
|
||||
with:
|
||||
args: https://otherway.fergl.ie/api/cron/cache
|
||||
|
||||
@@ -9,4 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cache outstanding events
|
||||
run: curl -X GET https://otherway.fergl.ie/api/cron/reminders
|
||||
uses: wei/curl@master
|
||||
with:
|
||||
args: https://otherway.fergl.ie/api/cron/reminders
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { themeChange } from "theme-change";
|
||||
import "./globals.css";
|
||||
import { Raleway } from "@next/font/google";
|
||||
import { NavBar } from "@/components/layout";
|
||||
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { defaults } from "@/lib/constants";
|
||||
|
||||
const font = Raleway({
|
||||
weight: ["400", "700"],
|
||||
@@ -19,7 +19,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
themeChange(false);
|
||||
const theme = localStorage.getItem("theme") || defaults.defaultTheme;
|
||||
if (theme && !document.body.dataset.theme) {
|
||||
document.body.dataset.theme = theme;
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<html lang="en">
|
||||
@@ -28,11 +31,9 @@ export default function RootLayout({
|
||||
<Toaster />
|
||||
<AuthUserProvider>
|
||||
<div className="flex flex-col min-h-screen bg-base-100">
|
||||
<div className="sticky top-0 z-30 flex justify-center flex-none w-full h-16 transition-all duration-100 bg-opacity-90 text-primary-content backdrop-blur">
|
||||
<NavBar />
|
||||
</div>
|
||||
<div className="-mt-[4rem] grow place-items-center items-end bg-gradient-to-br from-primary to-secondary pt-20 text-primary-content ">
|
||||
<main className="text-base-content">{children}</main>
|
||||
<div className="items-end grow place-items-center bg-base-200 text-primary-content">
|
||||
<main className=" text-base-content">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthUserProvider>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
const Loading = () => {
|
||||
|
||||
return (
|
||||
<div role="status">
|
||||
<svg
|
||||
|
||||
@@ -2,12 +2,11 @@ import React from "react";
|
||||
import HomePageComponent from "@/components/pages/home";
|
||||
|
||||
const getData = async () => {
|
||||
// const res = await fetch(
|
||||
// `${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
|
||||
// );
|
||||
const res = await fetch(
|
||||
"https://otherway.dev.fergl.ie:3000/api/shows/upcoming"
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
|
||||
@@ -4,14 +4,8 @@ import Link from "next/link";
|
||||
import { useAuthUserContext } from "@/lib/auth/authUserContext";
|
||||
import Image from "next/image";
|
||||
import { LogIn, LogOut, PlusSquare, Menu, User } from "react-feather";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ThemeSelector } from "../widgets/ui/themes";
|
||||
|
||||
const ThemeToggle = dynamic(
|
||||
() => import("@/components/widgets/ui/theme/ThemeToggle"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
const Navbar = () => {
|
||||
const { profile, loading, logOut } = useAuthUserContext();
|
||||
const NavMenu = profile ? (
|
||||
@@ -64,14 +58,14 @@ const Navbar = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="w-full mb-2 navbar">
|
||||
<nav className="w-full navbar bg-secondary-content text-accent-focus">
|
||||
<Link href="/">
|
||||
<Image src="/logo.png" alt="Otherway" width={48} height={48} />
|
||||
<Image src="/logo.png" alt="Otherway" width={42} height={42} />
|
||||
</Link>
|
||||
<div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row md:space-x-10">
|
||||
{!loading && NavMenu}
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
<ThemeSelector />
|
||||
|
||||
<div className="ml-auto lg:hidden">
|
||||
<div className="dropdown-end dropdown" data-cy="dropdown">
|
||||
|
||||
@@ -6,6 +6,9 @@ import db, { users } from "@/lib/db";
|
||||
import { debug } from "console";
|
||||
import { doc, setDoc } from "firebase/firestore";
|
||||
import React from "react";
|
||||
import ToastService from "@/components/widgets/toast";
|
||||
import logger from "@/lib/util/logging";
|
||||
|
||||
const ProfilePageComponentProfile = () => {
|
||||
const { loading, profile } = useAuthUserContext();
|
||||
const [sendReminders, setSendReminders] = React.useState(false);
|
||||
@@ -25,6 +28,7 @@ const ProfilePageComponentProfile = () => {
|
||||
}, [profile]);
|
||||
const _submitProfileForm = async ($event: React.SyntheticEvent) => {
|
||||
$event.preventDefault();
|
||||
try {
|
||||
const result = await setDoc(
|
||||
doc(users, profile?.id),
|
||||
Object.assign(
|
||||
@@ -33,12 +37,17 @@ const ProfilePageComponentProfile = () => {
|
||||
email,
|
||||
displayName,
|
||||
about: about || "",
|
||||
lastSeen: new Date(),
|
||||
lastSeen: new Date()
|
||||
}
|
||||
),
|
||||
{ merge: true }
|
||||
);
|
||||
console.log("ProfilePageComponentProfile", "_submitProfileForm", result);
|
||||
ToastService.success("Successfully updated your profile", "Success");
|
||||
} catch (err) {
|
||||
logger.error("ProfilePageComponentProfile", "_submitProfileForm", err);
|
||||
ToastService.error("Failed to update your profile.");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form className="space-y-8 divide-y" onSubmit={_submitProfileForm}>
|
||||
@@ -106,10 +115,6 @@ const ProfilePageComponentProfile = () => {
|
||||
updateFormValue={(v) => setAbout(v)}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<p className="mt-2 text-sm ">
|
||||
Write a few sentences about yourself.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +131,8 @@ const ProfilePageComponentProfile = () => {
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<path
|
||||
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -3,29 +3,29 @@ import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||
import { Show } from "@/models";
|
||||
import React from "react";
|
||||
import { MdAddAlarm } from "react-icons/md";
|
||||
import { error, success, warning } from "./toast/toastService";
|
||||
import ToastService from "./toast/toastService";
|
||||
|
||||
const RemindMeButton = ({ showId }: { showId: string }) => {
|
||||
const { profile } = useFirebaseAuth();
|
||||
const createShowReminder = async () => {
|
||||
if (profile?.id) {
|
||||
var response = await fetch(
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: profile?.id,
|
||||
showId: profile?.id,
|
||||
}),
|
||||
showId: profile?.id
|
||||
})
|
||||
}
|
||||
);
|
||||
if (response.status === 201) {
|
||||
success("Reminder created successfully");
|
||||
ToastService.success("Reminder created successfully");
|
||||
} else {
|
||||
error("Unable to create reminder at this time");
|
||||
ToastService.error("Unable to create reminder at this time");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { error, success, warning } from "./toastService";
|
||||
import ToastService from "./toastService";
|
||||
|
||||
export { success, warning, error };
|
||||
export default ToastService;
|
||||
|
||||
@@ -4,12 +4,12 @@ import ToastComponent, { ToastType } from "./ToastComponent";
|
||||
import {
|
||||
RiAlarmWarningLine,
|
||||
RiErrorWarningLine,
|
||||
RiShieldCheckLine
|
||||
RiShieldCheckLine,
|
||||
} from "react-icons/ri";
|
||||
|
||||
const success = (message: string, title?: string) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
const ToastService = {
|
||||
success: (message: string, title?: string) => {
|
||||
toast.custom((t) => (
|
||||
<ToastComponent
|
||||
title={title ?? "Success"}
|
||||
body={message}
|
||||
@@ -19,12 +19,10 @@ const success = (message: string, title?: string) => {
|
||||
toast.dismiss(t.id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
const warning = (message: string, title?: string) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
));
|
||||
},
|
||||
warning: (message: string, title?: string) => {
|
||||
toast.custom((t) => (
|
||||
<ToastComponent
|
||||
title={title ?? "Warning"}
|
||||
body={message}
|
||||
@@ -34,12 +32,10 @@ const warning = (message: string, title?: string) => {
|
||||
toast.dismiss(t.id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
const error = (message: string, title?: string) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
));
|
||||
},
|
||||
error: (message: string, title?: string) => {
|
||||
toast.custom((t) => (
|
||||
<ToastComponent
|
||||
title={title ?? "Error"}
|
||||
body={message}
|
||||
@@ -49,7 +45,7 @@ const error = (message: string, title?: string) => {
|
||||
toast.dismiss(t.id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
));
|
||||
},
|
||||
};
|
||||
export { success, warning, error };
|
||||
export default ToastService;
|
||||
|
||||
83
src/components/widgets/ui/themes/ThemeSelector.tsx
Normal file
83
src/components/widgets/ui/themes/ThemeSelector.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { themes } from "./themes";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IoColorPaletteOutline } from "react-icons/io5";
|
||||
import { BiDownArrow } from "react-icons/bi";
|
||||
import { defaults } from "@/lib/constants";
|
||||
|
||||
const ThemeSelector = () => {
|
||||
const [activeTheme, setActiveTheme] = useState(
|
||||
document?.body.dataset.theme ||
|
||||
localStorage.getItem("theme") ||
|
||||
defaults.defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (document) {
|
||||
document.body.dataset.theme = activeTheme;
|
||||
window.localStorage.setItem("theme", activeTheme);
|
||||
}
|
||||
}, [activeTheme]);
|
||||
const _switchTheme = (theme: string) => {
|
||||
setActiveTheme(theme);
|
||||
const elem = document.activeElement as HTMLElement;
|
||||
elem?.blur();
|
||||
};
|
||||
return (
|
||||
<div title="Change Theme" className="dropdown-end dropdown">
|
||||
<div tabIndex={0} className="gap-1 normal-case btn-ghost btn">
|
||||
<IoColorPaletteOutline className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6" />
|
||||
|
||||
<span className="hidden md:inline">Theme</span>
|
||||
<BiDownArrow className="hidden w-3 h-3 ml-1 fill-current opacity-60 sm:inline-block" />
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-content rounded-t-box rounded-b-box top-px h-[70vh] max-h-96 w-52 overflow-y-auto bg-base-200 text-base-content shadow-2xl">
|
||||
<div className="grid grid-cols-1 gap-3 p-3" tabIndex={0}>
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
className="overflow-hidden text-left rounded-lg outline-base-content"
|
||||
onClick={() => _switchTheme(theme.id)}
|
||||
>
|
||||
<div
|
||||
data-theme={theme.id}
|
||||
className="w-full font-sans cursor-pointer bg-base-100 text-base-content"
|
||||
>
|
||||
<div className="grid grid-cols-5 grid-rows-3">
|
||||
<div className="flex items-center col-span-5 row-span-3 row-start-1 gap-2 px-4 py-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="invisible w-3 h-3"
|
||||
>
|
||||
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z" />
|
||||
</svg>
|
||||
<div className="flex-grow text-sm font-bold">
|
||||
{theme.id}
|
||||
</div>
|
||||
<div className="flex flex-wrap flex-shrink-0 h-full gap-1">
|
||||
<div className="w-2 rounded bg-primary">
|
||||
<div className="w-2 rounded bg-secondary">
|
||||
<div className="w-2 rounded bg-accent">
|
||||
<div className="w-2 rounded bg-neutral"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
4
src/components/widgets/ui/themes/index.ts
Normal file
4
src/components/widgets/ui/themes/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
|
||||
export { ThemeSelector, ThemeToggle };
|
||||
118
src/components/widgets/ui/themes/themes.ts
Normal file
118
src/components/widgets/ui/themes/themes.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
export let themes = [
|
||||
{
|
||||
name: "🌝 light",
|
||||
id: "light",
|
||||
},
|
||||
{
|
||||
name: "🌚 dark",
|
||||
id: "dark",
|
||||
},
|
||||
{
|
||||
name: "🧁 cupcake",
|
||||
id: "cupcake",
|
||||
},
|
||||
{
|
||||
name: "🐝 bumblebee",
|
||||
id: "bumblebee",
|
||||
},
|
||||
{
|
||||
name: "✳️ Emerald",
|
||||
id: "emerald",
|
||||
},
|
||||
{
|
||||
name: "🏢 Corporate",
|
||||
id: "corporate",
|
||||
},
|
||||
{
|
||||
name: "🌃 synthwave",
|
||||
id: "synthwave",
|
||||
},
|
||||
{
|
||||
name: "👴 retro",
|
||||
id: "retro",
|
||||
},
|
||||
{
|
||||
name: "🤖 cyberpunk",
|
||||
id: "cyberpunk",
|
||||
},
|
||||
{
|
||||
name: "🌸 valentine",
|
||||
id: "valentine",
|
||||
},
|
||||
{
|
||||
name: "🎃 halloween",
|
||||
id: "halloween",
|
||||
},
|
||||
{
|
||||
name: "🌷 garden",
|
||||
id: "garden",
|
||||
},
|
||||
{
|
||||
name: "🌲 forest",
|
||||
id: "forest",
|
||||
},
|
||||
{
|
||||
name: "🐟 aqua",
|
||||
id: "aqua",
|
||||
},
|
||||
{
|
||||
name: "👓 lofi",
|
||||
id: "lofi",
|
||||
},
|
||||
{
|
||||
name: "🖍 pastel",
|
||||
id: "pastel",
|
||||
},
|
||||
{
|
||||
name: "🧚♀️ fantasy",
|
||||
id: "fantasy",
|
||||
},
|
||||
{
|
||||
name: "📝 Wireframe",
|
||||
id: "wireframe",
|
||||
},
|
||||
{
|
||||
name: "🏴 black",
|
||||
id: "black",
|
||||
},
|
||||
{
|
||||
name: "💎 luxury",
|
||||
id: "luxury",
|
||||
},
|
||||
{
|
||||
name: "🧛♂️ dracula",
|
||||
id: "dracula",
|
||||
},
|
||||
{
|
||||
name: "🖨 CMYK",
|
||||
id: "cmyk",
|
||||
},
|
||||
{
|
||||
name: "🍁 Autumn",
|
||||
id: "autumn",
|
||||
},
|
||||
{
|
||||
name: "💼 Business",
|
||||
id: "business",
|
||||
},
|
||||
{
|
||||
name: "💊 Acid",
|
||||
id: "acid",
|
||||
},
|
||||
{
|
||||
name: "🍋 Lemonade",
|
||||
id: "lemonade",
|
||||
},
|
||||
{
|
||||
name: "🌙 Night",
|
||||
id: "night",
|
||||
},
|
||||
{
|
||||
name: "☕️ Coffee",
|
||||
id: "coffee",
|
||||
},
|
||||
{
|
||||
name: "❄️ Winter",
|
||||
id: "winter",
|
||||
},
|
||||
];
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
TwitterAuthProvider,
|
||||
UserCredential,
|
||||
UserCredential
|
||||
} from "firebase/auth";
|
||||
import { app } from "./firebase";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Profile } from "@/models";
|
||||
import { users } from "../db";
|
||||
import { doc, setDoc } from "firebase/firestore";
|
||||
import { doc, getDoc, setDoc } from "firebase/firestore";
|
||||
import { servicesVersion } from "@ts-morph/common/lib/typescript";
|
||||
|
||||
export default function useFirebaseAuth() {
|
||||
const [profile, setProfile] = useState<Profile | undefined>();
|
||||
@@ -26,30 +27,30 @@ export default function useFirebaseAuth() {
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const getUserProfile = useCallback(() => {
|
||||
const getUserProfile = useCallback(async () => {
|
||||
if (auth.currentUser !== null) {
|
||||
// The user object has basic properties such as display name, email, etc.
|
||||
// Going forward we may look this up from the user table in firestore
|
||||
const profile = new Profile(
|
||||
const savedProfileRef = await getDoc(doc(users, auth.currentUser.uid));
|
||||
const savedProfile = savedProfileRef.data();
|
||||
const profile: Profile = new Profile(
|
||||
auth.currentUser.uid,
|
||||
auth.currentUser.email,
|
||||
auth.currentUser.displayName,
|
||||
auth.currentUser.photoURL,
|
||||
auth.currentUser.emailVerified,
|
||||
new Date()
|
||||
(savedProfile?.email || auth.currentUser.email) as string,
|
||||
(savedProfile?.displayName || auth.currentUser.email) as string,
|
||||
(savedProfile?.photoURL || auth.currentUser.email) as string,
|
||||
savedProfile?.about as string
|
||||
);
|
||||
setProfile(profile);
|
||||
setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
|
||||
merge: true,
|
||||
await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
|
||||
merge: true
|
||||
});
|
||||
return profile;
|
||||
}
|
||||
}, [auth.currentUser]);
|
||||
const authStateChanged = useCallback(
|
||||
(user: any) => {
|
||||
const authStateChanged = useCallback(async (user: any) => {
|
||||
if (user) {
|
||||
setLoading(true);
|
||||
const profile = getUserProfile();
|
||||
const profile = await getUserProfile();
|
||||
setProfile(profile);
|
||||
}
|
||||
setLoading(false);
|
||||
@@ -108,7 +109,7 @@ export default function useFirebaseAuth() {
|
||||
const result = await _processSignIn(provider);
|
||||
if (result) {
|
||||
const credential = GoogleAuthProvider.credentialFromResult(result);
|
||||
const profile = getUserProfile();
|
||||
const profile = await getUserProfile();
|
||||
setProfile(profile);
|
||||
router.push("/");
|
||||
}
|
||||
@@ -118,7 +119,7 @@ export default function useFirebaseAuth() {
|
||||
const result = await _processSignIn(provider);
|
||||
if (result) {
|
||||
const credential = TwitterAuthProvider.credentialFromResult(result);
|
||||
const profile = getUserProfile();
|
||||
const profile = await getUserProfile();
|
||||
setProfile(profile);
|
||||
router.push("/");
|
||||
}
|
||||
@@ -142,6 +143,6 @@ export default function useFirebaseAuth() {
|
||||
signInWithGoogle,
|
||||
signInWithTwitter,
|
||||
signInWithFacebook,
|
||||
getUserProfile,
|
||||
getUserProfile
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
WithFieldValue,
|
||||
QueryDocumentSnapshot,
|
||||
SnapshotOptions,
|
||||
Timestamp,
|
||||
Timestamp
|
||||
} from "firebase/firestore";
|
||||
|
||||
const firebaseConfig = {
|
||||
@@ -17,7 +17,7 @@ const firebaseConfig = {
|
||||
storageBucket: "radio-otherway.appspot.com",
|
||||
messagingSenderId: "47147490249",
|
||||
appId: "1:47147490249:web:a84515b3ce1c481826e618",
|
||||
measurementId: "G-12YB78EZM4",
|
||||
measurementId: "G-12YB78EZM4"
|
||||
};
|
||||
export const firebaseApp = initializeApp(firebaseConfig);
|
||||
const firestore = getFirestore();
|
||||
@@ -30,7 +30,7 @@ const showConverter = {
|
||||
...show,
|
||||
date: show.date
|
||||
? Timestamp.fromDate(new Date(show.date as string))
|
||||
: new Date(),
|
||||
: new Date()
|
||||
};
|
||||
},
|
||||
fromFirestore(
|
||||
@@ -39,7 +39,7 @@ const showConverter = {
|
||||
): Show {
|
||||
const data = snapshot.data(options)!;
|
||||
return new Show(snapshot.id, data.title, data.date.toDate(), data.creator);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Import all your model types
|
||||
@@ -52,4 +52,7 @@ export const shows =
|
||||
export const reminders = createCollection<Reminder>("reminders");
|
||||
export const remindersProcessed =
|
||||
createCollection<RemindersProcessed>("reminders");
|
||||
|
||||
|
||||
export default firestore;
|
||||
export { createCollection };
|
||||
|
||||
26
src/lib/db/settings.ts
Normal file
26
src/lib/db/settings.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Show } from "@/models";
|
||||
import { createCollection } from "@/lib/db/index";
|
||||
import { doc, query, setDoc, where } from "@firebase/firestore";
|
||||
import { getDoc } from "firebase/firestore";
|
||||
|
||||
interface Setting {
|
||||
value: string;
|
||||
updated: Date;
|
||||
}
|
||||
|
||||
const settingsCollection = createCollection<Setting>("settings");
|
||||
|
||||
const Settings = {
|
||||
read: async (key: string): Promise<string | undefined> => {
|
||||
const value = (await getDoc(doc(settingsCollection, key))).data();
|
||||
return value?.value || undefined;
|
||||
},
|
||||
write: async (key: string, value: string) => {
|
||||
await setDoc(doc(settingsCollection, key), {
|
||||
value,
|
||||
updated: new Date()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
22
src/lib/util/google/calendar.ts
Normal file
22
src/lib/util/google/calendar.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const { google } = require("googleapis");
|
||||
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
|
||||
const GOOGLE_CLIENT_EMAIL =
|
||||
process.env.GOOGLE_CALENDAR_CREDENTIALS_CLIENT_EMAIL;
|
||||
const GOOGLE_PROJECT_NUMBER = process.env.GOOGLE_CALENDAR_PROJECT_ID;
|
||||
const GOOGLE_CALENDAR_ID = process.env.GOOGLE_CALENDAR_ID;
|
||||
const SCOPES = ["https://www.googleapis.com/auth/calendar"];
|
||||
|
||||
const jwtClient = new google.auth.JWT(
|
||||
GOOGLE_CLIENT_EMAIL,
|
||||
null,
|
||||
GOOGLE_PRIVATE_KEY,
|
||||
SCOPES
|
||||
);
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
project: GOOGLE_PROJECT_NUMBER,
|
||||
auth: jwtClient
|
||||
});
|
||||
|
||||
export default calendar;
|
||||
export {GOOGLE_CALENDAR_ID}
|
||||
@@ -1,44 +1,31 @@
|
||||
import { Show } from "@/models";
|
||||
import calendar, { GOOGLE_CALENDAR_ID } from "@/lib/util/google/calendar";
|
||||
import logger from "../logging";
|
||||
|
||||
const { google } = require("googleapis");
|
||||
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
|
||||
const GOOGLE_CLIENT_EMAIL =
|
||||
process.env.GOOGLE_CALENDAR_CREDENTIALS_CLIENT_EMAIL;
|
||||
const GOOGLE_PROJECT_NUMBER = process.env.GOOGLE_CALENDAR_PROJECT_ID;
|
||||
const GOOGLE_CALENDAR_ID = process.env.GOOGLE_CALENDAR_ID;
|
||||
const SCOPES = ["https://www.googleapis.com/auth/calendar"];
|
||||
|
||||
const jwtClient = new google.auth.JWT(
|
||||
GOOGLE_CLIENT_EMAIL,
|
||||
null,
|
||||
GOOGLE_PRIVATE_KEY,
|
||||
SCOPES
|
||||
);
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
project: GOOGLE_PROJECT_NUMBER,
|
||||
auth: jwtClient
|
||||
});
|
||||
const getCalendarEntries = async () => {
|
||||
const getCalendarEntries = async (syncToken?: string) => {
|
||||
try {
|
||||
const events = await calendar.events.list({
|
||||
const e = await calendar.events.list({
|
||||
calendarId: GOOGLE_CALENDAR_ID,
|
||||
timeMin: new Date().toISOString(),
|
||||
maxResults: 10,
|
||||
singleEvents: true,
|
||||
orderBy: "startTime"
|
||||
syncToken: syncToken
|
||||
});
|
||||
return events.data.items.map((r: any) => {
|
||||
return {
|
||||
const events = _mapEvents(e);
|
||||
return events;
|
||||
} catch (err) {
|
||||
logger.error("calendarReader", "Unable to read events", err);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const _mapEvents = (events: any) => {
|
||||
const mapped = events.data.items.map((r: any) => ({
|
||||
id: r.id,
|
||||
title: r.summary,
|
||||
date: r.start.dateTime,
|
||||
creator: r.creator.email
|
||||
}));
|
||||
return {
|
||||
syncToken: events.data.nextSyncToken,
|
||||
events: mapped
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export { getCalendarEntries };
|
||||
|
||||
14
src/lib/util/google/calendarWatcher.ts
Normal file
14
src/lib/util/google/calendarWatcher.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import calendar, { GOOGLE_CALENDAR_ID } from "@/lib/util/google/calendar";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
|
||||
const setupCalendarWebhook = async () => {
|
||||
calendar.events.watch({
|
||||
resource: {
|
||||
id: uuidv4(),
|
||||
type: "web_hook",
|
||||
address: `https://external.fergl.ie/api/shows/calendar`
|
||||
},
|
||||
calendarId: GOOGLE_CALENDAR_ID
|
||||
});
|
||||
};
|
||||
export { setupCalendarWebhook };
|
||||
@@ -3,22 +3,23 @@ export default class Profile {
|
||||
email: string | null;
|
||||
displayName: string | null;
|
||||
photoURL: string | null;
|
||||
emailVerified: boolean;
|
||||
emailVerified: boolean = false;
|
||||
about?: String;
|
||||
lastSeen: Date;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
email: string | null,
|
||||
displayName: string | null,
|
||||
photoURL: string | null,
|
||||
emailVerified: boolean,
|
||||
about?: string,
|
||||
lastSeen?: Date
|
||||
) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.displayName = displayName;
|
||||
this.photoURL = photoURL;
|
||||
this.emailVerified = emailVerified;
|
||||
this.about = about || "";
|
||||
|
||||
this.lastSeen = lastSeen || new Date();
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@ import logger from "@/lib/util/logging";
|
||||
import { doc, setDoc } from "@firebase/firestore";
|
||||
import { shows } from "@/lib/db";
|
||||
import { Show } from "@/models";
|
||||
import Settings from "@/lib/db/settings";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const e = await getCalendarEntries();
|
||||
const entries = e.map((r: any) => Show.fromJson(r));
|
||||
const syncToken = await Settings.read("CalendarSyncToken");
|
||||
const e = await getCalendarEntries(syncToken);
|
||||
if (!e?.events) {
|
||||
res.status(204).json({ result: "No calendar entries found" });
|
||||
} else {
|
||||
const entries = e?.events.map((r: any) => Show.fromJson(r));
|
||||
for (const entry of entries) {
|
||||
logger.debug("Storing show", entry);
|
||||
const showRef = doc(shows, entry.id);
|
||||
@@ -19,10 +24,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}, { merge: true });
|
||||
}
|
||||
logger.debug("Stored show", res);
|
||||
if (e?.syncToken) {
|
||||
await Settings.write("CalendarSyncToken", e?.syncToken);
|
||||
}
|
||||
res.status(200).json({ status: "OK", entries });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
res.status(500).json({status: "Error"});
|
||||
res.status(500).json({ status: "Error" });
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {NextApiRequest, NextApiResponse} from "next";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.status(200).json({hello: process.env});
|
||||
await setupCalendarWebhook();
|
||||
res.status(200).json({ hello: process.env });
|
||||
};
|
||||
export default handler;
|
||||
|
||||
33
src/pages/api/shows/calendar.ts
Normal file
33
src/pages/api/shows/calendar.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { doc, getDocs, query, setDoc, where } from "@firebase/firestore";
|
||||
import { shows } from "@/lib/db";
|
||||
import logger from "@/lib/util/logging";
|
||||
import { Show } from "@/models";
|
||||
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
|
||||
import Settings from "@/lib/db/settings";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const resourceId = req.headers["x-goog-resource-id"];
|
||||
const channelToken = req.headers["x-goog-channel-token"];
|
||||
const channelId = req.headers["x-goog-channel-id"];
|
||||
const resourceState = req.headers["x-goog-resource-state"];
|
||||
logger.debug("Webhook callback",
|
||||
resourceId,
|
||||
channelToken,
|
||||
channelId,
|
||||
resourceState
|
||||
);
|
||||
|
||||
const changed = await getCalendarEntries();
|
||||
const entries = changed?.events.map((r: any) => Show.fromJson(r));
|
||||
for (const entry of entries) {
|
||||
const showRef = doc(shows, entry.id);
|
||||
await setDoc(showRef, {
|
||||
title: entry.title,
|
||||
date: entry.date,
|
||||
creator: entry.creator
|
||||
}, { merge: true });
|
||||
}
|
||||
res.status(200).json({ result: "We got pinged" });
|
||||
res.end();
|
||||
};
|
||||
66
yarn.lock
66
yarn.lock
@@ -830,47 +830,47 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/parser@^5.42.0":
|
||||
version "5.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.53.0.tgz#a1f2b9ae73b83181098747e96683f1b249ecab52"
|
||||
integrity sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==
|
||||
version "5.54.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.54.0.tgz#def186eb1b1dbd0439df0dacc44fb6d8d5c417fe"
|
||||
integrity sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.53.0"
|
||||
"@typescript-eslint/types" "5.53.0"
|
||||
"@typescript-eslint/typescript-estree" "5.53.0"
|
||||
"@typescript-eslint/scope-manager" "5.54.0"
|
||||
"@typescript-eslint/types" "5.54.0"
|
||||
"@typescript-eslint/typescript-estree" "5.54.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.53.0":
|
||||
version "5.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef"
|
||||
integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==
|
||||
"@typescript-eslint/scope-manager@5.54.0":
|
||||
version "5.54.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.54.0.tgz#74b28ac9a3fc8166f04e806c957adb8c1fd00536"
|
||||
integrity sha512-VTPYNZ7vaWtYna9M4oD42zENOBrb+ZYyCNdFs949GcN8Miwn37b8b7eMj+EZaq7VK9fx0Jd+JhmkhjFhvnovhg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.53.0"
|
||||
"@typescript-eslint/visitor-keys" "5.53.0"
|
||||
"@typescript-eslint/types" "5.54.0"
|
||||
"@typescript-eslint/visitor-keys" "5.54.0"
|
||||
|
||||
"@typescript-eslint/types@5.53.0":
|
||||
version "5.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f"
|
||||
integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==
|
||||
"@typescript-eslint/types@5.54.0":
|
||||
version "5.54.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.54.0.tgz#7d519df01f50739254d89378e0dcac504cab2740"
|
||||
integrity sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.53.0":
|
||||
version "5.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690"
|
||||
integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==
|
||||
"@typescript-eslint/typescript-estree@5.54.0":
|
||||
version "5.54.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.54.0.tgz#f6f3440cabee8a43a0b25fa498213ebb61fdfe99"
|
||||
integrity sha512-X2rJG97Wj/VRo5YxJ8Qx26Zqf0RRKsVHd4sav8NElhbZzhpBI8jU54i6hfo9eheumj4oO4dcRN1B/zIVEqR/MQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.53.0"
|
||||
"@typescript-eslint/visitor-keys" "5.53.0"
|
||||
"@typescript-eslint/types" "5.54.0"
|
||||
"@typescript-eslint/visitor-keys" "5.54.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.53.0":
|
||||
version "5.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f"
|
||||
integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==
|
||||
"@typescript-eslint/visitor-keys@5.54.0":
|
||||
version "5.54.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.54.0.tgz#846878afbf0cd67c19cfa8d75947383d4490db8f"
|
||||
integrity sha512-xu4wT7aRCakGINTLGeyGqDn+78BwFlggwBjnHa1ar/KaGagnmwLYmlrXIrgAaQ3AE1Vd6nLfKASm7LrFHNbKGA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.53.0"
|
||||
"@typescript-eslint/types" "5.54.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@upstash/qstash@^0.3.6":
|
||||
@@ -1409,9 +1409,9 @@ csstype@^3.0.2:
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
|
||||
daisyui@^2.49.0:
|
||||
version "2.51.1"
|
||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-2.51.1.tgz#20fe01f3f3caa8628ebb30fcc1b99341183d1702"
|
||||
integrity sha512-rMtdB8Rh8Ghd3+FDlMe63Caw+IopKIf7tviJGEx8gkjj5H+RCvU1bEbjx6DHsMsu477Gv5aafrLIdoUL+iMRrw==
|
||||
version "2.51.2"
|
||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-2.51.2.tgz#831440e02ea55f773f519a69b7f746824f4f5f84"
|
||||
integrity sha512-Jnfknn9HrOBNzj1kUI9g/kqNSA5oP5B/r6X4Ff/cdMO27glisO2Oi9QAO8hKwMN9LzfWGdfyqvJGvc3Ji78CXA==
|
||||
dependencies:
|
||||
color "^4.2"
|
||||
css-selector-tokenizer "^0.8.0"
|
||||
@@ -1573,9 +1573,9 @@ ee-first@1.1.1:
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.4.284:
|
||||
version "1.4.311"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz#953bc9a4767f5ce8ec125f9a1ad8e00e8f67e479"
|
||||
integrity sha512-RoDlZufvrtr2Nx3Yx5MB8jX3aHIxm8nRWPJm3yVvyHmyKaRvn90RjzB6hNnt0AkhS3IInJdyRfQb4mWhPvUjVw==
|
||||
version "1.4.312"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.312.tgz#e70a5b46252814ffc576b2c29032e1a559b9ad53"
|
||||
integrity sha512-e7g+PzxzkbiCD1aNhdj+Tx3TLlfrQF/Lf+LAaUdoLvB1kCxf9wJimqXdWEqnoiYjFtxIR1hGBmoHsBIcCBNOMA==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
|
||||
Reference in New Issue
Block a user