Major auth fixup

This commit is contained in:
Fergal Moran
2023-02-23 21:56:08 +00:00
parent 0f08aa5581
commit 15e0f87207
38 changed files with 1629 additions and 984 deletions

2
.env
View File

@@ -1,3 +1,5 @@
NEXT_PUBLIC_TITLE=RadioOtherway
QSTASH_CURRENT_SIGNING_KEY=khs3lpVBv1QtV/L9MTdXlcnoI8tTlg0aDfrFz+o8utA=
#auth

11
.vscode/settings.json vendored
View File

@@ -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
}

View File

@@ -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": {

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
import React from "react";
const Signup = async () => {
return <h1>Coming soon</h1>;
};
export default Signup;

View File

@@ -4,19 +4,13 @@ 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;
}) {
return (
@@ -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>

View File

@@ -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>
);
};

View File

@@ -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
View File

@@ -0,0 +1,7 @@
import { ProfilePageComponent } from "@/components/pages";
const ProfilePage = () => {
return <ProfilePageComponent />;
};
export default ProfilePage;

View File

@@ -1,80 +1,163 @@
"use client";
import React from "react";
import {useRouter} from "next/navigation";
import {BsFacebook, BsGoogle, BsTwitter} from "react-icons/bs";
import { useRouter } from "next/navigation";
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>
);
};

View File

@@ -1,78 +1,77 @@
"use client";
import React from "react";
import {BiLogInCircle} from "react-icons/bi";
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>
);
};

View 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;

View File

@@ -0,0 +1,3 @@
import ProfilePageComponent from "./ProfilePageComponent";
export { ProfilePageComponent };

View File

@@ -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,
}),
}

View 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;

View File

@@ -0,0 +1,3 @@
import TwitterButton from "./TwitterButton";
export { TwitterButton };

View File

@@ -0,0 +1,3 @@
export interface ISocialButtonProps {
onClick: () => {};
}

View 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;

View File

@@ -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";
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,10 +28,10 @@ const authUserContext
signInWithTwitter: async () => {
},
signInWithFacebook: async () => {
},
}
});
export function AuthUserProvider({children}: { children: React.ReactNode }) {
export function AuthUserProvider({ children }: { children: React.ReactNode }) {
const auth
= useFirebaseAuth();
return <authUserContext.Provider value={auth}>{children}</authUserContext.Provider>;

12
src/lib/auth/firebase.ts Normal file
View 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);

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
import {useEffect, useState} from 'react'
import { useEffect, useState } from "react";
import {
createUserWithEmailAndPassword,
FacebookAuthProvider,
@@ -9,10 +9,10 @@ import {
signInWithPopup, signInWithRedirect,
signOut,
TwitterAuthProvider
} from 'firebase/auth';
import {app} from '../db/firebaseAuth';
import {useRouter} from "next/navigation";
import {User} from "@/models";
} from "firebase/auth";
import { app } from "./firebase";
import { useRouter } from "next/navigation";
import { User } from "@/models";
const formatAuthUser = (user: User) => ({
uid: user.uid,
@@ -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,

View File

@@ -0,0 +1,4 @@
export const defaults = {
defaultTheme: "bumblebee",
defaultDarkTheme: "dark",
};

View File

@@ -0,0 +1,3 @@
import { defaults } from "./defaults";
export { defaults };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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
View File

@@ -0,0 +1,6 @@
interface RemindersProcessed {
reminderId: string;
dateProcessed: Date;
}
export type { RemindersProcessed };

View File

@@ -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,

View File

@@ -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);
}
};

View File

@@ -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);

View File

@@ -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()
});
}
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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",
},
},
],
},
};

1297
yarn.lock

File diff suppressed because it is too large Load Diff