Firebase all the things

This commit is contained in:
Fergal Moran
2023-02-23 04:49:21 +00:00
parent eb63e47b60
commit 0f08aa5581
41 changed files with 1305 additions and 174 deletions

2
.env
View File

@@ -1,7 +1,7 @@
QSTASH_CURRENT_SIGNING_KEY=khs3lpVBv1QtV/L9MTdXlcnoI8tTlg0aDfrFz+o8utA=
#auth
GOOGLE_APPLICATION_CREDENTIALS=radio-otherway-service-account.json
GOOGLE_APPLICATION_CREDENTIALS=serviceAccount.json
#calendar api
GOOGLE_CALENDAR_PROJECT_ID=47147490249

5
.firebaserc Normal file
View File

@@ -0,0 +1,5 @@
{
"projects": {
"default": "radio-otherway"
}
}

View File

@@ -1,12 +1,12 @@
name:
Cache Jobs
Cache Events
on:
# Every 5 minutes
schedule:
- cron: "*/5 * * * *"
jobs:
cron:
cache:
runs-on: ubuntu-latest
steps:
- name: Cache outstanding events
run: curl -X GET https://external.dev.fergl.ie:3000/api/cron/cache
run: curl -X GET https://external.fergl.ie/api/cron/cache

View File

@@ -0,0 +1,12 @@
name:
Send Reminders
on:
# Every 5 minutes
schedule:
- cron: "*/5 * * * *"
jobs:
reminders:
runs-on: ubuntu-latest
steps:
- name: Cache outstanding events
run: curl -X GET https://external.fergl.ie/api/cron/reminders

1
.gitignore vendored
View File

@@ -38,3 +38,4 @@ next-env.d.ts
.private/
radio-otherway-service-account.json
serviceAccount.json

59
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,59 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_KEEP_WHITESPACES_INSIDE" value="" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

1
.idea/web.iml generated
View File

@@ -5,6 +5,7 @@
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/.yarn" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

6
firebase.json Normal file
View File

@@ -0,0 +1,6 @@
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}

4
firestore.indexes.json Normal file
View File

@@ -0,0 +1,4 @@
{
"indexes": [],
"fieldOverrides": []
}

8
firestore.rules Normal file
View File

@@ -0,0 +1,8 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

View File

@@ -3,13 +3,15 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "node ./server.js",
"dev": "NODE_OPTIONS='-r next-logger' node ./server.js",
"debug": "node ./server.js",
"dev-nossl": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^1.7.11",
"@next/font": "13.1.5",
"@prisma/client": "^4.9.0",
"@types/node": "18.11.18",
@@ -23,9 +25,14 @@
"firebase": "^9.17.1",
"firebase-admin": "^11.5.0",
"next": "13.1.5",
"next-logger": "^3.0.1",
"pino": "^8.11.0",
"pino-logflare": "^0.3.12",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.7.1",
"twilio": "^4.8.0",
"typescript": "4.9.4"
},
"packageManager": "yarn@1.22.19",

View File

@@ -1,28 +1,34 @@
"use client";
import React from 'react';
import React from "react";
import "./globals.css";
import {Inter} from "@next/font/google";
import {NavBar} from "@/components/layout";
import {AuthUserProvider} from "@/lib/auth/authUserContext";
const inter = Inter({subsets: ["latin"]});
import { Inter } from "@next/font/google";
import { NavBar } from "@/components/layout";
import { AuthUserProvider } from "@/lib/auth/authUserContext";
import dynamic from "next/dynamic";
import { Toaster } from "react-hot-toast";
// const Toaster = dynamic(
// () => import("react-hot-toast").then((c) => c.Toaster),
// {
// ssr: false,
// }
// );
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({
children,
}: {
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" data-theme="bumblebee">
<head/>
<body className={`${inter.className} h-screen`}>
<AuthUserProvider>
<NavBar/>
<div className="-mt-[4rem]">
{children}
</div>
</AuthUserProvider>
</body>
<head />
<body className={`${inter.className} h-screen`}>
<AuthUserProvider>
<Toaster />
<NavBar />
<div className="-mt-[4rem]">{children}</div>
</AuthUserProvider>
</body>
</html>
);
}

