mirror of
https://github.com/fergalmoran/radio-otherway.git
synced 2025-12-22 09:50:29 +00:00
Fix FLUC on initial load
This commit is contained in:
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<file url="PROJECT" libraries="{theme-change}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/web.iml
generated
1
.idea/web.iml
generated
@@ -11,5 +11,6 @@
|
|||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="theme-change" level="application" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"eslint-config-next": "13.1.5",
|
"eslint-config-next": "13.1.5",
|
||||||
"feather-icons": "^4.29.0",
|
"feather-icons": "^4.29.0",
|
||||||
"firebase": "^9.17.1",
|
"firebase": "^9.17.1",
|
||||||
|
"firebase-admin": "^11.5.0",
|
||||||
"firebase-functions": "^4.2.1",
|
"firebase-functions": "^4.2.1",
|
||||||
"fireschema": "^4.0.4",
|
"fireschema": "^4.0.4",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
@@ -36,6 +37,8 @@
|
|||||||
"pino": "^8.11.0",
|
"pino": "^8.11.0",
|
||||||
"pino-logflare": "^0.3.12",
|
"pino-logflare": "^0.3.12",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-daisyui": "^3.0.3",
|
||||||
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginPage } from "@/components/auth";
|
import { LoginPage } from "@/components/pages/auth";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const Login = async () => {
|
const Login = async () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { SignupPage } from "@/components/pages/auth";
|
||||||
|
|
||||||
const Signup = async () => {
|
const Signup = async () => {
|
||||||
return <h1>Coming soon</h1>;
|
return <div className="flex flex-wrap w-full justify-evenly"><SignupPage /></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Signup;
|
export default Signup;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
export default function Head() {
|
export default function Head() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>Radio::Otherway</title>
|
<title>Radio::Otherway</title>
|
||||||
@@ -27,6 +30,8 @@ export default function Head() {
|
|||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
|
{/*need to include this here to avoid the FLUC on initial load*/}
|
||||||
|
<script src="https://unpkg.com/theme-change@2.2.0/index.js" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,41 +4,38 @@ import "./globals.css";
|
|||||||
import { Raleway } from "@next/font/google";
|
import { Raleway } from "@next/font/google";
|
||||||
import { NavBar, PushNotificationWrapper } from "@/components/layout";
|
import { NavBar, PushNotificationWrapper } from "@/components/layout";
|
||||||
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { themeChange } from "theme-change";
|
||||||
import { defaults } from "@/lib/constants";
|
|
||||||
|
|
||||||
const font = Raleway({
|
const font = Raleway({
|
||||||
weight: ["400", "700"],
|
weight: ["400", "700"],
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
variable: "--font-raleway",
|
variable: "--font-raleway"
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const theme = localStorage.getItem("theme") || defaults.defaultTheme;
|
themeChange(false);
|
||||||
if (theme && !document.body.dataset.theme) {
|
|
||||||
document.body.dataset.theme = theme;
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head />
|
<head />
|
||||||
<body className={`${font.className}`}>
|
<body className={`${font.className}`}>
|
||||||
<PushNotificationWrapper>
|
<AuthUserProvider>
|
||||||
<AuthUserProvider>
|
<PushNotificationWrapper>
|
||||||
<div className="flex flex-col min-h-screen bg-base-100">
|
<div className="flex flex-col min-h-screen bg-base-100">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="items-end grow place-items-center bg-base-200 text-primary-content">
|
<div className="items-end grow place-items-center bg-base-200 text-primary-content">
|
||||||
<main className=" text-base-content">{children}</main>
|
<main className=" text-base-content">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AuthUserProvider>
|
</PushNotificationWrapper>
|
||||||
</PushNotificationWrapper>
|
</AuthUserProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import LoginPage from "./LoginPage";
|
|
||||||
|
|
||||||
export { LoginPage };
|
|
||||||
@@ -38,9 +38,9 @@ const Navbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="/signup"
|
href="/signup"
|
||||||
id="signup"
|
id="signup"
|
||||||
className="font-normal normal-case font-body btn-primary btn-sm btn"
|
className="gap-1 normal-case btn-ghost btn"
|
||||||
>
|
>
|
||||||
<PlusSquare size={12} className="mr-2" />
|
<PlusSquare size={20} className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6" />
|
||||||
Register
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
@@ -62,10 +62,10 @@ const Navbar = () => {
|
|||||||
<Link href="/">
|
<Link href="/">
|
||||||
<Image src="/logo.png" alt="Otherway" width={42} height={42} />
|
<Image src="/logo.png" alt="Otherway" width={42} height={42} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row md:space-x-10">
|
<div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row">
|
||||||
{!loading && NavMenu}
|
{!loading && NavMenu}
|
||||||
|
<ThemeSelector />
|
||||||
</div>
|
</div>
|
||||||
<ThemeSelector />
|
|
||||||
|
|
||||||
<div className="ml-auto lg:hidden">
|
<div className="ml-auto lg:hidden">
|
||||||
<div className="dropdown-end dropdown" data-cy="dropdown">
|
<div className="dropdown-end dropdown" data-cy="dropdown">
|
||||||
|
|||||||
@@ -6,58 +6,92 @@ import { Toaster } from "react-hot-toast";
|
|||||||
import { getMessaging, onMessage } from "firebase/messaging";
|
import { getMessaging, onMessage } from "firebase/messaging";
|
||||||
import { app } from "@/lib/auth/firebase";
|
import { app } from "@/lib/auth/firebase";
|
||||||
import ToastService from "../widgets/toast";
|
import ToastService from "../widgets/toast";
|
||||||
|
import { doc, setDoc } from "@firebase/firestore";
|
||||||
|
import { useAuthUserContext } from "@/lib/auth/authUserContext";
|
||||||
|
import { users } from "@/lib/db";
|
||||||
|
import logger from "@/lib/util/logging";
|
||||||
|
import { parseUserAgent } from "react-device-detect";
|
||||||
|
|
||||||
const PushNotificationWrapper = ({ children }: React.PropsWithChildren) => {
|
const PushNotificationWrapper = ({ children }: React.PropsWithChildren) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useEffect(() => {
|
const { profile } = useAuthUserContext();
|
||||||
setToken();
|
useEffect(() => {
|
||||||
|
const _getAndStoreRegistrationToken = async () => {
|
||||||
|
const { ua } = parseUserAgent(window.navigator.userAgent);
|
||||||
|
|
||||||
// Event listener that listens for the push notification event in the background
|
if (!profile) return;
|
||||||
if ("serviceWorker" in navigator) {
|
await setToken();
|
||||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
||||||
console.log("event for the service worker", event);
|
// Event listener that listens for the push notification event in the background
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||||
|
console.log("event for the service worker", event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calls the getMessage() function if the token is there
|
||||||
|
async function setToken() {
|
||||||
|
try {
|
||||||
|
if (!profile) return;
|
||||||
|
const token = await firebaseCloudMessaging.init();
|
||||||
|
if (token && profile) {
|
||||||
|
const newRegistration = {
|
||||||
|
fcmToken: token,
|
||||||
|
deviceType: ua,
|
||||||
|
lastSeen: new Date()
|
||||||
|
};
|
||||||
|
const index = profile.deviceRegistrations?.findIndex(reg => {
|
||||||
|
return reg.fcmToken === token;
|
||||||
|
});
|
||||||
|
if (index !== undefined && index !== -1) {
|
||||||
|
if (profile.deviceRegistrations && profile.deviceRegistrations[index]) {
|
||||||
|
profile.deviceRegistrations[index] = newRegistration;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profile.deviceRegistrations?.push(newRegistration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const profileWithRegistrations = Object.assign({}, profile);
|
||||||
|
await setDoc(doc(users, profile?.id), profileWithRegistrations, { merge: true });
|
||||||
|
getMessage();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_getAndStoreRegistrationToken()
|
||||||
|
.catch(err => {
|
||||||
|
logger.error("PushNotificationWrapper", "_getAndStoreRegistrationToken_error", err);
|
||||||
|
});
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
|
function getMessage() {
|
||||||
|
const messaging = getMessaging(app);
|
||||||
|
onMessage(messaging, (message) => {
|
||||||
|
ToastService.custom(
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
message?.data?.url &&
|
||||||
|
handleClickPushNotification(message?.data?.url)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h5>{message?.notification?.title}</h5>
|
||||||
|
<h6>{message?.notification?.body}</h6>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calls the getMessage() function if the token is there
|
const handleClickPushNotification = (url: string) => {
|
||||||
async function setToken() {
|
router.push(url);
|
||||||
try {
|
};
|
||||||
const token = await firebaseCloudMessaging.init();
|
return (
|
||||||
if (token) {
|
<>
|
||||||
console.log("token", token);
|
|
||||||
getMessage();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function getMessage() {
|
|
||||||
const messaging = getMessaging(app);
|
|
||||||
|
|
||||||
onMessage(messaging, (message) => {
|
|
||||||
ToastService.custom(
|
|
||||||
<div
|
|
||||||
onClick={() =>
|
|
||||||
message?.data?.url &&
|
|
||||||
handleClickPushNotification(message?.data?.url)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<h5>{message?.notification?.title}</h5>
|
|
||||||
<h6>{message?.notification?.body}</h6>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const handleClickPushNotification = (url: string) => {
|
|
||||||
router.push(url);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
;
|
||||||
|
|
||||||
export default PushNotificationWrapper;
|
export default PushNotificationWrapper;
|
||||||
|
|||||||
157
src/components/pages/auth/SignupPage.tsx
Normal file
157
src/components/pages/auth/SignupPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||||
|
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
|
||||||
|
|
||||||
|
const SignupPage = () => {
|
||||||
|
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, profile, signUp } =
|
||||||
|
useFirebaseAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
const [error, setError] = React.useState("");
|
||||||
|
const [email, setEmail] = React.useState("");
|
||||||
|
const [password, setPassword] = React.useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = React.useState("");
|
||||||
|
|
||||||
|
|
||||||
|
const register = async (
|
||||||
|
$event: React.SyntheticEvent<HTMLButtonElement>
|
||||||
|
): Promise<void> => {
|
||||||
|
$event.preventDefault();
|
||||||
|
const result = await signUp(email, password);
|
||||||
|
if (result === "auth/email-already-in-use") {
|
||||||
|
setError("This email address has already been used to create an account.");
|
||||||
|
} else if (result === "auth/invalid-email") {
|
||||||
|
setError("Please enter a correct email address");
|
||||||
|
} else {
|
||||||
|
setError("Unable to create an account for you at this time");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1">
|
||||||
|
<h3 className="my-4 text-2xl font-semibold font-title">
|
||||||
|
Create New Account
|
||||||
|
</h3>
|
||||||
|
{error && (
|
||||||
|
<div className="shadow-lg alert alert-error mb-4">
|
||||||
|
<div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="flex-shrink-0 w-6 h-6 stroke-current" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form action="#" className="flex flex-col space-y-5">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<label htmlFor="email" className="text-sm">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
autoFocus
|
||||||
|
className="input-bordered input-primary input input-sm"
|
||||||
|
value={email}
|
||||||
|
onChange={(event) => {
|
||||||
|
setEmail(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label htmlFor="password" className="text-sm">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className="input-bordered input-primary input input-sm"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label htmlFor="password" className="text-sm">
|
||||||
|
Repeat password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className="input-bordered input-primary input input-sm"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(event) => {
|
||||||
|
setConfirmPassword(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="w-full btn-primary btn"
|
||||||
|
onClick={(event) => {
|
||||||
|
void register(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Register for Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-5">
|
||||||
|
<span className="flex items-center justify-center space-x-2">
|
||||||
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
|
<span className="font-normal text-gray-500">or sign up with</span>
|
||||||
|
<span className="h-px bg-gray-400 w-14" />
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-1/3 gap-2 btn"
|
||||||
|
onClick={signInWithTwitter}
|
||||||
|
>
|
||||||
|
<div className="text-base-content">
|
||||||
|
<IoLogoTwitter />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-base-content">
|
||||||
|
Twitter
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-1/3 gap-2 btn"
|
||||||
|
onClick={signInWithGoogle}
|
||||||
|
>
|
||||||
|
<div className="text-base-content">
|
||||||
|
<IoLogoGoogle />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-base-content">
|
||||||
|
Gmail
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-1/3 gap-2 btn"
|
||||||
|
onClick={signInWithFacebook}
|
||||||
|
>
|
||||||
|
<div className="text-base-content">
|
||||||
|
<IoLogoFacebook />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-base-content">
|
||||||
|
Facebook
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupPage;
|
||||||
4
src/components/pages/auth/index.ts
Normal file
4
src/components/pages/auth/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import LoginPage from "./LoginPage";
|
||||||
|
import SignupPage from "./SignupPage";
|
||||||
|
|
||||||
|
export { LoginPage, SignupPage };
|
||||||
@@ -19,8 +19,7 @@ const ProfilePageComponentProfile = () => {
|
|||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
const [image, setImage] = React.useState("");
|
const [image, setImage] = React.useState("");
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log("ProfilePageComponentProfile", "useEffect", profile);
|
if (profile) {
|
||||||
if (profile) {
|
|
||||||
setEmail(profile.email as string);
|
setEmail(profile.email as string);
|
||||||
setDisplayName(profile.displayName as string);
|
setDisplayName(profile.displayName as string);
|
||||||
setAbout(profile.about as string);
|
setAbout(profile.about as string);
|
||||||
|
|||||||
@@ -5,21 +5,11 @@ import { useState, useEffect } from "react";
|
|||||||
import { IoColorPaletteOutline } from "react-icons/io5";
|
import { IoColorPaletteOutline } from "react-icons/io5";
|
||||||
import { BiDownArrow } from "react-icons/bi";
|
import { BiDownArrow } from "react-icons/bi";
|
||||||
import { defaults } from "@/lib/constants";
|
import { defaults } from "@/lib/constants";
|
||||||
|
import { useTheme } from "react-daisyui";
|
||||||
|
import { themeChange } from "theme-change";
|
||||||
|
|
||||||
const ThemeSelector = () => {
|
const ThemeSelector = () => {
|
||||||
const [activeTheme, setActiveTheme] = useState(
|
const _switchTheme = useCallback(() => {
|
||||||
typeof window !== "undefined" && localStorage.getItem("theme") ||
|
|
||||||
defaults.defaultTheme);
|
|
||||||
//
|
|
||||||
useEffect(() => {
|
|
||||||
if (document) {
|
|
||||||
document.body.dataset.theme = activeTheme;
|
|
||||||
window.localStorage.setItem("theme", activeTheme);
|
|
||||||
}
|
|
||||||
}, [activeTheme]);
|
|
||||||
|
|
||||||
const _switchTheme = useCallback((theme: string) => {
|
|
||||||
setActiveTheme(theme);
|
|
||||||
const elem = document.activeElement as HTMLElement;
|
const elem = document.activeElement as HTMLElement;
|
||||||
elem?.blur();
|
elem?.blur();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -39,7 +29,8 @@ const ThemeSelector = () => {
|
|||||||
<button
|
<button
|
||||||
key={theme.id}
|
key={theme.id}
|
||||||
className="overflow-hidden text-left rounded-lg outline-base-content"
|
className="overflow-hidden text-left rounded-lg outline-base-content"
|
||||||
onClick={() => _switchTheme(theme.id)}
|
data-set-theme={theme.id}
|
||||||
|
onClick={() => _switchTheme()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-theme={theme.id}
|
data-theme={theme.id}
|
||||||
|
|||||||
8
src/lib/auth/firebaseAdmin.tsx
Normal file
8
src/lib/auth/firebaseAdmin.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import admin from "firebase-admin";
|
||||||
|
|
||||||
|
const serviceAccount = JSON.parse(
|
||||||
|
process.env.FIREBASE_SERVICE_ACCOUNT_KEY as string
|
||||||
|
);
|
||||||
|
export const firebaseAdmin = admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount)
|
||||||
|
});
|
||||||
@@ -3,16 +3,16 @@ import localforage from "localforage";
|
|||||||
import { getMessaging, getToken } from "firebase/messaging";
|
import { getMessaging, getToken } from "firebase/messaging";
|
||||||
import { app } from "./firebase";
|
import { app } from "./firebase";
|
||||||
|
|
||||||
|
|
||||||
const firebaseCloudMessaging = {
|
const firebaseCloudMessaging = {
|
||||||
init: async () => {
|
init: async (): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
const messaging = getMessaging(app);
|
const messaging = getMessaging(app);
|
||||||
|
|
||||||
const tokenInLocalForage = await localforage.getItem("fcm_token");
|
const tokenInLocalForage = await localforage.getItem("fcm_token");
|
||||||
|
|
||||||
// Return the token if it is alredy in our local storage
|
// Return the token if it is alredy in our local storage
|
||||||
if (tokenInLocalForage !== null) {
|
if (tokenInLocalForage !== null) {
|
||||||
return tokenInLocalForage;
|
return tokenInLocalForage as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request the push notification permission from browser
|
// Request the push notification permission from browser
|
||||||
@@ -23,14 +23,14 @@ const firebaseCloudMessaging = {
|
|||||||
|
|
||||||
// Set token in our local storage
|
// Set token in our local storage
|
||||||
if (token) {
|
if (token) {
|
||||||
localforage.setItem("fcm_token", token);
|
await localforage.setItem("fcm_token", token);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
},
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export { firebaseCloudMessaging };
|
export { firebaseCloudMessaging };
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Profile } from "@/models";
|
|||||||
import { users } from "../db";
|
import { users } from "../db";
|
||||||
import { doc, getDoc, setDoc } from "firebase/firestore";
|
import { doc, getDoc, setDoc } from "firebase/firestore";
|
||||||
import { servicesVersion } from "@ts-morph/common/lib/typescript";
|
import { servicesVersion } from "@ts-morph/common/lib/typescript";
|
||||||
|
import logger from "../util/logging";
|
||||||
|
|
||||||
export default function useFirebaseAuth() {
|
export default function useFirebaseAuth() {
|
||||||
const [profile, setProfile] = useState<Profile | undefined>();
|
const [profile, setProfile] = useState<Profile | undefined>();
|
||||||
@@ -38,7 +39,9 @@ export default function useFirebaseAuth() {
|
|||||||
(savedProfile?.email || auth.currentUser.email) as string,
|
(savedProfile?.email || auth.currentUser.email) as string,
|
||||||
(savedProfile?.displayName || auth.currentUser.email) as string,
|
(savedProfile?.displayName || auth.currentUser.email) as string,
|
||||||
(savedProfile?.photoURL || auth.currentUser.email) as string,
|
(savedProfile?.photoURL || auth.currentUser.email) as string,
|
||||||
savedProfile?.about as string
|
savedProfile?.about as string,
|
||||||
|
new Date(),
|
||||||
|
savedProfile?.deviceRegistrations
|
||||||
);
|
);
|
||||||
setProfile(profile);
|
setProfile(profile);
|
||||||
await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
|
await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
|
||||||
@@ -65,8 +68,16 @@ export default function useFirebaseAuth() {
|
|||||||
const signIn = (email: string, password: string) =>
|
const signIn = (email: string, password: string) =>
|
||||||
signInWithEmailAndPassword(auth, email, password);
|
signInWithEmailAndPassword(auth, email, password);
|
||||||
|
|
||||||
const signUp = (email: string, password: string) =>
|
const signUp = async (email: string, password: string): Promise<string> => {
|
||||||
createUserWithEmailAndPassword(auth, email, password);
|
try {
|
||||||
|
const response = await createUserWithEmailAndPassword(auth, email, password);
|
||||||
|
logger.debug("useFireBaseAuth", "signUp_success", response);
|
||||||
|
return "";
|
||||||
|
} catch (err: { code: string } | any) {
|
||||||
|
logger.error("useFireBaseAuth", "signUp", err);
|
||||||
|
return err.code;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const logOut = () => signOut(auth).then(clear);
|
const logOut = () => signOut(auth).then(clear);
|
||||||
|
|
||||||
|
|||||||
5
src/models/deviceregistration.ts
Normal file
5
src/models/deviceregistration.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default interface DeviceRegistration {
|
||||||
|
deviceType: string;
|
||||||
|
fcmToken: string;
|
||||||
|
lastSeen: Date;
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import Profile from "./profile";
|
|||||||
import Show from "./show";
|
import Show from "./show";
|
||||||
import Reminder from "./reminder";
|
import Reminder from "./reminder";
|
||||||
import Notification from "./notification";
|
import Notification from "./notification";
|
||||||
|
import DeviceRegistration from "./deviceregistration";
|
||||||
import type { RemindersProcessed } from "./processes";
|
import type { RemindersProcessed } from "./processes";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -9,5 +10,6 @@ export {
|
|||||||
Show,
|
Show,
|
||||||
Reminder,
|
Reminder,
|
||||||
Notification,
|
Notification,
|
||||||
|
type DeviceRegistration,
|
||||||
RemindersProcessed
|
RemindersProcessed
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import DeviceRegistration from "@/models/deviceregistration";
|
||||||
|
|
||||||
export default class Profile {
|
export default class Profile {
|
||||||
id: string;
|
id: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
@@ -6,6 +8,7 @@ export default class Profile {
|
|||||||
emailVerified: boolean = false;
|
emailVerified: boolean = false;
|
||||||
about?: String;
|
about?: String;
|
||||||
lastSeen: Date;
|
lastSeen: Date;
|
||||||
|
deviceRegistrations?: DeviceRegistration[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -13,7 +16,8 @@ export default class Profile {
|
|||||||
displayName: string | null,
|
displayName: string | null,
|
||||||
photoURL: string | null,
|
photoURL: string | null,
|
||||||
about?: string,
|
about?: string,
|
||||||
lastSeen?: Date
|
lastSeen?: Date,
|
||||||
|
deviceRegistrations?: DeviceRegistration[]
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
@@ -22,5 +26,6 @@ export default class Profile {
|
|||||||
this.about = about || "";
|
this.about = about || "";
|
||||||
|
|
||||||
this.lastSeen = lastSeen || new Date();
|
this.lastSeen = lastSeen || new Date();
|
||||||
|
this.deviceRegistrations = deviceRegistrations || this.deviceRegistrations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { getMessaging } from "firebase/messaging";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const message = {
|
|
||||||
data: {
|
|
||||||
score: "850",
|
|
||||||
time: "2:45",
|
|
||||||
},
|
|
||||||
token:
|
|
||||||
"eQM4lB5_AWPgTPfIqdJ4jQ:APA91bHwBiljwSbyx7gU_IPLu59hFPkCPd-0OElLNQyF4m6nBluXfBnyvh115tpSjR0ceO9IhY7PuwkoR09u_JykBmjBvrsuUkDWzOdKxVLnV5STOV98dr56Avo5HdmPrei6SxMbHeTb",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send a message to the device corresponding to the provided
|
|
||||||
// registration token.
|
|
||||||
getMessaging()
|
|
||||||
.send(message)
|
|
||||||
.then((response) => {
|
|
||||||
console.log("Successfully sent message:", response);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error sending message:", error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
47
src/pages/api/debug/fcm/index.ts
Normal file
47
src/pages/api/debug/fcm/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import logger from "@/lib/util/logging";
|
||||||
|
import { firebaseAdmin } from "@/lib/auth/firebaseAdmin";
|
||||||
|
import { doc, getDocs, query, where } from "@firebase/firestore";
|
||||||
|
import { getDoc } from "firebase/firestore";
|
||||||
|
import { users } from "@/lib/db";
|
||||||
|
import { Profile, Reminder } from "@/models";
|
||||||
|
|
||||||
|
const _getPayload = (message: string) => ({
|
||||||
|
notification: {
|
||||||
|
title: "Argle Bargle",
|
||||||
|
body: `You are a ${message}`,
|
||||||
|
image: "https://otherway.fergl.ie/logo.png"
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
url: "https://google.com"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
|
|
||||||
|
// Send a message to the device corresponding to the provided
|
||||||
|
// registration token.
|
||||||
|
try {
|
||||||
|
const results: any[] = [];
|
||||||
|
const userResultSet = await getDocs(query(users, where("email", "==", "fergal.moran@gmail.com")));
|
||||||
|
for (let u of userResultSet.docs) {
|
||||||
|
const user = u.data() as Profile;
|
||||||
|
if (user?.deviceRegistrations) {
|
||||||
|
for (const token of user?.deviceRegistrations) {
|
||||||
|
results.push(await firebaseAdmin
|
||||||
|
.messaging()
|
||||||
|
.sendToDevice(
|
||||||
|
token.fcmToken,
|
||||||
|
_getPayload(token.deviceType)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.status(200).json({ results });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("fcm", "sendPush", "Failed", err);
|
||||||
|
res.status(500);
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
export default handler;
|
||||||
@@ -2,7 +2,6 @@ import { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher";
|
import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
await setupCalendarWebhook();
|
|
||||||
res.status(200).json({ hello: process.env });
|
res.status(200).json({ hello: process.env });
|
||||||
};
|
};
|
||||||
export default handler;
|
export default handler;
|
||||||
Reference in New Issue
Block a user