mirror of
https://github.com/fergalmoran/radio-otherway.git
synced 2025-12-22 09:50:29 +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=
|
||||
|
||||
#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.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
@@ -18,22 +18,27 @@
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@upstash/qstash": "^0.3.6",
|
||||
"classnames": "^2.3.2",
|
||||
"daisyui": "^2.49.0",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-config-next": "13.1.5",
|
||||
"firebase": "^9.17.1",
|
||||
"firebase-admin": "^11.5.0",
|
||||
"firebase-functions": "^4.2.1",
|
||||
"fireschema": "^4.0.4",
|
||||
"next": "13.1.5",
|
||||
"next-logger": "^3.0.1",
|
||||
"next-seo": "^5.15.0",
|
||||
"pino": "^8.11.0",
|
||||
"pino-logflare": "^0.3.12",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-icons": "^4.7.1",
|
||||
"twilio": "^4.8.0",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "4.9.4",
|
||||
"zod": "^3.20.6"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,7 +2,12 @@ import { LoginPage } from "@/components/auth";
|
||||
import React from "react";
|
||||
|
||||
const Login = async () => {
|
||||
return <LoginPage />;
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap justify-evenly w-full">
|
||||
<LoginPage />
|
||||
</div>
|
||||
</>);
|
||||
};
|
||||
|
||||
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 { NavBar } from "@/components/layout";
|
||||
import { AuthUserProvider } from "@/lib/auth/authUserContext";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
// const Toaster = dynamic(
|
||||
// () => import("react-hot-toast").then((c) => c.Toaster),
|
||||
// {
|
||||
// ssr: false,
|
||||
// }
|
||||
// );
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
@@ -24,9 +18,15 @@ export default function RootLayout({
|
||||
<head />
|
||||
<body className={`${inter.className} h-screen`}>
|
||||
<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 />
|
||||
<NavBar />
|
||||
<div className="-mt-[4rem]">{children}</div>
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthUserProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,13 +2,24 @@ import React from "react";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<div
|
||||
className="inline-block w-8 h-8 border-4 rounded-full spinner-border animate-spin"
|
||||
role="status"
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
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>
|
||||
</div>
|
||||
<path
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ const getData = async (): Promise<Show[]> => {
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`
|
||||
);
|
||||
logger.debug("getDate", res);
|
||||
return await res.json();
|
||||
const data = await res.json();
|
||||
return data.map((r: string) => Show.fromJson(r));
|
||||
};
|
||||
|
||||
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";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {BsFacebook, BsGoogle, BsTwitter} from "react-icons/bs";
|
||||
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
|
||||
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
|
||||
|
||||
const LoginPage = () => {
|
||||
const {signInWithGoogle, signInWithTwitter, signInWithFacebook} = useFirebaseAuth();
|
||||
const router = useRouter()
|
||||
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, user, signIn } =
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-100 flex flex-col justify-center sm:py-12">
|
||||
<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">
|
||||
Login to Radio Otherway
|
||||
</h1>
|
||||
<div className="bg-white shadow w-full rounded-lg">
|
||||
<div className="px-5 py-7">
|
||||
<label className="font-semibold text-sm text-gray-600 pb-1 block">E-mail</label>
|
||||
<input type="text" className="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"/>
|
||||
<label className="font-semibold text-sm text-gray-600 pb-1 block">Password</label>
|
||||
<input type="text" className="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"/>
|
||||
<button type="button"
|
||||
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">
|
||||
<span className="inline-block mr-2">Login</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
className="w-4 h-4 inline-block">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
|
||||
</svg>
|
||||
</button>
|
||||
<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">
|
||||
Account Login
|
||||
</h3>
|
||||
<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="relative flex py-5 items-center">
|
||||
<div className="flex-grow border-t border-gray-400"></div>
|
||||
<span className="flex-shrink mx-4 text-gray-400">or login with</span>
|
||||
<div className="flex-grow border-t border-gray-400"></div>
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="password" className="text-sm">
|
||||
Password
|
||||
</label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
className="w-4 h-4 inline-block align-text-top">
|
||||
<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"/>
|
||||
</svg>
|
||||
<span className="inline-block ml-1">Forgot Password</span>
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setForgot(true);
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:underline focus:text-blue-800"
|
||||
>
|
||||
Forgot Password?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className="input-bordered input-primary input input-sm"
|
||||
value={password}
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</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 Link from "next/link";
|
||||
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 {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 (
|
||||
<div
|
||||
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">
|
||||
<nav className="w-full navbar">
|
||||
<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>
|
||||
<nav className="w-full mb-2 navbar">
|
||||
<Link href="/">
|
||||
<Image src="/logo.png" alt="Otherway" width={32} height={32} />
|
||||
</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>
|
||||
</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";
|
||||
|
||||
const RemindMeButton = ({ show }: { show: Show }) => {
|
||||
const { authUser } = useFirebaseAuth();
|
||||
const { user } = useFirebaseAuth();
|
||||
const createShowReminder = async () => {
|
||||
if (authUser?.uid) {
|
||||
if (user?.uid) {
|
||||
var response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/reminders`,
|
||||
{
|
||||
@@ -17,7 +17,7 @@ const RemindMeButton = ({ show }: { show: Show }) => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: authUser?.uid,
|
||||
userId: user?.uid,
|
||||
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 { User } from "@/models";
|
||||
|
||||
interface IAuthUserContext {
|
||||
authUser: User | undefined;
|
||||
user: User | undefined;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => {},
|
||||
signUp: (email: string, password: string) => {},
|
||||
@@ -15,7 +15,7 @@ interface IAuthUserContext {
|
||||
|
||||
const authUserContext
|
||||
= createContext<IAuthUserContext>({
|
||||
authUser: undefined,
|
||||
user: undefined,
|
||||
loading: true,
|
||||
signIn: async (email: string, password: string) => {
|
||||
},
|
||||
@@ -28,7 +28,7 @@ const authUserContext
|
||||
signInWithTwitter: async () => {
|
||||
},
|
||||
signInWithFacebook: async () => {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
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 { getFirestore } from "firebase/firestore";
|
||||
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
||||
authDomain: "radio-otherway.firebaseapp.com",
|
||||
@@ -9,7 +10,8 @@ const firebaseConfig = {
|
||||
storageBucket: "radio-otherway.appspot.com",
|
||||
messagingSenderId: "47147490249",
|
||||
appId: "1:47147490249:web:a84515b3ce1c481826e618",
|
||||
measurementId: "G-12YB78EZM4",
|
||||
measurementId: "G-12YB78EZM4"
|
||||
};
|
||||
|
||||
// export default admin.firestore();
|
||||
export const app = initializeApp(firebaseConfig);
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
createUserWithEmailAndPassword,
|
||||
FacebookAuthProvider,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
signInWithPopup, signInWithRedirect,
|
||||
signOut,
|
||||
TwitterAuthProvider
|
||||
} from 'firebase/auth';
|
||||
import {app} from '../db/firebaseAuth';
|
||||
} from "firebase/auth";
|
||||
import { app } from "./firebase";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User } from "@/models";
|
||||
|
||||
@@ -20,27 +20,27 @@ const formatAuthUser = (user: User) => ({
|
||||
});
|
||||
|
||||
export default function useFirebaseAuth() {
|
||||
const auth = getAuth(app)
|
||||
const auth = getAuth(app);
|
||||
const router = useRouter();
|
||||
|
||||
const [authUser, setAuthUser] = useState<User | undefined>(undefined);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const authStateChanged = async (authState: any) => {
|
||||
if (!authState) {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
var formattedUser = formatAuthUser(authState);
|
||||
setAuthUser(formattedUser);
|
||||
setUser(formattedUser);
|
||||
setLoading(false);
|
||||
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setAuthUser(undefined);
|
||||
setUser(undefined);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
@@ -52,47 +52,47 @@ export default function useFirebaseAuth() {
|
||||
|
||||
const logOut = () =>
|
||||
signOut(auth).then(clear);
|
||||
const signInWithGoogle = () => {
|
||||
const signInWithGoogle = async () => {
|
||||
const provider = new GoogleAuthProvider();
|
||||
provider.setCustomParameters({prompt: 'select_account'});
|
||||
provider.setCustomParameters({ prompt: "select_account" });
|
||||
|
||||
return signInWithPopup(auth, provider)
|
||||
.then((result) => {
|
||||
try {
|
||||
const result = await signInWithPopup(auth, provider);
|
||||
const credential = GoogleAuthProvider.credentialFromResult(result);
|
||||
if (credential) {
|
||||
const token = credential.accessToken;
|
||||
const user = result.user;
|
||||
// const user = result.user;
|
||||
}
|
||||
router.push('/');
|
||||
})
|
||||
.catch((error) => {
|
||||
router.push("/");
|
||||
} catch (error: any) {
|
||||
const errorCode = error.code;
|
||||
const errorMessage = error.message;
|
||||
const email = error.email;
|
||||
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('/');
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorCode = error.code;
|
||||
const errorMessage = error.message;
|
||||
const email = error.email;
|
||||
const credential = GoogleAuthProvider.credentialFromError(error);
|
||||
});
|
||||
};
|
||||
const signInWithTwitter = () => {
|
||||
// const provider = new TwitterAuthProvider();
|
||||
//
|
||||
// return auth.signInWithPopup(auth, provider)
|
||||
// .then((result) => {
|
||||
// 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 provider = new FacebookAuthProvider()
|
||||
const provider = new FacebookAuthProvider();
|
||||
return signInWithPopup(auth, provider)
|
||||
.then((result) => {
|
||||
const credential = TwitterAuthProvider.credentialFromResult(result);
|
||||
@@ -100,7 +100,7 @@ export default function useFirebaseAuth() {
|
||||
const token = credential.accessToken;
|
||||
const user = result.user;
|
||||
}
|
||||
router.push('/');
|
||||
router.push("/");
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorCode = error.code;
|
||||
@@ -112,11 +112,11 @@ export default function useFirebaseAuth() {
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, authStateChanged);
|
||||
return unsubscribe
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
authUser,
|
||||
user,
|
||||
loading,
|
||||
signIn,
|
||||
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 { 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 = {
|
||||
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
|
||||
@@ -16,13 +23,32 @@ const firestore = getFirestore();
|
||||
const createCollection = <T = DocumentData>(collectionName: string) => {
|
||||
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 { Show, Reminder } from "@/models";
|
||||
import { Show, Reminder, RemindersProcessed } from "@/models";
|
||||
// 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 remindersProcessed = createCollection<RemindersProcessed>("reminders");
|
||||
export default firestore;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const getMonthName = (date: string) =>
|
||||
new Date(date).toLocaleString("default", { month: "short" });
|
||||
const getTime = (date: string) =>
|
||||
new Date(date).toLocaleTimeString("en-IE", { timeStyle: "short" });
|
||||
const getMonthName = (date: Date) => {
|
||||
return date.toLocaleString("default", { month: "short" });
|
||||
};
|
||||
const getTime = (date: Date) =>
|
||||
date.toLocaleTimeString("en-IE", { timeStyle: "short" });
|
||||
|
||||
const getStartOfToday = (admin: any) => {
|
||||
const now = new Date();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Show } from "@/models";
|
||||
|
||||
const { google } = require("googleapis");
|
||||
const GOOGLE_PRIVATE_KEY = process.env.GOOGLE_CALENDAR_CREDENTIALS_PRIVATE_KEY;
|
||||
const GOOGLE_CLIENT_EMAIL =
|
||||
@@ -16,7 +17,7 @@ const jwtClient = new google.auth.JWT(
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
project: GOOGLE_PROJECT_NUMBER,
|
||||
auth: jwtClient,
|
||||
auth: jwtClient
|
||||
});
|
||||
const getCalendarEntries = async () => {
|
||||
try {
|
||||
@@ -25,10 +26,18 @@ const getCalendarEntries = async () => {
|
||||
timeMin: new Date().toISOString(),
|
||||
maxResults: 10,
|
||||
singleEvents: true,
|
||||
orderBy: "startTime",
|
||||
orderBy: "startTime"
|
||||
});
|
||||
return events.data.items;
|
||||
} catch (err) {}
|
||||
return events.data.items.map((r: any) => {
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.summary,
|
||||
date: r.start.dateTime,
|
||||
creator: r.creator.email
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import User from "./user";
|
||||
import Show from "./show";
|
||||
import Reminder from "./reminder";
|
||||
import Notification from "./notification";
|
||||
import type { RemindersProcessed } from "./processes";
|
||||
|
||||
export {
|
||||
User,
|
||||
Show,
|
||||
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";
|
||||
|
||||
export default class Reminder {
|
||||
id: string;
|
||||
userId: string;
|
||||
showId: string;
|
||||
notifications: Notification[];
|
||||
created: Date;
|
||||
|
||||
constructor(userId: string,
|
||||
constructor(id: string,
|
||||
userId: string,
|
||||
showId: string,
|
||||
notifications: [],
|
||||
created: Date) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.showId = showId;
|
||||
this.notifications = notifications.map(n => Notification.fromJson(n));
|
||||
@@ -18,6 +21,7 @@ export default class Reminder {
|
||||
|
||||
static fromJson(r: any): Reminder {
|
||||
return new Reminder(
|
||||
r.id,
|
||||
r.userId,
|
||||
r.showId,
|
||||
r.notifications,
|
||||
|
||||
@@ -4,10 +4,10 @@ export default class Show {
|
||||
date: Date;
|
||||
creator: string;
|
||||
|
||||
constructor(id: string, title: string, date: string, creator: string) {
|
||||
constructor(id: string, title: string, date: Date, creator: string) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.date = new Date(date);
|
||||
this.date = date;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class Show {
|
||||
return new Show(
|
||||
r.id,
|
||||
r.title,
|
||||
r.date,
|
||||
new Date(r.date),
|
||||
r.creator);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getCalendarEntries } from "@/lib/util/google/calendarReader";
|
||||
import { shows } from "@/lib/db";
|
||||
import { Show } from "@/models";
|
||||
import logger from "@/lib/util/logging";
|
||||
import { doc, setDoc } from "@firebase/firestore";
|
||||
import { shows } from "@/lib/db";
|
||||
import { Show } from "@/models";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const entries = await getCalendarEntries();
|
||||
const shows = entries.map((r: any) => Show.fromJson(r));
|
||||
for (const show of shows) {
|
||||
logger.debug("Storing show", show);
|
||||
const showRef = doc(shows, show.id);
|
||||
const e = await getCalendarEntries();
|
||||
const entries = e.map((r: any) => Show.fromJson(r));
|
||||
for (const entry of entries) {
|
||||
logger.debug("Storing show", entry);
|
||||
const showRef = doc(shows, entry.id);
|
||||
await setDoc(showRef, {
|
||||
title: show.title,
|
||||
date: show.date,
|
||||
creator: show.creator
|
||||
title: entry.title,
|
||||
date: entry.date,
|
||||
creator: entry.creator
|
||||
}, { merge: true });
|
||||
}
|
||||
logger.debug("Stored show", res);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { Notification, Reminder, Show } from "@/models";
|
||||
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) => {
|
||||
//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.
|
||||
|
||||
//get all the shows that are on today
|
||||
const upcoming = await getDocs(query(shows, where("date", ">", new Date())));
|
||||
// const shows = await db.shows;
|
||||
// .where("date", ">", new Date())
|
||||
// .where("date", "<", addSeconds(new Date(), 60 * 60))
|
||||
// .get()
|
||||
// ;
|
||||
//, where("date", ">", new Date())
|
||||
|
||||
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) {
|
||||
const show = s.data() as Show;
|
||||
const show = s.data();
|
||||
//load all the reminders for this show
|
||||
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
|
||||
for (const r of activeReminders.docs) {
|
||||
const reminder = r.data() as Reminder;
|
||||
for (let n in reminder.notifications) {
|
||||
const notification = Notification.fromJson(n);
|
||||
const targetDate = addSeconds(new Date(), notification.secondsBefore * -1);
|
||||
if (dateDifferenceInSeconds(targetDate, new Date(show.date)) <= 5 * 60) {
|
||||
for (let notification of reminder.notifications) {
|
||||
const targetDate = addSeconds(new Date(), notification.secondsBefore);
|
||||
const differenceInSeconds = dateDifferenceInSeconds(targetDate, show.date);
|
||||
if (differenceInSeconds >= 0) {
|
||||
//time to fire off a notification
|
||||
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 db from "@/lib/db";
|
||||
import { reminders, shows } from "@/lib/db";
|
||||
import { addDoc, doc, setDoc } from "@firebase/firestore";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "POST") {
|
||||
const { userId, showId } = req.body;
|
||||
const docKey = `${userId}_${showId}`;
|
||||
const remindersRef = db.collection("reminders");
|
||||
const dbShow = remindersRef.doc(docKey);
|
||||
|
||||
const reminder = await remindersRef.doc(docKey).set({
|
||||
const remindersRef = doc(reminders, docKey);
|
||||
await setDoc(remindersRef, {
|
||||
userId,
|
||||
showId,
|
||||
created: new Date(),
|
||||
notifications: [
|
||||
{ secondsBefore: 60 * 60, destination: "353868065119" } //just set a single reminder for an hour beforehand
|
||||
]
|
||||
}, { merge: true });
|
||||
res.status(201).json(reminder);
|
||||
res.status(201);
|
||||
} else {
|
||||
res.status(405);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
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 shows = await db
|
||||
.collection("shows")
|
||||
.orderBy("date")
|
||||
.get();
|
||||
res.status(200).json(shows.docs.map(show => {
|
||||
const result = {
|
||||
id: show.id,
|
||||
...show.data()
|
||||
};
|
||||
return result;
|
||||
}));
|
||||
const q = query(
|
||||
shows,
|
||||
where("date", ">", new Date())
|
||||
);
|
||||
const upcoming = await getDocs(q);
|
||||
res.status(200).json(upcoming.docs.map(r => r.data()));
|
||||
res.end();
|
||||
};
|
||||
export default handler;
|
||||
|
||||
@@ -10,4 +10,64 @@ module.exports = {
|
||||
// },
|
||||
// ],
|
||||
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