mirror of
https://github.com/fergalmoran/radio-otherway.git
synced 2025-12-27 20:28:18 +00:00
Major auth fixup
This commit is contained in:
2
.env
2
.env
@@ -1,3 +1,5 @@
|
|||||||
|
NEXT_PUBLIC_TITLE=RadioOtherway
|
||||||
|
|
||||||
QSTASH_CURRENT_SIGNING_KEY=khs3lpVBv1QtV/L9MTdXlcnoI8tTlg0aDfrFz+o8utA=
|
QSTASH_CURRENT_SIGNING_KEY=khs3lpVBv1QtV/L9MTdXlcnoI8tTlg0aDfrFz+o8utA=
|
||||||
|
|
||||||
#auth
|
#auth
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
".vscode":true,
|
||||||
|
".next": true,
|
||||||
|
".vercel": true
|
||||||
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
}
|
}
|
||||||
@@ -18,22 +18,27 @@
|
|||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
"@upstash/qstash": "^0.3.6",
|
"@upstash/qstash": "^0.3.6",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"daisyui": "^2.49.0",
|
"daisyui": "^2.49.0",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-config-next": "13.1.5",
|
"eslint-config-next": "13.1.5",
|
||||||
"firebase": "^9.17.1",
|
"firebase": "^9.17.1",
|
||||||
"firebase-admin": "^11.5.0",
|
"firebase-functions": "^4.2.1",
|
||||||
|
"fireschema": "^4.0.4",
|
||||||
"next": "13.1.5",
|
"next": "13.1.5",
|
||||||
"next-logger": "^3.0.1",
|
"next-logger": "^3.0.1",
|
||||||
|
"next-seo": "^5.15.0",
|
||||||
"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-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-feather": "^2.0.10",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.7.1",
|
||||||
"twilio": "^4.8.0",
|
"twilio": "^4.8.0",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4",
|
||||||
|
"zod": "^3.20.6"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19",
|
"packageManager": "yarn@1.22.19",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { LoginPage } from "@/components/auth";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const Login = async () => {
|
const Login = async () => {
|
||||||
return <LoginPage />;
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap justify-evenly w-full">
|
||||||
|
<LoginPage />
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
7
src/app/(auth)/signup/page.tsx
Normal file
7
src/app/(auth)/signup/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Signup = async () => {
|
||||||
|
return <h1>Coming soon</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Signup;
|
||||||
@@ -4,18 +4,12 @@ import "./globals.css";
|
|||||||
import { Inter } from "@next/font/google";
|
import { Inter } from "@next/font/google";
|
||||||
import { NavBar } from "@/components/layout";
|
import { NavBar } from "@/components/layout";
|
||||||
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
// const Toaster = dynamic(
|
|
||||||
// () => import("react-hot-toast").then((c) => c.Toaster),
|
|
||||||
// {
|
|
||||||
// ssr: false,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
@@ -24,9 +18,15 @@ export default function RootLayout({
|
|||||||
<head />
|
<head />
|
||||||
<body className={`${inter.className} h-screen`}>
|
<body className={`${inter.className} h-screen`}>
|
||||||
<AuthUserProvider>
|
<AuthUserProvider>
|
||||||
|
<div className="min-h-screen w-full bg-base-100 text-base-content m-auto">
|
||||||
|
<div className="max-w-7xl flex flex-col min-h-screen mx-auto p-5">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="-mt-[4rem]">{children}</div>
|
<main className="flex-1">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AuthUserProvider>
|
</AuthUserProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ import React from "react";
|
|||||||
|
|
||||||
const Loading = () => {
|
const Loading = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center">
|
<div role="status">
|
||||||
<div
|
<svg
|
||||||
className="inline-block w-8 h-8 border-4 rounded-full spinner-border animate-spin"
|
aria-hidden="true"
|
||||||
role="status"
|
className="inline w-8 h-8 mr-2 text-gray-200 animate-spin fill-green-500 dark:text-gray-600"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<span className="visually-hidden">Loading...</span>
|
<path
|
||||||
</div>
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const getData = async (): Promise<Show[]> => {
|
|||||||
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
|
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
|
||||||
);
|
);
|
||||||
logger.debug("getDate", res);
|
logger.debug("getDate", res);
|
||||||
return await res.json();
|
const data = await res.json();
|
||||||
|
return data.map((r: string) => Show.fromJson(r));
|
||||||
};
|
};
|
||||||
|
|
||||||
const Home = async () => {
|
const Home = async () => {
|
||||||
|
|||||||
7
src/app/profile/page.tsx
Normal file
7
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ProfilePageComponent } from "@/components/pages";
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
|
return <ProfilePageComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
@@ -1,80 +1,163 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {BsFacebook, BsGoogle, BsTwitter} from "react-icons/bs";
|
|
||||||
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||||
|
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const {signInWithGoogle, signInWithTwitter, signInWithFacebook} = useFirebaseAuth();
|
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, user, signIn } =
|
||||||
const router = useRouter()
|
useFirebaseAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
const [email, setEmail] = React.useState("");
|
||||||
|
const [password, setPassword] = React.useState("");
|
||||||
|
const [forgot, setForgot] = React.useState(false);
|
||||||
|
|
||||||
|
const login = async (
|
||||||
|
event: React.SyntheticEvent<HTMLButtonElement>
|
||||||
|
): Promise<void> => {
|
||||||
|
const result = await signIn(email, password);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 flex flex-col justify-center sm:py-12">
|
<div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1">
|
||||||
<div className="xs:p-0 mx-auto md:w-full md:max-w-md">
|
<>
|
||||||
<h1 className="font-bold text-center text-2xl mb-5 mx-auto">
|
<h3 className="my-4 text-2xl font-semibold font-title">
|
||||||
Login to Radio Otherway
|
Account Login
|
||||||
</h1>
|
</h3>
|
||||||
<div className="bg-white shadow w-full rounded-lg">
|
<form action="#" className="flex flex-col space-y-5">
|
||||||
<div className="px-5 py-7">
|
<div className="flex flex-col space-y-1">
|
||||||
<label className="font-semibold text-sm text-gray-600 pb-1 block">E-mail</label>
|
<label htmlFor="email" className="text-sm">
|
||||||
<input type="text" className="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"/>
|
Email address
|
||||||
<label className="font-semibold text-sm text-gray-600 pb-1 block">Password</label>
|
</label>
|
||||||
<input type="text" className="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"/>
|
<input
|
||||||
<button type="button"
|
type="email"
|
||||||
className="transition duration-200 bg-blue-500 hover:bg-blue-600 focus:bg-blue-700 focus:shadow-sm focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 text-white w-full py-2.5 rounded-lg text-sm shadow-sm hover:shadow-md font-semibold text-center inline-block">
|
id="email"
|
||||||
<span className="inline-block mr-2">Login</span>
|
autoFocus
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
className="input-bordered input-primary input input-sm"
|
||||||
className="w-4 h-4 inline-block">
|
value={email}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
|
onChange={(event) => {
|
||||||
</svg>
|
setEmail(event.target.value);
|
||||||
</button>
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex py-5 items-center">
|
<div className="flex flex-col space-y-1">
|
||||||
<div className="flex-grow border-t border-gray-400"></div>
|
<div className="flex items-center justify-between">
|
||||||
<span className="flex-shrink mx-4 text-gray-400">or login with</span>
|
<label htmlFor="password" className="text-sm">
|
||||||
<div className="flex-grow border-t border-gray-400"></div>
|
Password
|
||||||
</div>
|
</label>
|
||||||
<div className="p-5">
|
|
||||||
<div className="btn-group w-full">
|
|
||||||
<button type="button"
|
|
||||||
className="btn gap-2"
|
|
||||||
onClick={signInWithGoogle}
|
|
||||||
>
|
|
||||||
<BsGoogle className="h-6 w-6"/>
|
|
||||||
Google
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
className="btn gap-2"
|
|
||||||
onClick={signInWithTwitter}>
|
|
||||||
<BsTwitter className="h-6 w-6"/>
|
|
||||||
Twitter
|
|
||||||
</button>
|
|
||||||
<button type="button"
|
|
||||||
className="btn gap-2"
|
|
||||||
onClick={signInWithFacebook}>
|
|
||||||
<BsFacebook className="h-6 w-6"/>
|
|
||||||
Facebook
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="py-5">
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<div className="text-center sm:text-left whitespace-nowrap">
|
|
||||||
<button
|
<button
|
||||||
className="transition duration-200 mx-5 px-5 py-4 cursor-pointer font-normal text-sm rounded-lg text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-200 focus:ring-2 focus:ring-gray-400 focus:ring-opacity-50 ring-inset">
|
type="button"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
onClick={() => {
|
||||||
className="w-4 h-4 inline-block align-text-top">
|
setForgot(true);
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
}}
|
||||||
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/>
|
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
|
||||||
</svg>
|
>
|
||||||
<span className="inline-block ml-1">Forgot Password</span>
|
Forgot Password?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<input
|
||||||
</div>
|
type="password"
|
||||||
</div>
|
id="password"
|
||||||
</div>
|
className="input-bordered input-primary input input-sm"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => {
|
||||||
|
setPassword(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="w-full btn-primary btn"
|
||||||
|
onClick={(event) => {
|
||||||
|
void login(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</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 login 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 className="flex flex-row space-x-1">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none"
|
||||||
|
onClick={signInWithTwitter}
|
||||||
|
>
|
||||||
|
<div className="text-base-content">
|
||||||
|
<IoLogoTwitter />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-base-content">
|
||||||
|
Twitter
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none"
|
||||||
|
onClick={signInWithGoogle}
|
||||||
|
>
|
||||||
|
<div className="text-base-content">
|
||||||
|
<IoLogoGoogle />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-base-content">
|
||||||
|
Gmail
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border rounded-md group border-base-200 hover:bg-base-300 focus:outline-none"
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,76 +3,75 @@ import React from "react";
|
|||||||
import { BiLogInCircle } from "react-icons/bi";
|
import { BiLogInCircle } from "react-icons/bi";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||||
import {useAuthUserContext} from '@/lib/auth/authUserContext';
|
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 Signup from "@/app/(auth)/signup/page";
|
||||||
|
|
||||||
|
const ThemeToggle = dynamic(
|
||||||
|
() => import("@/components/widgets/ui/theme/ThemeToggle"),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const {authUser, loading, logOut} = useAuthUserContext();
|
const { user, loading, logOut } = useAuthUserContext();
|
||||||
|
const NavMenu = user ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
id="profile"
|
||||||
|
className="font-normal normal-case font-body btn-primary btn-sm btn"
|
||||||
|
>
|
||||||
|
<User size={12} className="mr-2" />
|
||||||
|
Profile
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
id="logout-btn"
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
onClick={() => logOut()}
|
||||||
|
>
|
||||||
|
<LogOut size={12} className="mr-2" />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<Link
|
||||||
|
href="/signup"
|
||||||
|
id="signup"
|
||||||
|
className="font-normal normal-case font-body btn-primary btn-sm btn"
|
||||||
|
>
|
||||||
|
<PlusSquare size={12} className="mr-2" />
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
<Link href="/login" id="login" className="btn-ghost btn-sm btn">
|
||||||
|
<LogIn size={12} className="mr-2" />
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<nav className="w-full mb-2 navbar">
|
||||||
className="sticky top-0 z-30 flex justify-center w-full h-16 transition-all duration-100 shadow-sm bg-base-100 bg-opacity-90 text-base-content backdrop-blur">
|
<Link href="/">
|
||||||
<nav className="w-full navbar">
|
<Image src="/logo.png" alt="Otherway" width={32} height={32} />
|
||||||
<div className="navbar-start">
|
|
||||||
<div className="dropdown">
|
|
||||||
<label tabIndex={0} className="btn-ghost btn lg:hidden">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 6h16M4 12h8m-8 6h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="p-2 mt-3 shadow dropdown-content menu rounded-box menu-compact w-52 bg-base-100"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a className="normal-case btn-ghost drawer-button btn">
|
|
||||||
Item 1
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a className="normal-case btn-ghost drawer-button btn">
|
|
||||||
Item 3
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<a href="/" className="text-xl normal-case btn-ghost btn">Radio::Otherway</a>
|
|
||||||
</div>
|
|
||||||
<div className="hidden navbar-center lg:flex">
|
|
||||||
<ul className="px-1 menu menu-horizontal">
|
|
||||||
<li>
|
|
||||||
<a>Coming Up</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="navbar-end">
|
|
||||||
{authUser ? (
|
|
||||||
<button className="gap-4 btn" onClick={() => logOut()}>
|
|
||||||
<BiLogInCircle className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6"/>
|
|
||||||
<span>Logout</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Link className="gap-4 btn" href="/login">
|
|
||||||
<BiLogInCircle className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6"/>
|
|
||||||
<span>Login</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
<div className="flex-col hidden ml-auto text-sm text-center font-body lg:flex lg:flex-row lg:space-x-10">
|
||||||
|
{NavMenu}
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto lg:hidden">
|
||||||
|
<div className="dropdown-end dropdown" data-cy="dropdown">
|
||||||
|
<div tabIndex={0} className="m-1 cursor-pointer">
|
||||||
|
<Menu />
|
||||||
|
</div>
|
||||||
|
<div className="w-24 mt-3 space-y-3 text-center dropdown-content menu">
|
||||||
|
{NavMenu}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
406
src/components/pages/ProfilePageComponent.tsx
Normal file
406
src/components/pages/ProfilePageComponent.tsx
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
"user client";
|
||||||
|
import React from "react";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { User, Bell } from "react-feather";
|
||||||
|
import { useAuthUserContext } from "@/lib/auth/authUserContext";
|
||||||
|
|
||||||
|
const ProfilePageComponent = () => {
|
||||||
|
const { user, loading, logOut } = useAuthUserContext();
|
||||||
|
const subNavigation = [
|
||||||
|
{ name: "Profile", href: "#", icon: User, current: true },
|
||||||
|
{ name: "Notifications", href: "#", icon: Bell, current: false },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden bg-white rounded-lg shadow">
|
||||||
|
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||||
|
<aside className="py-6 lg:col-span-3">
|
||||||
|
<nav className="space-y-1">
|
||||||
|
{subNavigation.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className={classNames(
|
||||||
|
item.current
|
||||||
|
? "border-teal-500 bg-teal-50 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
||||||
|
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
|
||||||
|
"group flex items-center border-l-4 px-3 py-2 text-sm font-medium"
|
||||||
|
)}
|
||||||
|
aria-current={item.current ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={classNames(
|
||||||
|
item.current
|
||||||
|
? "text-teal-500 group-hover:text-teal-500"
|
||||||
|
: "text-gray-400 group-hover:text-gray-500",
|
||||||
|
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="truncate">{item.name}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className="divide-y divide-gray-200 lg:col-span-9"
|
||||||
|
action="#"
|
||||||
|
method="POST"
|
||||||
|
>
|
||||||
|
{/* Profile section */}
|
||||||
|
<div className="px-4 py-6 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
Profile
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
This information will be displayed publicly so be careful what
|
||||||
|
you share.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col mt-6 lg:flex-row">
|
||||||
|
<div className="flex-grow space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="username"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<div className="flex mt-1 rounded-md shadow-sm">
|
||||||
|
<span className="inline-flex items-center px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-md bg-gray-50 sm:text-sm">
|
||||||
|
workcation.com/
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
autoComplete="username"
|
||||||
|
className="flex-grow block w-full min-w-0 border-gray-300 rounded-none rounded-r-md focus:border-sky-500 focus:ring-sky-500 sm:text-sm"
|
||||||
|
defaultValue={user.handle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="about"
|
||||||
|
name="about"
|
||||||
|
rows={3}
|
||||||
|
className="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm"
|
||||||
|
defaultValue={""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Brief description for your profile. URLs are hyperlinked.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-grow mt-6 lg:mt-0 lg:ml-6 lg:flex-shrink-0 lg:flex-grow-0">
|
||||||
|
<p
|
||||||
|
className="text-sm font-medium text-gray-700"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
Photo
|
||||||
|
</p>
|
||||||
|
<div className="mt-1 lg:hidden">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 inline-block w-12 h-12 overflow-hidden rounded-full"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-full h-full rounded-full"
|
||||||
|
src={user.imageUrl}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-5 rounded-md shadow-sm">
|
||||||
|
<div className="relative flex items-center justify-center px-3 py-2 border border-gray-300 rounded-md group focus-within:ring-2 focus-within:ring-sky-500 focus-within:ring-offset-2 hover:bg-gray-50">
|
||||||
|
<label
|
||||||
|
htmlFor="mobile-user-photo"
|
||||||
|
className="relative text-sm font-medium leading-4 text-gray-700 pointer-events-none"
|
||||||
|
>
|
||||||
|
<span>Change</span>
|
||||||
|
<span className="sr-only"> user photo</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="mobile-user-photo"
|
||||||
|
name="user-photo"
|
||||||
|
type="file"
|
||||||
|
className="absolute w-full h-full border-gray-300 rounded-md opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative hidden overflow-hidden rounded-full lg:block">
|
||||||
|
<img
|
||||||
|
className="relative w-40 h-40 rounded-full"
|
||||||
|
src={user.imageUrl}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="desktop-user-photo"
|
||||||
|
className="absolute inset-0 flex items-center justify-center w-full h-full text-sm font-medium text-white bg-black bg-opacity-75 opacity-0 focus-within:opacity-100 hover:opacity-100"
|
||||||
|
>
|
||||||
|
<span>Change</span>
|
||||||
|
<span className="sr-only"> user photo</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="desktop-user-photo"
|
||||||
|
name="user-photo"
|
||||||
|
className="absolute inset-0 w-full h-full border-gray-300 rounded-md opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-12 gap-6 mt-6">
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
htmlFor="first-name"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
First name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first-name"
|
||||||
|
id="first-name"
|
||||||
|
autoComplete="given-name"
|
||||||
|
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:border-sky-500 focus:outline-none focus:ring-sky-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
htmlFor="last-name"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Last name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last-name"
|
||||||
|
id="last-name"
|
||||||
|
autoComplete="family-name"
|
||||||
|
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:border-sky-500 focus:outline-none focus:ring-sky-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-12">
|
||||||
|
<label
|
||||||
|
htmlFor="url"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="url"
|
||||||
|
id="url"
|
||||||
|
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:border-sky-500 focus:outline-none focus:ring-sky-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
htmlFor="company"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Company
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="company"
|
||||||
|
id="company"
|
||||||
|
autoComplete="organization"
|
||||||
|
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:border-sky-500 focus:outline-none focus:ring-sky-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Privacy section */}
|
||||||
|
<div className="pt-6 divide-y divide-gray-200">
|
||||||
|
<div className="px-4 sm:px-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
Privacy
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Ornare eu a volutpat eget vulputate. Fringilla commodo amet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul role="list" className="mt-2 divide-y divide-gray-200">
|
||||||
|
<Switch.Group
|
||||||
|
as="li"
|
||||||
|
className="flex items-center justify-between py-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Switch.Label
|
||||||
|
as="p"
|
||||||
|
className="text-sm font-medium text-gray-900"
|
||||||
|
passive
|
||||||
|
>
|
||||||
|
Available to hire
|
||||||
|
</Switch.Label>
|
||||||
|
<Switch.Description className="text-sm text-gray-500">
|
||||||
|
Nulla amet tempus sit accumsan. Aliquet turpis sed sit
|
||||||
|
lacinia.
|
||||||
|
</Switch.Description>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={availableToHire}
|
||||||
|
onChange={setAvailableToHire}
|
||||||
|
className={classNames(
|
||||||
|
availableToHire ? "bg-teal-500" : "bg-gray-200",
|
||||||
|
"relative ml-4 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
availableToHire ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
<Switch.Group
|
||||||
|
as="li"
|
||||||
|
className="flex items-center justify-between py-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Switch.Label
|
||||||
|
as="p"
|
||||||
|
className="text-sm font-medium text-gray-900"
|
||||||
|
passive
|
||||||
|
>
|
||||||
|
Make account private
|
||||||
|
</Switch.Label>
|
||||||
|
<Switch.Description className="text-sm text-gray-500">
|
||||||
|
Pharetra morbi dui mi mattis tellus sollicitudin cursus
|
||||||
|
pharetra.
|
||||||
|
</Switch.Description>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={privateAccount}
|
||||||
|
onChange={setPrivateAccount}
|
||||||
|
className={classNames(
|
||||||
|
privateAccount ? "bg-teal-500" : "bg-gray-200",
|
||||||
|
"relative ml-4 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
privateAccount ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
<Switch.Group
|
||||||
|
as="li"
|
||||||
|
className="flex items-center justify-between py-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Switch.Label
|
||||||
|
as="p"
|
||||||
|
className="text-sm font-medium text-gray-900"
|
||||||
|
passive
|
||||||
|
>
|
||||||
|
Allow commenting
|
||||||
|
</Switch.Label>
|
||||||
|
<Switch.Description className="text-sm text-gray-500">
|
||||||
|
Integer amet, nunc hendrerit adipiscing nam. Elementum ame
|
||||||
|
</Switch.Description>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={allowCommenting}
|
||||||
|
onChange={setAllowCommenting}
|
||||||
|
className={classNames(
|
||||||
|
allowCommenting ? "bg-teal-500" : "bg-gray-200",
|
||||||
|
"relative ml-4 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
allowCommenting ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
<Switch.Group
|
||||||
|
as="li"
|
||||||
|
className="flex items-center justify-between py-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Switch.Label
|
||||||
|
as="p"
|
||||||
|
className="text-sm font-medium text-gray-900"
|
||||||
|
passive
|
||||||
|
>
|
||||||
|
Allow mentions
|
||||||
|
</Switch.Label>
|
||||||
|
<Switch.Description className="text-sm text-gray-500">
|
||||||
|
Adipiscing est venenatis enim molestie commodo eu gravid
|
||||||
|
</Switch.Description>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={allowMentions}
|
||||||
|
onChange={setAllowMentions}
|
||||||
|
className={classNames(
|
||||||
|
allowMentions ? "bg-teal-500" : "bg-gray-200",
|
||||||
|
"relative ml-4 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
allowMentions ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Switch.Group>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end px-4 py-4 mt-4 sm:px-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex justify-center px-4 py-2 ml-5 text-sm font-medium text-white border border-transparent rounded-md shadow-sm bg-sky-700 hover:bg-sky-800 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePageComponent;
|
||||||
3
src/components/pages/index.ts
Normal file
3
src/components/pages/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import ProfilePageComponent from "./ProfilePageComponent";
|
||||||
|
|
||||||
|
export { ProfilePageComponent };
|
||||||
@@ -6,9 +6,9 @@ import { MdAddAlarm } from "react-icons/md";
|
|||||||
import { error, success, warning } from "./toast/toastService";
|
import { error, success, warning } from "./toast/toastService";
|
||||||
|
|
||||||
const RemindMeButton = ({ show }: { show: Show }) => {
|
const RemindMeButton = ({ show }: { show: Show }) => {
|
||||||
const { authUser } = useFirebaseAuth();
|
const { user } = useFirebaseAuth();
|
||||||
const createShowReminder = async () => {
|
const createShowReminder = async () => {
|
||||||
if (authUser?.uid) {
|
if (user?.uid) {
|
||||||
var response = await fetch(
|
var response = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
|
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ const RemindMeButton = ({ show }: { show: Show }) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: authUser?.uid,
|
userId: user?.uid,
|
||||||
showId: show.id,
|
showId: show.id,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/components/widgets/buttons/social/TwitterButton.tsx
Normal file
25
src/components/widgets/buttons/social/TwitterButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ISocialButtonProps } from "./socialButtonProps";
|
||||||
|
|
||||||
|
const TwitterButton = ({ onClick }: ISocialButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-secondary btn mb-2 flex
|
||||||
|
w-1/3 gap-2 rounded px-6 py-2.5 text-xs font-medium uppercase leading-normal text-white shadow-md transition duration-150 ease-in-out hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg"
|
||||||
|
style={{ backgroundColor: "#1da1f2" }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="mr-2 h-3.5 w-3.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
|
||||||
|
</svg>
|
||||||
|
Twitter
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TwitterButton;
|
||||||
3
src/components/widgets/buttons/social/index.ts
Normal file
3
src/components/widgets/buttons/social/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import TwitterButton from "./TwitterButton";
|
||||||
|
|
||||||
|
export { TwitterButton };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ISocialButtonProps {
|
||||||
|
onClick: () => {};
|
||||||
|
}
|
||||||
40
src/components/widgets/ui/theme/ThemeToggle.tsx
Normal file
40
src/components/widgets/ui/theme/ThemeToggle.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
This component will handle the theme (dark/light). You are able to change the selected theme line 9.
|
||||||
|
DaisyUI have more than 10 themes availables https://daisyui.com/docs/default-themes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HiOutlineMoon, HiOutlineSun } from "react-icons/hi";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { defaults } from "@/lib/constants";
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
primary: defaults.defaultTheme,
|
||||||
|
secondary: defaults.defaultDarkTheme,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeToggle = (): JSX.Element => {
|
||||||
|
const [activeTheme, setActiveTheme] = useState(
|
||||||
|
document.body.dataset.theme || ""
|
||||||
|
);
|
||||||
|
const inactiveTheme =
|
||||||
|
activeTheme === defaults.defaultTheme
|
||||||
|
? defaults.defaultDarkTheme
|
||||||
|
: defaults.defaultTheme;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.dataset.theme = activeTheme;
|
||||||
|
window.localStorage.setItem("theme", activeTheme);
|
||||||
|
}, [activeTheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="flex ml-3" onClick={() => setActiveTheme(inactiveTheme)}>
|
||||||
|
{activeTheme === theme.secondary ? (
|
||||||
|
<HiOutlineSun className="m-auto text-xl hover:text-accent" />
|
||||||
|
) : (
|
||||||
|
<HiOutlineMoon className="m-auto text-xl hover:text-accent" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, {createContext, useContext, Context} from 'react'
|
import React, { createContext, useContext, Context } from "react";
|
||||||
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||||
import { User } from "@/models";
|
import { User } from "@/models";
|
||||||
|
|
||||||
interface IAuthUserContext {
|
interface IAuthUserContext {
|
||||||
authUser: User | undefined;
|
user: User | undefined;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
signIn: (email: string, password: string) => {},
|
signIn: (email: string, password: string) => {},
|
||||||
signUp: (email: string, password: string) => {},
|
signUp: (email: string, password: string) => {},
|
||||||
@@ -15,7 +15,7 @@ interface IAuthUserContext {
|
|||||||
|
|
||||||
const authUserContext
|
const authUserContext
|
||||||
= createContext<IAuthUserContext>({
|
= createContext<IAuthUserContext>({
|
||||||
authUser: undefined,
|
user: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
signIn: async (email: string, password: string) => {
|
signIn: async (email: string, password: string) => {
|
||||||
},
|
},
|
||||||
@@ -28,7 +28,7 @@ const authUserContext
|
|||||||
signInWithTwitter: async () => {
|
signInWithTwitter: async () => {
|
||||||
},
|
},
|
||||||
signInWithFacebook: async () => {
|
signInWithFacebook: async () => {
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AuthUserProvider({ children }: { children: React.ReactNode }) {
|
export function AuthUserProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
12
src/lib/auth/firebase.ts
Normal file
12
src/lib/auth/firebase.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { initializeApp } from "firebase/app";
|
||||||
|
|
||||||
|
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 app = initializeApp(firebaseConfig);
|
||||||
@@ -2,6 +2,7 @@ import firebase, { getApp, getApps, initializeApp } from "firebase/app";
|
|||||||
import "firebase/auth";
|
import "firebase/auth";
|
||||||
// import { getFirestore } from "firebase/firestore";
|
// import { getFirestore } from "firebase/firestore";
|
||||||
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
||||||
authDomain: "radio-otherway.firebaseapp.com",
|
authDomain: "radio-otherway.firebaseapp.com",
|
||||||
@@ -9,7 +10,8 @@ const firebaseConfig = {
|
|||||||
storageBucket: "radio-otherway.appspot.com",
|
storageBucket: "radio-otherway.appspot.com",
|
||||||
messagingSenderId: "47147490249",
|
messagingSenderId: "47147490249",
|
||||||
appId: "1:47147490249:web:a84515b3ce1c481826e618",
|
appId: "1:47147490249:web:a84515b3ce1c481826e618",
|
||||||
measurementId: "G-12YB78EZM4",
|
measurementId: "G-12YB78EZM4"
|
||||||
};
|
};
|
||||||
|
|
||||||
// export default admin.firestore();
|
// export default admin.firestore();
|
||||||
export const app = initializeApp(firebaseConfig);
|
export const app = initializeApp(firebaseConfig);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
createUserWithEmailAndPassword,
|
createUserWithEmailAndPassword,
|
||||||
FacebookAuthProvider,
|
FacebookAuthProvider,
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
signInWithPopup, signInWithRedirect,
|
signInWithPopup, signInWithRedirect,
|
||||||
signOut,
|
signOut,
|
||||||
TwitterAuthProvider
|
TwitterAuthProvider
|
||||||
} from 'firebase/auth';
|
} from "firebase/auth";
|
||||||
import {app} from '../db/firebaseAuth';
|
import { app } from "./firebase";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { User } from "@/models";
|
import { User } from "@/models";
|
||||||
|
|
||||||
@@ -20,27 +20,27 @@ const formatAuthUser = (user: User) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function useFirebaseAuth() {
|
export default function useFirebaseAuth() {
|
||||||
const auth = getAuth(app)
|
const auth = getAuth(app);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [authUser, setAuthUser] = useState<User | undefined>(undefined);
|
const [user, setUser] = useState<User | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const authStateChanged = async (authState: any) => {
|
const authStateChanged = async (authState: any) => {
|
||||||
if (!authState) {
|
if (!authState) {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
var formattedUser = formatAuthUser(authState);
|
var formattedUser = formatAuthUser(authState);
|
||||||
setAuthUser(formattedUser);
|
setUser(formattedUser);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
setAuthUser(undefined);
|
setUser(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,47 +52,47 @@ export default function useFirebaseAuth() {
|
|||||||
|
|
||||||
const logOut = () =>
|
const logOut = () =>
|
||||||
signOut(auth).then(clear);
|
signOut(auth).then(clear);
|
||||||
const signInWithGoogle = () => {
|
const signInWithGoogle = async () => {
|
||||||
const provider = new GoogleAuthProvider();
|
const provider = new GoogleAuthProvider();
|
||||||
provider.setCustomParameters({prompt: 'select_account'});
|
provider.setCustomParameters({ prompt: "select_account" });
|
||||||
|
|
||||||
return signInWithPopup(auth, provider)
|
try {
|
||||||
.then((result) => {
|
const result = await signInWithPopup(auth, provider);
|
||||||
const credential = GoogleAuthProvider.credentialFromResult(result);
|
const credential = GoogleAuthProvider.credentialFromResult(result);
|
||||||
if (credential) {
|
if (credential) {
|
||||||
const token = credential.accessToken;
|
const token = credential.accessToken;
|
||||||
const user = result.user;
|
// const user = result.user;
|
||||||
}
|
}
|
||||||
router.push('/');
|
router.push("/");
|
||||||
})
|
} catch (error: any) {
|
||||||
.catch((error) => {
|
|
||||||
const errorCode = error.code;
|
const errorCode = error.code;
|
||||||
const errorMessage = error.message;
|
const errorMessage = error.message;
|
||||||
const email = error.email;
|
const email = error.email;
|
||||||
const credential = GoogleAuthProvider.credentialFromError(error);
|
const credential = GoogleAuthProvider.credentialFromError(error);
|
||||||
});
|
|
||||||
}
|
|
||||||
const signInWithTwitter = () => {
|
|
||||||
const provider = new TwitterAuthProvider();
|
|
||||||
|
|
||||||
return signInWithPopup(auth, provider)
|
|
||||||
.then((result) => {
|
|
||||||
const credential = TwitterAuthProvider.credentialFromResult(result);
|
|
||||||
if (credential) {
|
|
||||||
const token = credential.accessToken;
|
|
||||||
const user = result.user;
|
|
||||||
}
|
}
|
||||||
router.push('/');
|
};
|
||||||
})
|
const signInWithTwitter = () => {
|
||||||
.catch((error) => {
|
// const provider = new TwitterAuthProvider();
|
||||||
const errorCode = error.code;
|
//
|
||||||
const errorMessage = error.message;
|
// return auth.signInWithPopup(auth, provider)
|
||||||
const email = error.email;
|
// .then((result) => {
|
||||||
const credential = GoogleAuthProvider.credentialFromError(error);
|
// const credential = TwitterAuthProvider.credentialFromResult(result);
|
||||||
});
|
// if (credential) {
|
||||||
|
// const token = credential.accessToken;
|
||||||
|
// const user = result.user;
|
||||||
|
// }
|
||||||
|
// router.push("/");
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// const errorCode = error.code;
|
||||||
|
// const errorMessage = error.message;
|
||||||
|
// const email = error.email;
|
||||||
|
// const credential = GoogleAuthProvider.credentialFromError(error);
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
const signInWithFacebook = () => {
|
const signInWithFacebook = () => {
|
||||||
const provider = new FacebookAuthProvider()
|
const provider = new FacebookAuthProvider();
|
||||||
return signInWithPopup(auth, provider)
|
return signInWithPopup(auth, provider)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const credential = TwitterAuthProvider.credentialFromResult(result);
|
const credential = TwitterAuthProvider.credentialFromResult(result);
|
||||||
@@ -100,7 +100,7 @@ export default function useFirebaseAuth() {
|
|||||||
const token = credential.accessToken;
|
const token = credential.accessToken;
|
||||||
const user = result.user;
|
const user = result.user;
|
||||||
}
|
}
|
||||||
router.push('/');
|
router.push("/");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorCode = error.code;
|
const errorCode = error.code;
|
||||||
@@ -112,11 +112,11 @@ export default function useFirebaseAuth() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = onAuthStateChanged(auth, authStateChanged);
|
const unsubscribe = onAuthStateChanged(auth, authStateChanged);
|
||||||
return unsubscribe
|
return unsubscribe;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authUser,
|
user,
|
||||||
loading,
|
loading,
|
||||||
signIn,
|
signIn,
|
||||||
signUp,
|
signUp,
|
||||||
|
|||||||
4
src/lib/constants/defaults.ts
Normal file
4
src/lib/constants/defaults.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const defaults = {
|
||||||
|
defaultTheme: "bumblebee",
|
||||||
|
defaultDarkTheme: "dark",
|
||||||
|
};
|
||||||
3
src/lib/constants/index.ts
Normal file
3
src/lib/constants/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { defaults } from "./defaults";
|
||||||
|
|
||||||
|
export { defaults };
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import serviceAccount from "serviceAccount.json";
|
import serviceAccount from "serviceAccount.json";
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "firebase/app";
|
||||||
import { getFirestore, CollectionReference, collection, DocumentData } from "firebase/firestore";
|
import {
|
||||||
|
getFirestore,
|
||||||
|
CollectionReference,
|
||||||
|
collection,
|
||||||
|
DocumentData,
|
||||||
|
WithFieldValue,
|
||||||
|
QueryDocumentSnapshot, SnapshotOptions, Timestamp
|
||||||
|
} from "firebase/firestore";
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
||||||
@@ -16,13 +23,32 @@ const firestore = getFirestore();
|
|||||||
const createCollection = <T = DocumentData>(collectionName: string) => {
|
const createCollection = <T = DocumentData>(collectionName: string) => {
|
||||||
return collection(firestore, collectionName) as CollectionReference<T>;
|
return collection(firestore, collectionName) as CollectionReference<T>;
|
||||||
};
|
};
|
||||||
|
const showConverter = {
|
||||||
|
toFirestore(show: WithFieldValue<Show>): DocumentData {
|
||||||
|
return {
|
||||||
|
...show,
|
||||||
|
date: Timestamp.fromDate(<Date>show.date)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fromFirestore(
|
||||||
|
snapshot: QueryDocumentSnapshot,
|
||||||
|
options: SnapshotOptions
|
||||||
|
): Show {
|
||||||
|
const data = snapshot.data(options)!;
|
||||||
|
return new Show(
|
||||||
|
snapshot.id,
|
||||||
|
data.title,
|
||||||
|
data.date.toDate(),
|
||||||
|
data.creator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Import all your model types
|
// Import all your model types
|
||||||
import { Show, Reminder } from "@/models";
|
import { Show, Reminder, RemindersProcessed } from "@/models";
|
||||||
// export all your collections
|
// export all your collections
|
||||||
|
|
||||||
export const shows = createCollection<Show>("shows");
|
export const shows = createCollection<Show>("shows")
|
||||||
|
.withConverter(showConverter);
|
||||||
export const reminders = createCollection<Reminder>("reminders");
|
export const reminders = createCollection<Reminder>("reminders");
|
||||||
|
export const remindersProcessed = createCollection<RemindersProcessed>("reminders");
|
||||||
export default firestore;
|
export default firestore;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
const getMonthName = (date: string) =>
|
const getMonthName = (date: Date) => {
|
||||||
new Date(date).toLocaleString("default", { month: "short" });
|
return date.toLocaleString("default", { month: "short" });
|
||||||
const getTime = (date: string) =>
|
};
|
||||||
new Date(date).toLocaleTimeString("en-IE", { timeStyle: "short" });
|
const getTime = (date: Date) =>
|
||||||
|
date.toLocaleTimeString("en-IE", { timeStyle: "short" });
|
||||||
|
|
||||||
const getStartOfToday = (admin: any) => {
|
const getStartOfToday = (admin: any) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Show } from "@/models";
|
import { Show } from "@/models";
|
||||||
|
|
||||||
const { google } = require("googleapis");
|
const { google } = require("googleapis");
|
||||||
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
|
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
|
||||||
const GOOGLE_CLIENT_EMAIL =
|
const GOOGLE_CLIENT_EMAIL =
|
||||||
@@ -16,7 +17,7 @@ const jwtClient = new google.auth.JWT(
|
|||||||
const calendar = google.calendar({
|
const calendar = google.calendar({
|
||||||
version: "v3",
|
version: "v3",
|
||||||
project: GOOGLE_PROJECT_NUMBER,
|
project: GOOGLE_PROJECT_NUMBER,
|
||||||
auth: jwtClient,
|
auth: jwtClient
|
||||||
});
|
});
|
||||||
const getCalendarEntries = async () => {
|
const getCalendarEntries = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -25,10 +26,18 @@ const getCalendarEntries = async () => {
|
|||||||
timeMin: new Date().toISOString(),
|
timeMin: new Date().toISOString(),
|
||||||
maxResults: 10,
|
maxResults: 10,
|
||||||
singleEvents: true,
|
singleEvents: true,
|
||||||
orderBy: "startTime",
|
orderBy: "startTime"
|
||||||
});
|
});
|
||||||
return events.data.items;
|
return events.data.items.map((r: any) => {
|
||||||
} catch (err) {}
|
return {
|
||||||
|
id: r.id,
|
||||||
|
title: r.summary,
|
||||||
|
date: r.start.dateTime,
|
||||||
|
creator: r.creator.email
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import User from "./user";
|
|||||||
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 type { RemindersProcessed } from "./processes";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
User,
|
User,
|
||||||
Show,
|
Show,
|
||||||
Reminder,
|
Reminder,
|
||||||
Notification
|
Notification,
|
||||||
|
RemindersProcessed
|
||||||
};
|
};
|
||||||
|
|||||||
6
src/models/processes.ts
Normal file
6
src/models/processes.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
interface RemindersProcessed {
|
||||||
|
reminderId: string;
|
||||||
|
dateProcessed: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { RemindersProcessed };
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import Notification from "./notification";
|
import Notification from "./notification";
|
||||||
|
|
||||||
export default class Reminder {
|
export default class Reminder {
|
||||||
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
showId: string;
|
showId: string;
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
created: Date;
|
created: Date;
|
||||||
|
|
||||||
constructor(userId: string,
|
constructor(id: string,
|
||||||
|
userId: string,
|
||||||
showId: string,
|
showId: string,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
created: Date) {
|
created: Date) {
|
||||||
|
this.id = id;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.showId = showId;
|
this.showId = showId;
|
||||||
this.notifications = notifications.map(n => Notification.fromJson(n));
|
this.notifications = notifications.map(n => Notification.fromJson(n));
|
||||||
@@ -18,6 +21,7 @@ export default class Reminder {
|
|||||||
|
|
||||||
static fromJson(r: any): Reminder {
|
static fromJson(r: any): Reminder {
|
||||||
return new Reminder(
|
return new Reminder(
|
||||||
|
r.id,
|
||||||
r.userId,
|
r.userId,
|
||||||
r.showId,
|
r.showId,
|
||||||
r.notifications,
|
r.notifications,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ export default class Show {
|
|||||||
date: Date;
|
date: Date;
|
||||||
creator: string;
|
creator: string;
|
||||||
|
|
||||||
constructor(id: string, title: string, date: string, creator: string) {
|
constructor(id: string, title: string, date: Date, creator: string) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.date = new Date(date);
|
this.date = date;
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export default class Show {
|
|||||||
return new Show(
|
return new Show(
|
||||||
r.id,
|
r.id,
|
||||||
r.title,
|
r.title,
|
||||||
r.date,
|
new Date(r.date),
|
||||||
r.creator);
|
r.creator);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
|
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
|
||||||
import { shows } from "@/lib/db";
|
|
||||||
import { Show } from "@/models";
|
|
||||||
import logger from "@/lib/util/logging";
|
import logger from "@/lib/util/logging";
|
||||||
import { doc, setDoc } from "@firebase/firestore";
|
import { doc, setDoc } from "@firebase/firestore";
|
||||||
|
import { shows } from "@/lib/db";
|
||||||
|
import { Show } from "@/models";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
try {
|
try {
|
||||||
const entries = await getCalendarEntries();
|
const e = await getCalendarEntries();
|
||||||
const shows = entries.map((r: any) => Show.fromJson(r));
|
const entries = e.map((r: any) => Show.fromJson(r));
|
||||||
for (const show of shows) {
|
for (const entry of entries) {
|
||||||
logger.debug("Storing show", show);
|
logger.debug("Storing show", entry);
|
||||||
const showRef = doc(shows, show.id);
|
const showRef = doc(shows, entry.id);
|
||||||
await setDoc(showRef, {
|
await setDoc(showRef, {
|
||||||
title: show.title,
|
title: entry.title,
|
||||||
date: show.date,
|
date: entry.date,
|
||||||
creator: show.creator
|
creator: entry.creator
|
||||||
}, { merge: true });
|
}, { merge: true });
|
||||||
}
|
}
|
||||||
logger.debug("Stored show", res);
|
logger.debug("Stored show", res);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { shows, reminders } from "@/lib/db";
|
import { shows, reminders, remindersProcessed } from "@/lib/db";
|
||||||
import { addSeconds, dateDifferenceInSeconds } from "@/lib/util/dateUtils";
|
import { addSeconds, dateDifferenceInSeconds } from "@/lib/util/dateUtils";
|
||||||
import { Notification, Reminder, Show } from "@/models";
|
import { Notification, Reminder, Show } from "@/models";
|
||||||
import { sendSMS } from "@/lib/util/notifications/sms";
|
import { sendSMS } from "@/lib/util/notifications/sms";
|
||||||
import { doc, getDocs, query, where } from "@firebase/firestore";
|
import { doc, getDocs, query, setDoc, where } from "@firebase/firestore";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
//this handler is called by whatever CRON mechanism we're using
|
//this handler is called by whatever CRON mechanism we're using
|
||||||
@@ -11,15 +11,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
// if any shows are coming up and send out reminders.
|
// if any shows are coming up and send out reminders.
|
||||||
|
|
||||||
//get all the shows that are on today
|
//get all the shows that are on today
|
||||||
const upcoming = await getDocs(query(shows, where("date", ">", new Date())));
|
//, where("date", ">", new Date())
|
||||||
// const shows = await db.shows;
|
|
||||||
// .where("date", ">", new Date())
|
|
||||||
// .where("date", "<", addSeconds(new Date(), 60 * 60))
|
|
||||||
// .get()
|
|
||||||
// ;
|
|
||||||
|
|
||||||
|
const q = query(
|
||||||
|
shows,
|
||||||
|
where("date", ">", new Date()),
|
||||||
|
where("date", "<", addSeconds(new Date(), 60 * 60 * 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
;
|
||||||
|
const upcoming = await getDocs(q);
|
||||||
for (const s of upcoming.docs) {
|
for (const s of upcoming.docs) {
|
||||||
const show = s.data() as Show;
|
const show = s.data();
|
||||||
//load all the reminders for this show
|
//load all the reminders for this show
|
||||||
const activeReminders = await getDocs(query(reminders, where("showId", "==", show.id)));
|
const activeReminders = await getDocs(query(reminders, where("showId", "==", show.id)));
|
||||||
|
|
||||||
@@ -27,12 +30,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
//due in the next 5 minutes should get queued
|
//due in the next 5 minutes should get queued
|
||||||
for (const r of activeReminders.docs) {
|
for (const r of activeReminders.docs) {
|
||||||
const reminder = r.data() as Reminder;
|
const reminder = r.data() as Reminder;
|
||||||
for (let n in reminder.notifications) {
|
for (let notification of reminder.notifications) {
|
||||||
const notification = Notification.fromJson(n);
|
const targetDate = addSeconds(new Date(), notification.secondsBefore);
|
||||||
const targetDate = addSeconds(new Date(), notification.secondsBefore * -1);
|
const differenceInSeconds = dateDifferenceInSeconds(targetDate, show.date);
|
||||||
if (dateDifferenceInSeconds(targetDate, new Date(show.date)) <= 5 * 60) {
|
if (differenceInSeconds >= 0) {
|
||||||
//time to fire off a notification
|
//time to fire off a notification
|
||||||
await sendSMS("353868065119", "New show starting in 1 hour");
|
await sendSMS("353868065119", "New show starting in 1 hour");
|
||||||
|
const docKey = `${reminder.userId}_${reminder.showId}`;
|
||||||
|
const processedRef = doc(remindersProcessed, docKey);
|
||||||
|
|
||||||
|
await setDoc(processedRef, {
|
||||||
|
reminderId: reminder.id,
|
||||||
|
dateProcessed: new Date()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import db from "@/lib/db";
|
import { reminders, shows } from "@/lib/db";
|
||||||
|
import { addDoc, doc, setDoc } from "@firebase/firestore";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const { userId, showId } = req.body;
|
const { userId, showId } = req.body;
|
||||||
const docKey = `${userId}_${showId}`;
|
const docKey = `${userId}_${showId}`;
|
||||||
const remindersRef = db.collection("reminders");
|
const remindersRef = doc(reminders, docKey);
|
||||||
const dbShow = remindersRef.doc(docKey);
|
await setDoc(remindersRef, {
|
||||||
|
|
||||||
const reminder = await remindersRef.doc(docKey).set({
|
|
||||||
userId,
|
userId,
|
||||||
showId,
|
showId,
|
||||||
created: new Date(),
|
|
||||||
notifications: [
|
notifications: [
|
||||||
{ secondsBefore: 60 * 60, destination: "353868065119" } //just set a single reminder for an hour beforehand
|
{ secondsBefore: 60 * 60, destination: "353868065119" } //just set a single reminder for an hour beforehand
|
||||||
]
|
]
|
||||||
}, { merge: true });
|
}, { merge: true });
|
||||||
res.status(201).json(reminder);
|
res.status(201);
|
||||||
} else {
|
} else {
|
||||||
res.status(405);
|
res.status(405);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import db from "@/lib/db";
|
import db, { shows } from "@/lib/db";
|
||||||
|
import { getDocs, query, where } from "@firebase/firestore";
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const shows = await db
|
const q = query(
|
||||||
.collection("shows")
|
shows,
|
||||||
.orderBy("date")
|
where("date", ">", new Date())
|
||||||
.get();
|
);
|
||||||
res.status(200).json(shows.docs.map(show => {
|
const upcoming = await getDocs(q);
|
||||||
const result = {
|
res.status(200).json(upcoming.docs.map(r => r.data()));
|
||||||
id: show.id,
|
|
||||||
...show.data()
|
|
||||||
};
|
|
||||||
return result;
|
|
||||||
}));
|
|
||||||
res.end();
|
res.end();
|
||||||
};
|
};
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -10,4 +10,64 @@ module.exports = {
|
|||||||
// },
|
// },
|
||||||
// ],
|
// ],
|
||||||
plugins: [require("daisyui")],
|
plugins: [require("daisyui")],
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
radioTheme: {
|
||||||
|
primary: "#00B8F0",
|
||||||
|
"primary-focus": "#009de0",
|
||||||
|
"primary-content": "#ffffff",
|
||||||
|
|
||||||
|
secondary: "#f03800",
|
||||||
|
"secondary-focus": "#e22f00",
|
||||||
|
"secondary-content": "#ffffff",
|
||||||
|
|
||||||
|
accent: "#00f0b0",
|
||||||
|
"accent-focus": "#00e28a",
|
||||||
|
"accent-content": "#ffffff",
|
||||||
|
|
||||||
|
neutral: "#3d4451",
|
||||||
|
"neutral-focus": "#2a2e37",
|
||||||
|
"neutral-content": "#ffffff",
|
||||||
|
|
||||||
|
"base-100": "#ffffff",
|
||||||
|
"base-200": "#767676",
|
||||||
|
"base-300": "#d1d5db",
|
||||||
|
"base-content": "#1f2937",
|
||||||
|
|
||||||
|
info: "#2094f3" /* Info */,
|
||||||
|
success: "#009485" /* Success */,
|
||||||
|
warning: "#ff9900" /* Warning */,
|
||||||
|
error: "#ff5724" /* Error */,
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
primary: "#00B8F0",
|
||||||
|
"primary-focus": "#009de0",
|
||||||
|
"primary-content": "#ffffff",
|
||||||
|
|
||||||
|
secondary: "#f03800",
|
||||||
|
"secondary-focus": "#e22f00",
|
||||||
|
"secondary-content": "#ffffff",
|
||||||
|
|
||||||
|
accent: "#00f0b0",
|
||||||
|
"accent-focus": "#00e28a",
|
||||||
|
"accent-content": "#ffffff",
|
||||||
|
|
||||||
|
neutral: "#3d4451",
|
||||||
|
"neutral-focus": "#2a2e37",
|
||||||
|
"neutral-content": "#ffffff",
|
||||||
|
|
||||||
|
"base-100": "#2A2E37",
|
||||||
|
"base-200": "#EBECF0",
|
||||||
|
"base-300": "#16181D",
|
||||||
|
"base-content": "#EBECF0",
|
||||||
|
|
||||||
|
info: "#2094f3",
|
||||||
|
success: "#009485",
|
||||||
|
warning: "#ff9900",
|
||||||
|
error: "#ff5724",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user