View File

@@ -1,48 +1,67 @@
const getData = async () => {
import RemindMeButton from "@/components/widgets/RemindMeButton";
import { getMonthName, getTime } from "@/lib/util/dateUtils";
import logger from "@/lib/util/logging";
import { Show } from "@/models";
const getData = async (): Promise<Show[]> => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
);
return res.json();
logger.debug("getDate", res);
return await res.json();
};
export default async function Home() {
const Home = async () => {
const results = await getData();
return results.message ? (
logger.debug("results", results);
return results.length === 0 ? (
<div className="min-h-screen hero bg-base-200">
<div className="text-center hero-content">
<div className="max-w-md">
<h1 className="text-5xl font-bold">{results.message}</h1>
<h1 className="text-5xl font-bold">No upcoming shows found</h1>
</div>
</div>
</div>
) : (
<div>
<h1 className="text-xl font-extrabold font-title text-primary-content md:text-2xl lg:text-4xl">
Upcoming Shows
</h1>
<div className="px-4 mt-4 overflow-x-auto">
<table className="table w-full">
<thead>
<tr>
<th>When</th>
<th>Who</th>
<th/>
</tr>
</thead>
<tbody>
{results && results.map((r: any) => (
<tr key={r.id}>
<td>{new Date(r.date).toLocaleString("en-IE")}</td>
<td>{r.title}</td>
<td>
<button className="btn">Remind me</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="container flex justify-center h-screen py-20 mx-auto">
<div className="flex flex-col w-6/12 h-full pl-4">
<div className="px-5 py-2 text-sm font-bold text-gray-500 bg-white border-b border-gray-300 shadow">
Tracking events
</div>
<div
className="w-full h-full overflow-auto bg-white shadow"
id="journal-scroll"
>
<table className="w-full">
<tbody>
{results &&
results.map((show: Show) => (
<tr
key={show.id}
className="relative py-1 transform scale-100 border-b-2 border-blue-100 cursor-default text-md"
>
<td className="pl-5 pr-3 whitespace-no-wrap">
<div className="text-gray-400">
{`${new Date(show.date).getDay()} ${getMonthName(show.date)}`}
</div>
<div>{getTime(show.date)}</div>
</td>
<td className="px-2 py-4 space-y-2 whitespace-no-wrap">
<div className="font-medium leading-5 text-gray-500">
{show.creator}
</div>
<div className="leading-5 text-gray-900">{show.title}</div>
</td>
<td className="px-2 py-4">
<RemindMeButton show={show} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
};
export default Home;

View File

@@ -12,8 +12,8 @@ const PrivacyComponent = () => {
rel="noreferrer">Free
Privacy Policy Generator</a>.</p>
<h1>Interpretation and Definitions</h1>
<h2>Interpretation</h2>
<p>The words of which the initial letter is capitalized have meanings defined under the following conditions. The
<h2>Interpretation</h2>m
<p>The words of which the initial letter is capitalized have eanings defined under the following conditions. The
following definitions shall have the same meaning regardless of whether they appear in singular or in
plural.</p>
<h2>Definitions</h2>

View File

@@ -0,0 +1,43 @@
"use client";
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";
const RemindMeButton = ({ show }: { show: Show }) => {
const { authUser } = useFirebaseAuth();
const createShowReminder = async () => {
if (authUser?.uid) {
var response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: authUser?.uid,
showId: show.id,
}),
}
);
if (response.status === 201) {
success("Reminder created successfully");
} else {
error("Unable to create reminder at this time");
}
}
};
return (
<button
className="gap-2 btn"
onClick={async () => await createShowReminder()}
>
<MdAddAlarm className="w-6 h-6" />
Remind Me!
</button>
);
};
export default RemindMeButton;

View File

@@ -1,4 +1,6 @@
import ErrorText from "./ErrorText";
import InputText from "./InputText";
import RemindMeButton from "./RemindMeButton";
import ToastService from "./toast/toastService";
export { ErrorText, InputText };
export { ErrorText, InputText, RemindMeButton, ToastService };

View File

@@ -0,0 +1,82 @@
import React from "react";
import {
RiAlarmWarningLine,
RiErrorWarningLine,
RiShieldCheckLine,
} from "react-icons/ri";
import { CgCloseO } from "react-icons/cg";
enum ToastType {
success,
warning,
error,
}
interface IToastComponentProps {
title: string;
body: string;
type: ToastType;
isVisible: boolean;
onToastClicked: () => void;
}
const ToastComponent = ({
title,
body,
type,
isVisible,
onToastClicked,
}: IToastComponentProps) => {
const _getToastIcon = (type: ToastType): React.ReactNode => {
switch (type) {
case ToastType.success:
return <RiShieldCheckLine className="w-10 h-10 text-white" />;
case ToastType.warning:
return <RiAlarmWarningLine className="w-10 h-10 text-white" />;
case ToastType.error:
return <RiErrorWarningLine className="w-10 h-10 text-white" />;
}
};
function _getBackgroundColour(type: ToastType) {
switch (type) {
case ToastType.success:
return "alert-success";
case ToastType.warning:
return "alert-warning";
case ToastType.error:
return "alert-error";
}
}
return (
<div className="flex flex-col items-center space-y-4 w-96 text-base-content sm:items-end">
<div
className={`pointer-events-auto flex w-full max-w-md rounded-lg shadow-lg ${_getBackgroundColour(
type
)}`}
>
<div className="flex-1 w-0 p-4">
<div className="flex items-start">
<div className="flex-shrink-0 pt-0.5">{_getToastIcon(type)}</div>
<div className="flex-1 w-0 ml-3">
<p className="text-sm font-medium ">{title}</p>
<p className="mt-1 text-sm ">{body}</p>
</div>
</div>
</div>
<div className="flex border-l border-gray-200">
<button
type="button"
title="Close"
className="flex items-center justify-center w-full p-4 text-sm font-medium border border-transparent rounded-none rounded-r-lg focus:outline-none focus:ring-0 focus:ring-offset-0"
onClick={() => onToastClicked()}
>
<CgCloseO className="w-8 h-8" />
</button>
</div>
</div>
</div>
);
};
export default ToastComponent;
export { ToastType };

View File

@@ -0,0 +1,55 @@
import toast from "react-hot-toast";
import ToastComponent, { ToastType } from "./ToastComponent";
import {
RiAlarmWarningLine,
RiErrorWarningLine,
RiShieldCheckLine
} from "react-icons/ri";
const success = (message: string, title?: string) => {
toast.custom(
(t) => (
<ToastComponent
title={title ?? "Success"}
body={message}
isVisible={t.visible}
type={ToastType.success}
onToastClicked={() => {
toast.dismiss(t.id);
}}
/>
)
);
};
const warning = (message: string, title?: string) => {
toast.custom(
(t) => (
<ToastComponent
title={title ?? "Warning"}
body={message}
isVisible={t.visible}
type={ToastType.warning}
onToastClicked={() => {
toast.dismiss(t.id);
}}
/>
)
);
};
const error = (message: string, title?: string) => {
toast.custom(
(t) => (
<ToastComponent
title={title ?? "Error"}
body={message}
type={ToastType.error}
isVisible={t.visible}
onToastClicked={() => {
toast.dismiss(t.id);
}}
/>
)
);
};
export { success, warning, error };

View File

@@ -1,6 +1,6 @@
import React, {createContext, useContext, Context} from 'react'
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import {User} from "@/lib/auth/user";
import {User} from "@/models";
interface IAuthUserContext {
authUser: User | undefined;

View File

@@ -10,9 +10,9 @@ import {
signOut,
TwitterAuthProvider
} from 'firebase/auth';
import {app} from './firebase';
import {User} from "@/lib/auth/user";
import {app} from '../db/firebaseAuth';
import {useRouter} from "next/navigation";
import {User} from "@/models";
const formatAuthUser = (user: User) => ({
uid: user.uid,

View File

@@ -1,4 +0,0 @@
export type User = {
uid: string;
email: string
}

26
src/lib/db/db Normal file
View File

@@ -0,0 +1,26 @@
import { firestore } from "firebase-admin";
// Import or define your types
// import { YourType } from '~/@types'
import { Show, Reminder, Notification } from "@/models";
import firebase from "firebase/compat";
import FirestoreDataConverter = firebase.firestore.FirestoreDataConverter;
const converter = <T>() => ({
toFirestore: (data: Partial<T>): FirebaseFirestore.DocumentData => data,
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot): T => snap.data
});
const dataPoint = <T>(collectionPath: string) => firestore()
.collection(collectionPath)
.withConverter(converter<T>());
const db = {
shows: dataPoint<Show>("shows"),
reminders: dataPoint<Reminder>("reminders")
};
export { db };
export default db;

View File

@@ -1,6 +1,6 @@
import firebase, {getApp, getApps, initializeApp} from 'firebase/app';
import 'firebase/auth';
import {getFirestore} from "firebase/firestore";
import firebase, { getApp, getApps, initializeApp } from "firebase/app";
import "firebase/auth";
// import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
@@ -9,7 +9,7 @@ const firebaseConfig = {
storageBucket: "radio-otherway.appspot.com",
messagingSenderId: "47147490249",
appId: "1:47147490249:web:a84515b3ce1c481826e618",
measurementId: "G-12YB78EZM4"
measurementId: "G-12YB78EZM4",
};
// export default admin.firestore();
export const app = initializeApp(firebaseConfig);

28
src/lib/db/index.ts Normal file
View File

@@ -0,0 +1,28 @@
import serviceAccount from "serviceAccount.json";
import { initializeApp } from "firebase/app";
import { getFirestore, CollectionReference, collection, DocumentData } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
authDomain: "radio-otherway.firebaseapp.com",
projectId: "radio-otherway",
storageBucket: "radio-otherway.appspot.com",
messagingSenderId: "47147490249",
appId: "1:47147490249:web:a84515b3ce1c481826e618",
measurementId: "G-12YB78EZM4"
};
export const firebaseApp = initializeApp(firebaseConfig);
const firestore = getFirestore();
const createCollection = <T = DocumentData>(collectionName: string) => {
return collection(firestore, collectionName) as CollectionReference<T>;
};
// Import all your model types
import { Show, Reminder } from "@/models";
// export all your collections
export const shows = createCollection<Show>("shows");
export const reminders = createCollection<Reminder>("reminders");
export default firestore;

19
src/lib/util/dateUtils.ts Normal file
View File

@@ -0,0 +1,19 @@
const getMonthName = (date: string) =>
new Date(date).toLocaleString("default", { month: "short" });
const getTime = (date: string) =>
new Date(date).toLocaleTimeString("en-IE", { timeStyle: "short" });
const getStartOfToday = (admin: any) => {
const now = new Date();
now.setHours(5, 0, 0, 0); // +5 hours for Eastern Time
const timestamp = admin.firestore.Timestamp.fromDate(now);
return timestamp; // ex. 1631246400
};
const dateDifferenceInSeconds = (date1: Date, date2: Date) =>
Math.abs((date1.getTime() - date2.getTime())) / 1000;
const addSeconds = (date: Date, seconds: number) => {
date.setSeconds(date.getSeconds() + seconds);
return date;
};
export { getMonthName, getTime, getStartOfToday, dateDifferenceInSeconds, addSeconds };

View File

@@ -0,0 +1,35 @@
import { Show } from "@/models";
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 () => {
try {
const events = await calendar.events.list({
calendarId: GOOGLE_CALENDAR_ID,
timeMin: new Date().toISOString(),
maxResults: 10,
singleEvents: true,
orderBy: "startTime",
});
return events.data.items;
} catch (err) {}
return null;
};
export { getCalendarEntries };

View File

@@ -0,0 +1,24 @@
import pino from 'pino'
import {logflarePinoVercel} from 'pino-logflare'
const {stream, send} = logflarePinoVercel({
apiKey: "MK3qgU_-pwHQ",
sourceToken: "f7d8c11d-8f36-4981-8168-bfd69aa72bbf"
});
// create pino logger
const logger = pino({
browser: {
transmit: {
level: "info",
send: send,
}
},
level: "debug",
base: {
env: process.env.NODE_ENV,
revision: process.env.VERCEL_GITHUB_COMMIT_SHA,
},
}, stream);
export default logger

View File

@@ -0,0 +1,16 @@
import logger from "@/lib/util/logging";
const sendSMS = async (number: string, body: string) => {
const twilio = require("twilio");
const client = require("twilio")(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);
const message = await client.messages
.create({
body: body,
to: number, // Text this number
from: process.env.TWILIO_FROM // From a valid Twilio number
});
logger.debug(`SMS sent to ${number}`, message);
};
export { sendSMS };

11
src/models/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import User from "./user";
import Show from "./show";
import Reminder from "./reminder";
import Notification from "./notification";
export {
User,
Show,
Reminder,
Notification
};

View File

@@ -0,0 +1,17 @@
export default class Notification {
secondsBefore: number;
destination: string;
constructor(secondsBefore: number,
destination: string) {
this.secondsBefore = secondsBefore;
this.destination = destination;
}
static fromJson(r: any): Notification {
return new Notification(
r.secondsBefore,
r.destination
);
}
};

26
src/models/reminder.ts Normal file
View File

@@ -0,0 +1,26 @@
import Notification from "./notification";
export default class Reminder {
userId: string;
showId: string;
notifications: Notification[];
created: Date;
constructor(userId: string,
showId: string,
notifications: [],
created: Date) {
this.userId = userId;
this.showId = showId;
this.notifications = notifications.map(n => Notification.fromJson(n));
this.created = created;
}
static fromJson(r: any): Reminder {
return new Reminder(
r.userId,
r.showId,
r.notifications,
r.created);
}
};

21
src/models/show.ts Normal file
View File

@@ -0,0 +1,21 @@
export default class Show {
id: string;
title: string;
date: Date;
creator: string;
constructor(id: string, title: string, date: string, creator: string) {
this.id = id;
this.title = title;
this.date = new Date(date);
this.creator = creator;
}
static fromJson(r: any): Show {
return new Show(
r.id,
r.title,
r.date,
r.creator);
}
};

9
src/models/user.ts Normal file
View File

@@ -0,0 +1,9 @@
export default class User {
uid: string;
email: string
constructor(uid: string, email: string) {
this.uid = uid;
this.email = email;
}
}

View File

@@ -1,6 +1,29 @@
import {NextApiRequest, NextApiResponse} from "next";
import { NextApiRequest, NextApiResponse } from "next";
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
import { shows } from "@/lib/db";
import { Show } from "@/models";
import logger from "@/lib/util/logging";
import { doc, setDoc } from "@firebase/firestore";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({hello: "sailor"});
try {
const entries = await getCalendarEntries();
const shows = entries.map((r: any) => Show.fromJson(r));
for (const show of shows) {
logger.debug("Storing show", show);
const showRef = doc(shows, show.id);
await setDoc(showRef, {
title: show.title,
date: show.date,
creator: show.creator
}, { merge: true });
}
logger.debug("Stored show", res);
res.status(200);
} catch (err) {
logger.error(err);
res.status(500);
}
res.end();
};
export default handler;

View File

@@ -0,0 +1,43 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { shows, reminders } from "@/lib/db";
import { addSeconds, dateDifferenceInSeconds } from "@/lib/util/dateUtils";
import { Notification, Reminder, Show } from "@/models";
import { sendSMS } from "@/lib/util/notifications/sms";
import { doc, getDocs, query, where } from "@firebase/firestore";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
//this handler is called by whatever CRON mechanism we're using
//it will run every 5 minutes to check
// if any shows are coming up and send out reminders.
//get all the shows that are on today
const upcoming = await getDocs(query(shows, where("date", ">", new Date())));
// const shows = await db.shows;
// .where("date", ">", new Date())
// .where("date", "<", addSeconds(new Date(), 60 * 60))
// .get()
// ;
for (const s of upcoming.docs) {
const show = s.data() as Show;
//load all the reminders for this show
const activeReminders = await getDocs(query(reminders, where("showId", "==", show.id)));
//this runs every 5 minutes so any shows that have a reminder
//due in the next 5 minutes should get queued
for (const r of activeReminders.docs) {
const reminder = r.data() as Reminder;
for (let n in reminder.notifications) {
const notification = Notification.fromJson(n);
const targetDate = addSeconds(new Date(), notification.secondsBefore * -1);
if (dateDifferenceInSeconds(targetDate, new Date(show.date)) <= 5 * 60) {
//time to fire off a notification
await sendSMS("353868065119", "New show starting in 1 hour");
}
}
}
}
res.status(200);
res.end();
};
export default handler;

View File

@@ -0,0 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.json({ ping: "ppong" });
};
export default handler;

View File

@@ -0,0 +1,26 @@
import { NextApiRequest, NextApiResponse } from "next";
import db from "@/lib/db";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
const { userId, showId } = req.body;
const docKey = `${userId}_${showId}`;
const remindersRef = db.collection("reminders");
const dbShow = remindersRef.doc(docKey);
const reminder = await remindersRef.doc(docKey).set({
userId,
showId,
created: new Date(),
notifications: [
{ secondsBefore: 60 * 60, destination: "353868065119" } //just set a single reminder for an hour beforehand
]
}, { merge: true });
res.status(201).json(reminder);
} else {
res.status(405);
}
res.end();
};
export default handler;

View File

@@ -1,58 +1,18 @@
// https://www.googleapis.com/calendar/v3/calendars/calendarId/events
import { NextApiRequest, NextApiResponse } from "next";
import db from "@/lib/db";
import {NextApiRequest, NextApiResponse} from "next";
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 function handler(req: NextApiRequest, res: NextApiResponse) {
calendar.events.list(
{
calendarId: GOOGLE_CALENDAR_ID,
timeMin: new Date().toISOString(),
maxResults: 10,
singleEvents: true,
orderBy: "startTime",
},
(error: any, result: any) => {
if (error) {
res.status(200).json({error: error});
} else {
if (result.data.items.length) {
res.status(200).json(
result.data.items.map((r: any) => {
return {
id: r.id,
title: r.summary,
date: r.start.dateTime,
};
})
);
} else {
res.status(200).json({
events: [],
message: "No upcoming events.",
});
}
}
res.end();
}
);
}
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const shows = await db
.collection("shows")
.orderBy("date")
.get();
res.status(200).json(shows.docs.map(show => {
const result = {
id: show.id,
...show.data()
};
return result;
}));
res.end();
};
export default handler;

View File

@@ -4,5 +4,10 @@ module.exports = {
theme: {
extend: {},
},
// safelist: [
// {
// pattern: /./, // the "." means "everything"
// },
// ],
plugins: [require("daisyui")],
};

617
yarn.lock

File diff suppressed because it is too large Load Diff