Merge branch '@feature/images' into develop

This commit is contained in:
Fergal Moran
2023-03-02 05:45:38 +00:00
46 changed files with 1253 additions and 610 deletions

97
.eslintrc.js Normal file
View File

@@ -0,0 +1,97 @@
const prettierConfig = require("./.prettierrc.js");
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
node: true,
},
extends: [
"next",
"prettier",
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"next/core-web-vitals",
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["react"],
rules: {
// Possible errors
"no-console": "warn",
// Best practices
"dot-notation": "error",
"no-else-return": "error",
"no-floating-decimal": "error",
"no-sequences": "error",
// Stylistic
"array-bracket-spacing": "error",
"computed-property-spacing": ["error", "never"],
curly: "error",
"no-lonely-if": "error",
"no-unneeded-ternary": "error",
"one-var-declaration-per-line": "error",
quotes: [
"error",
"single",
{
allowTemplateLiterals: false,
avoidEscape: true,
},
],
// ES6
"array-callback-return": "off",
"prefer-const": "error",
// Imports
"import/prefer-default-export": "off",
"sort-imports": [
"error",
{
ignoreCase: true,
ignoreDeclarationSort: true,
},
],
"no-unused-expressions": "off",
"no-prototype-builtins": "off",
// REACT
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"jsx-a11y/href-no-hash": [0],
"react/display-name": 0,
"react/no-deprecated": "error",
"react/no-unsafe": [
"error",
{
checkAliases: true,
},
],
"react/jsx-sort-props": [
"error",
{
ignoreCase: true,
},
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": 0,
// Prettier
// eslint looks for the prettier config at the top level of the package/app
// but the config lives in the `config/` directory. Passing the config here
// to get around this.
"prettier/prettier": ["error", prettierConfig],
},
settings: {
react: {
version: "detect",
},
},
};

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -0,0 +1,15 @@
android-chrome-192x192.png,1674667592000,0875127d8f3725504a67bbebdfa564444dacf191b86845454918a9b05ed8b3a1
android-chrome-256x256.png,1674667592000,01a8a8ec16e650134b655a25f7a94b9ca497ba92879c9466990387c22ebbcaa9
apple-touch-icon.png,1674667592000,881fadedab15fe1a7c1e4174cf3ed027af6b0108a3cd4f95a7c096b7e94a5766
browserconfig.xml,1674667592000,9ce44d5f41efc3b07118b339128eeb2380985c8c72b55e77d86e286137be3b4f
favicon-16x16.png,1674667592000,45b499326c559c0a4a81367cc1d5db952a8ac90a31dea35c8f8fa6a324e4f893
favicon-32x32.png,1674667592000,9c9ea32acf0310554cc723ebd9b7fa69e66c29790961698ee7af9d211af3cb0a
favicon.ico,1674667593000,824a2c204e12d0a5d873da41724fe6e8338efab3a4d5d4fb430f92070eac930e
firebase-messaging-sw.js,1677588733520,c2c4a0d1bc2d0816016729907e8cbd52b30cbd87f944383cffae4142564ba6f7
index.html,1677696301408,fc305676e6e69d5f514b023d8a014a1b91ccc76eb8baae43b75812063ce2f39d
logo.png,1676993163860,0875127d8f3725504a67bbebdfa564444dacf191b86845454918a9b05ed8b3a1
mstile-150x150.png,1674667593000,5fd3f47afb80328e82891432c714fddf5422dae49e1bf85fa4b2e57afcd8ad21
safari-pinned-tab.svg,1674667593000,1e1c8219be1c655a9b75d68093070c7ebd0f45892f04641bece262985b8ca088
site.webmanifest,1674667593000,2021d6fe8b196a9cd9e8cc746c38ace194567ec0f58463df25de88a1b2c5cf70
theme.js,1677676883452,2412441b4ed458d32b6545b812f9cdfc39f86fa8d4290b7288c558b8c9f84e2b
img/logo.jpg,1674673878003,62fa6bb035376283e64deeaa8096ff63aabd421daeeb9b0c1cce53396a2d2a80

View File

@@ -0,0 +1,20 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
'on':
push:
branches:
- develop
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_RADIO_OTHERWAY }}'
channelId: live
projectId: radio-otherway

View File

@@ -0,0 +1,17 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR
'on': pull_request
jobs:
build_and_preview:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_RADIO_OTHERWAY }}'
projectId: radio-otherway

View File

@@ -2,5 +2,19 @@
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

View File

@@ -3,6 +3,9 @@ const nextConfig = {
experimental: {
appDir: true,
},
}
images: {
domains: ["firebasestorage.googleapis.com"],
},
};
module.exports = nextConfig
module.exports = nextConfig;

View File

@@ -3,29 +3,32 @@
"version": "0.1.1",
"private": true,
"scripts": {
"turbo": "NODE_OPTIONS='-r next-logger' next dev --turbo",
"dev": "NODE_OPTIONS='-r next-logger' node ./server.js",
"turbo": "next dev --turbo",
"dev": "node ./server.js",
"debug": "node ./server.js",
"dev-nossl": "next dev",
"build": "next build",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@babel/plugin-transform-react-display-name": "^7.18.6",
"@headlessui/react": "^1.7.11",
"@next/font": "13.2.0",
"@prisma/client": "^4.9.0",
"@types/feather-icons": "^4.29.1",
"@types/node": "18.14.1",
"@types/node": "18.14.2",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@upstash/qstash": "^0.3.6",
"axios": "^1.3.4",
"babel": "^6.23.0",
"classnames": "^2.3.2",
"daisyui": "^2.49.0",
"encoding": "^0.1.13",
"eslint": "8.32.0",
"eslint-config-next": "^13.2.2",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
"eslint-plugin-react-hooks": "^4.6.0",
"feather-icons": "^4.29.0",
"firebase": "^9.17.1",
"firebase-admin": "^11.5.0",
@@ -33,15 +36,15 @@
"fireschema": "^4.0.4",
"http-status-codes": "^2.2.0",
"localforage": "^1.10.0",
"next": "^13.2.2",
"logrocket": "^3.0.1",
"next": "13.2.3",
"next-logger": "^3.0.1",
"next-seo": "^5.15.0",
"pino": "^8.11.0",
"pino-logflare": "^0.3.12",
"react": "^18.2.0",
"path-browserify": "^1.0.1",
"react": "18.2.0",
"react-daisyui": "^3.0.3",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-feather": "^2.0.10",
"react-hook-form": "^7.43.2",
@@ -50,16 +53,18 @@
"react-phone-number-input": "^3.2.19",
"theme-change": "^2.3.0",
"twilio": "^4.8.0",
"typescript": "4.9.4",
"typescript": "4.9.5",
"zod": "^3.20.6"
},
"packageManager": "yarn@1.22.19",
"devDependencies": {
"@google-cloud/local-auth": "2.1.1",
"@hookform/devtools": "^4.3.0",
"autoprefixer": "^10.4.13",
"eslint-config-prettier": "^8.6.0",
"googleapis": "111.0.0",
"postcss": "^8.4.21",
"prettier": "^2.8.3",
"prettier": "^2.8.4",
"prettier-plugin-tailwindcss": "^0.2.2",
"prisma": "^4.9.0",
"tailwindcss": "^3.2.4"

89
public/index.html Normal file
View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to Firebase Hosting</title>
<!-- update the version number as needed -->
<script defer src="/__/firebase/9.17.1/firebase-app-compat.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/9.17.1/firebase-auth-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-database-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-firestore-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-functions-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-messaging-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-storage-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-analytics-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-remote-config-compat.js"></script>
<script defer src="/__/firebase/9.17.1/firebase-performance-compat.js"></script>
<!--
initialize the SDK after all desired features are loaded, set useEmulator to false
to avoid connecting the SDK to running emulators.
-->
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>Welcome</h2>
<h1>Firebase Hosting Setup Complete</h1>
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
</div>
<p id="load">Firebase SDK Loading&hellip;</p>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loadEl = document.querySelector('#load');
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// // The Firebase SDK is initialized and available here!
//
// firebase.auth().onAuthStateChanged(user => { });
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
// firebase.firestore().doc('/foo/bar').get().then(() => { });
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
// firebase.messaging().requestPermission().then(() => { });
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
// firebase.analytics(); // call to activate
// firebase.analytics().logEvent('tutorial_completed');
// firebase.performance(); // call to activate
//
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
try {
let app = firebase.app();
let features = [
'auth',
'database',
'firestore',
'functions',
'messaging',
'storage',
'analytics',
'remoteConfig',
'performance',
].filter(feature => typeof app[feature] === 'function');
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
} catch (e) {
console.error(e);
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
}
});
</script>
</body>
</html>

View File

@@ -7,8 +7,12 @@ import { NavBar, PushNotificationWrapper } from "@/components/layout";
import { AuthUserProvider } from "@/lib/auth/authUserContext";
import { themeChange } from "theme-change";
import Script from "next/script";
import { Toaster } from "react-hot-toast";
import useLogRocket from "@/lib/util/logging/logRocket";
import logger from "@/lib/util/logging";
import FirestoreProvider from "@/components/providers/FirebaseProvider";
// only initialize when in the browser
const font = Raleway({
weight: ["400", "700"],
subsets: ["latin"],
@@ -19,24 +23,27 @@ const RootLayout = ({ children }: React.PropsWithChildren) => {
React.useEffect(() => {
logger.info("Bootstrapping application");
themeChange(false);
}, []);
}, [logger]);
return (
<html lang="en">
<head>
<Script src="/theme.js" strategy="beforeInteractive" />
<Script src="/theme.js" />
</head>
<body className={`${font.className}`}>
<AuthUserProvider>
<PushNotificationWrapper>
<div className="flex flex-col min-h-screen bg-base-100">
<NavBar />
<div className="items-end grow place-items-center bg-base-200 text-primary-content">
<main className=" text-base-content">{children}</main>
<Toaster />
<FirestoreProvider>
<AuthUserProvider>
<PushNotificationWrapper>
<div className="flex flex-col min-h-screen bg-base-100">
<NavBar />
<div className="items-end grow place-items-center bg-base-200 text-base-content">
<main className=" text-base-content">{children}</main>
</div>
</div>
</div>
</PushNotificationWrapper>
</AuthUserProvider>
</PushNotificationWrapper>
</AuthUserProvider>
</FirestoreProvider>
</body>
</html>
);

View File

@@ -1,7 +1,31 @@
import React from "react";
import { useState } from "react";
const LogRocket = require("logrocket");
import packageJson from "../../package.json";
const Loading = () => {
if (process.env.LOGROCKET_ID && window !== undefined) {
LogRocket.init(process.env.LOGROCKET_ID, {
release: packageJson.version,
rootHostname: "radio-otherway.fergl.ie",
console: {
shouldAggregateConsoleErrors: true,
},
network: {
requestSanitizer: (request: any) => {
// if the url contains token 'ignore' it
if (request.url.toLowerCase().indexOf("token") !== -1) {
// ignore the request response pair
return null;
}
// remove Authorization header from logrocket
request.headers.Authorization = undefined;
// otherwise log the request normally
return request;
},
},
});
}
return (
<div role="status">

View File

@@ -1,6 +1,10 @@
import React from "react";
import HomePageComponent from "@/components/pages/home";
export const metadata = {
title: "Radio Otherway",
};
const getData = async () => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/shows/upcoming`,

View File

@@ -1,4 +1,3 @@
import { firebaseCloudMessaging } from "@/lib/auth/firebaseMessaging";
import * as firebase from "firebase/app";
import { useRouter } from "next/navigation";
import React, { useEffect } from "react";
@@ -11,87 +10,94 @@ import { useAuthUserContext } from "@/lib/auth/authUserContext";
import { users } from "@/lib/db";
import logger from "@/lib/util/logging";
import { parseUserAgent } from "react-device-detect";
import { firebaseCloudMessaging } from "@/lib/util/notifications/firebaseMessaging";
const PushNotificationWrapper = ({ children }: React.PropsWithChildren) => {
const router = useRouter();
const { profile } = useAuthUserContext();
useEffect(() => {
const _getAndStoreRegistrationToken = async () => {
const { ua } = parseUserAgent(window.navigator.userAgent);
const router = useRouter();
const { profile } = useAuthUserContext();
useEffect(() => {
const _getAndStoreRegistrationToken = async () => {
const { ua } = parseUserAgent(window.navigator.userAgent);
if (!profile) return;
await setToken();
if (!profile) return;
await setToken();
// Event listener that listens for the push notification event in the background
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("event for the service worker", event);
});
}
// Calls the getMessage() function if the token is there
async function setToken() {
try {
if (!profile) return;
const token = await firebaseCloudMessaging.init();
if (token && profile) {
const newRegistration = {
fcmToken: token,
deviceType: ua,
lastSeen: new Date()
};
const index = profile.deviceRegistrations?.findIndex(reg => {
return reg.fcmToken === token;
});
if (index !== undefined && index !== -1) {
if (profile.deviceRegistrations && profile.deviceRegistrations[index]) {
profile.deviceRegistrations[index] = newRegistration;
}
} else {
profile.deviceRegistrations?.push(newRegistration);
}
}
const profileWithRegistrations = Object.assign({}, profile);
await setDoc(doc(users, profile?.id), profileWithRegistrations, { merge: true });
getMessage();
} catch (error) {
console.log(error);
}
}
};
_getAndStoreRegistrationToken()
.catch(err => {
logger.error("PushNotificationWrapper", "_getAndStoreRegistrationToken_error", err);
// Event listener that listens for the push notification event in the background
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("event for the service worker", event);
});
}, [profile]);
}
function getMessage() {
const messaging = getMessaging(app);
onMessage(messaging, (message) => {
ToastService.custom(
<div
onClick={() =>
message?.data?.url &&
handleClickPushNotification(message?.data?.url)
// Calls the getMessage() function if the token is there
async function setToken() {
try {
if (!profile) return;
const token = await firebaseCloudMessaging.init();
if (token && profile) {
const newRegistration = {
fcmToken: token,
deviceType: ua,
lastSeen: new Date(),
};
const index = profile.deviceRegistrations?.findIndex((reg) => {
return reg.fcmToken === token;
});
if (index !== undefined && index !== -1) {
if (
profile.deviceRegistrations &&
profile.deviceRegistrations[index]
) {
profile.deviceRegistrations[index] = newRegistration;
}
} else {
profile.deviceRegistrations?.push(newRegistration);
}
>
<h5>{message?.notification?.title}</h5>
<h6>{message?.notification?.body}</h6>
</div>
);
});
}
const handleClickPushNotification = (url: string) => {
router.push(url);
}
const profileWithRegistrations = Object.assign({}, profile);
await setDoc(doc(users, profile?.id), profileWithRegistrations, {
merge: true,
});
getMessage();
} catch (error) {
console.log(error);
}
}
};
return (
<>
<Toaster />
{children}
</>
);
}
;
_getAndStoreRegistrationToken().catch((err) => {
logger.error(
"PushNotificationWrapper",
"_getAndStoreRegistrationToken_error",
err
);
});
}, [profile]);
function getMessage() {
const messaging = getMessaging(app);
onMessage(messaging, (message) => {
ToastService.custom(
<div
onClick={() =>
message?.data?.url &&
handleClickPushNotification(message?.data?.url)
}
>
<h5>{message?.notification?.title}</h5>
<h6>{message?.notification?.body}</h6>
</div>
);
});
}
const handleClickPushNotification = (url: string) => {
router.push(url);
};
return (
<>
<Toaster />
{children}
</>
);
};
export default PushNotificationWrapper;

View File

@@ -1,19 +1,23 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
import { useFirebaseAuth } from "@/lib/auth";
const LoginPage = () => {
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, profile, signIn } =
useFirebaseAuth();
const {
signInWithGoogle,
signInWithFacebook,
signInWithTwitter,
profile,
signIn,
} = useFirebaseAuth();
const router = useRouter();
const [error, setError] = React.useState("");
const [forgot, setForgot] = React.useState(false);
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const login = async (
event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
@@ -21,9 +25,7 @@ const LoginPage = () => {
};
return (
<div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1">
<h3 className="my-4 text-2xl font-semibold font-title">
Account Login
</h3>
<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">
@@ -122,10 +124,18 @@ const LoginPage = () => {
{error && (
<div className="shadow-lg alert alert-error">
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="flex-shrink-0 w-6 h-6 stroke-current" fill="none"
viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
className="flex-shrink-0 w-6 h-6 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{error}</span>
</div>

View File

@@ -1,11 +1,11 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
import { AiOutlineExclamationCircle } from "react-icons/ai";
import { Info } from "react-feather";
import { useFirebaseAuth } from "@/lib/auth";
const SignupPage = () => {
const {

View File

@@ -57,14 +57,6 @@ const ProfilePageComponent = () => {
if (profile) {
reset(profile);
}
// setValue([
// { displayName: profile?.displayName },
// { email: profile?.email },
// { about: profile?.about },
// { photoURL: profile?.photoURL },
// { headerPhotoURL: profile?.headerPhotoURL },
// { mobileNumber: profile?.mobileNumber },
// ]);
}, [profile, reset]);
const onSubmit: SubmitHandler<ProfileForm> = async (data) => {
console.log(data);
@@ -93,7 +85,7 @@ const ProfilePageComponent = () => {
}
};
React.useEffect(() => {}, [selectedItem]);
useEffect(() => {}, [selectedItem]);
const _getView = () => {
if (loading) {
return <div>Loading</div>;
@@ -140,6 +132,7 @@ const ProfilePageComponent = () => {
<form onSubmit={handleSubmit(onSubmit)}>
{selectedItem === "profile" ? (
<ProfilePageComponentProfile
setValue={setValue}
register={register}
profile={profile}
/>

View File

@@ -1,20 +1,23 @@
"use client";
import { HeadingSubComponent } from "@/components/widgets/text";
import React from "react";
import { UseFormRegister } from "react-hook-form";
import React, { useState } from "react";
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
import { ProfileForm } from "@/components/pages/profile/ProfilePageComponent";
import { Profile } from "@/models";
import InputText from "@/components/widgets/inputs/InputText";
import { FirebaseImageUpload, InputText } from "@/components/widgets/inputs";
import { InputTextArea } from "@/components/widgets/inputs";
interface IProfilePageComponentProfileProps {
register: UseFormRegister<ProfileForm>;
profile: Profile;
register: UseFormRegister<ProfileForm>;
setValue: UseFormSetValue<ProfileForm>;
}
const ProfilePageComponentProfile = ({
register,
profile,
register,
setValue,
}: IProfilePageComponentProfileProps) => {
const [photoURLFile, setPhotoURLFile] = useState("");
return (
<div className="space-y-8 divide-y sm:space-y-5">
<div>
@@ -83,23 +86,15 @@ const ProfilePageComponentProfile = ({
subHeading="Upload a picture to distinguish you from the rest"
/>
<div className="mt-1 sm:col-span-2 sm:mt-0">
<div className="flex items-center">
<span className="w-12 h-12 overflow-hidden rounded-full">
<svg
className="w-full h-full "
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
<button
type="button"
className="px-3 py-2 ml-5 text-sm font-medium leading-4 border rounded-md shadow-sm hover: focus:outline-none focus:ring-2 focus:ring-offset-2"
>
Change
</button>
</div>
<FirebaseImageUpload
forType="user"
imageType="avatar"
itemId={profile.id}
imageUrl={profile.photoURL}
controlName="photoURL"
setValue={setValue}
{...register("photoURL")}
/>
</div>
</div>
@@ -109,40 +104,15 @@ const ProfilePageComponentProfile = ({
subHeading="Upload a wide photo for the top of your profile page"
/>
<div className="mt-1 sm:col-span-2 sm:mt-0">
<div className="flex justify-center max-w-lg px-6 pt-5 pb-6 border-2 border-dashed rounded-md">
<div className="space-y-1 text-center">
<svg
className="w-12 h-12 mx-auto "
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div className="flex text-sm ">
<label
htmlFor="file-upload"
className="relative font-medium rounded-md cursor-pointer te focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2"
>
<span>Upload a file</span>
<input
id="file-upload"
name="file-upload"
type="file"
className="sr-only"
/>
</label>
<p className="pl-1">or drag and drop</p>
</div>
<p className="text-xs ">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
<FirebaseImageUpload
forType="user"
imageType="profile"
itemId={profile.id}
imageUrl={profile.headerPhotoURL}
controlName="headerPhotoURL"
setValue={setValue}
{...register("headerPhotoURL")}
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
import { FirestoreProvider, useFirebaseApp } from "reactfire";
import { useMemo } from "react";
import {
enableIndexedDbPersistence,
connectFirestoreEmulator,
initializeFirestore,
} from "firebase/firestore";
import { isBrowser } from "~/core/generic";
export default function FirestoreProvider({
children,
useEmulator,
}: React.PropsWithChildren<{ useEmulator?: boolean }>) {
const firestore = useFirestore();
// connect to emulator if enabled
if (useEmulator) {
const host = getFirestoreHost();
const port = Number(getFirestorePort());
try {
connectFirestoreEmulator(firestore, host, port);
} catch (e) {
// this may happen on re-renderings
}
}
const enablePersistence = isBrowser();
// We enable offline capabilities by caching Firestore in IndexedDB
// NB: if you don't want to cache results, please remove the next few lines
if (enablePersistence) {
enableIndexedDbPersistence(firestore);
}
return <FirestoreProvider sdk={firestore}>{children}</FirestoreProvider>;
}
function getFirestoreHost() {
return process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_HOST ?? "localhost";
}
function getFirestorePort() {
return process.env.NEXT_PUBLIC_FIRESTORE_EMULATOR_PORT ?? 8080;
}
function useFirestore() {
const app = useFirebaseApp();
return useMemo(() => initializeFirestore(app, {}), [app]);
}

View File

@@ -0,0 +1,14 @@
import Loading from "@/app/loading";
import { defaults } from "@/lib/constants";
import React, { useEffect } from "react";
const ThemeProvider = ({ children }: React.PropsWithChildren) => {
const [theme, setTheme] = React.useState("");
useEffect(() => {
setTheme(localStorage.getItem("theme") || defaults.defaultTheme);
}, [theme]);
return theme ? <>{children}</> : <Loading />;
};
ThemeProvider.displayName = "ThemeProvider";
export default ThemeProvider;

View File

@@ -0,0 +1,2 @@
import ThemeProvider from "./ThemeProvider";
export { ThemeProvider };

View File

@@ -1,5 +1,5 @@
"use client";
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { useFirebaseAuth } from "@/lib/auth";
import { Show } from "@/models";
import React from "react";
import { MdAddAlarm } from "react-icons/md";

View File

@@ -0,0 +1,232 @@
import {
ref as storageRef,
uploadBytesResumable,
getDownloadURL,
} from "firebase/storage";
import { storage } from "@/lib/db";
import Image from "next/image";
import {
useEffect,
useRef,
useState,
forwardRef,
ChangeEventHandler,
} from "react";
var path = require("path");
import { UploadCloud } from "react-feather";
import { getFileExtension } from "@/lib/util/fileUtils";
import ITextInputProps from "@/components/widgets/inputs/props";
import {
Controller,
FieldPath,
UseFormReturn,
UseFormSetValue,
} from "react-hook-form";
import { ProfileForm } from "@/components/pages/profile/ProfilePageComponent";
interface IFirebaseImageUploaderProps {
forType: "user" | "show";
imageType: "avatar" | "profile";
itemId: string;
imageUrl?: string;
controlName: FieldPath<ProfileForm>;
setValue: UseFormSetValue<ProfileForm>;
onChange: ChangeEventHandler<HTMLInputElement>;
onBlur: ChangeEventHandler<HTMLInputElement>;
}
const FirebaseImageUploader = forwardRef<
HTMLInputElement,
IFirebaseImageUploaderProps
>(
(
{
controlName,
forType,
imageType,
itemId,
imageUrl,
setValue,
onChange,
onBlur,
},
ref
) => {
const [file, setFile] = useState<File | null>();
const [filePath, setFilePath] = useState();
const [isUploading, setIsUploading] = useState(false);
const [percent, setPercent] = useState(0);
const fileInput = useRef<HTMLInputElement>(null);
const [error, setError] = useState("");
const _handleChange = (event: React.FormEvent<HTMLInputElement>) => {
const e = event.target as HTMLInputElement;
if (!e.files || e.files.length === 0) {
return;
}
setFile(e.files[0]);
};
useEffect(() => {
const _cleanUp = () => {
setFile(null);
setIsUploading(false);
setPercent(0);
};
const _handleUpload = () => {
if (!file) {
setError("You must choose a file to upload first");
return;
}
console.log("FirebaseImageUploader", "Creating storage refs", storage);
const extension = getFileExtension(file);
const newFilePath = path.join(
"files",
"images",
forType,
imageType,
`${itemId}.${extension}`
);
setFilePath(newFilePath);
console.log("FirebaseImageUploader", "FilePath", newFilePath);
const remoteFileReference = storageRef(storage, newFilePath);
console.log(
"FirebaseImageUploader",
"Created storage refs",
remoteFileReference
);
console.log("FirebaseImageUploader", "Starting upload task");
setIsUploading(true);
const uploadTask = uploadBytesResumable(remoteFileReference, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const percent = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
console.log(
"FirebaseImageUploader",
"uploading",
`${percent}% done`
);
setPercent(percent);
},
(err) => {
_cleanUp();
console.log(err);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
if (controlName) {
setValue(controlName, url);
}
});
_cleanUp();
}
);
};
if (file && !isUploading) {
_handleUpload();
}
}, [controlName, file, forType, imageType, isUploading, itemId, setValue]);
return imageType === "avatar" ? (
<div className="flex flex-col">
<div className="flex items-center space-x-1">
<span className="w-12 h-12 overflow-hidden rounded-full">
{imageUrl ? (
<Image
alt="Existing image"
src={imageUrl}
className="w-full h-full "
width={48}
height={48}
/>
) : (
<svg
className="w-full h-full "
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</span>
<button
type="button"
className={`btn-outline btn-primary btn gap-2 ${
isUploading ? "loading" : ""
}`}
onClick={() => fileInput?.current?.click()}
>
<UploadCloud
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
/>
{!imageUrl ? "Upload image" : "Change image"}
</button>
<input
name={controlName}
placeholder="File input hidden"
className="invisible"
type="file"
accept="image/*"
ref={fileInput}
onBlur={onBlur}
onChange={($event) => {
_handleChange($event);
onChange($event);
}}
/>
</div>
{percent > 0 && (
<span className="block m-1 overflow-hidden text-xs italic text-gray-600 rounded-full">
{`Uploading, ${percent}% done`}
</span>
)}
</div>
) : (
<div className="flex justify-center max-w-lg px-6 pt-5 pb-6 border-2 border-dashed rounded-md">
<div className="space-y-1 text-center">
<svg
className="w-12 h-12 mx-auto "
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div className="flex text-sm ">
<label
htmlFor="file-upload"
className="relative font-medium rounded-md cursor-pointer te focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2"
>
<span>Upload a file</span>
<input
id="file-upload"
name="file-upload"
type="file"
className="sr-only"
/>
</label>
<p className="pl-1">or drag and drop</p>
</div>
<p className="text-xs ">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
);
}
);
FirebaseImageUploader.displayName = "FirebaseImageUploader";
export default FirebaseImageUploader;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @next/next/no-img-element */
import React from "react";
import Dropzone, { DropzoneRef, useDropzone } from "react-dropzone";
@@ -25,6 +26,7 @@ const ImageUpload = () => {
<div id="preview">
{acceptedFiles[0] && (
<img
alt="Uploaded image"
className="object-cover"
src={URL.createObjectURL(acceptedFiles[0])}
/>

View File

@@ -2,8 +2,10 @@ import React, { forwardRef } from "react";
import ITextInputProps from "./props";
const InputText = forwardRef<HTMLInputElement, ITextInputProps>(
({ id, type, placeholder, label, showLabel = true, onChange, onBlur }, ref) => {
(
{ id, type, placeholder, label, showLabel = true, onChange, onBlur },
ref
) => {
return (
<React.Fragment>
{showLabel && (
@@ -26,5 +28,3 @@ const InputText = forwardRef<HTMLInputElement, ITextInputProps>(
);
InputText.displayName = "InputTextAreaComponent";
export default InputText;

View File

@@ -1,6 +1,6 @@
import ImageUpload from "./ImageUpload";
import InputText from "./InputText";
import InputTextArea from "./InputTextArea";
import FirebaseImageUpload from "./FirebaseImageUploader";
export { ImageUpload };
export { InputTextArea };
export { InputText, ImageUpload, InputTextArea, FirebaseImageUpload };

View File

@@ -1,6 +1,6 @@
import React from "react";
import { firebaseCloudMessaging } from "@/lib/auth/firebaseMessaging";
import localforage from "localforage";
import { firebaseCloudMessaging } from "@/lib/util/notifications/firebaseMessaging";
const RequestPushNotifications = () => {
const _checkNotifications = async () => {

View File

@@ -1,5 +1,4 @@
import React from "react";
import InputText from "../inputs/InputText";
import { UseFormRegister } from "react-hook-form";
import { ProfileForm } from "@/components/pages/profile/ProfilePageComponent";
import { Profile } from "@/models";

View File

@@ -1,24 +1,25 @@
import React, { createContext, useContext, Context } from "react";
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { Profile } from "@/models";
import useFirebaseAuth from "./useFirebaseAuth";
interface IAuthUserContext {
loading: boolean;
profile: Profile | undefined,
logOut: () => void
profile: Profile | undefined;
logOut: () => void;
}
const authUserContext = createContext<IAuthUserContext>({
loading: true,
profile: undefined,
logOut: () => {
}
logOut: () => {},
});
export function AuthUserProvider({ children }: { children: React.ReactNode }) {
const { loading, profile, logOut } = useFirebaseAuth();
return (
<authUserContext.Provider value={{ loading, profile, logOut }}>{children}</authUserContext.Provider>
<authUserContext.Provider value={{ loading, profile, logOut }}>
{children}
</authUserContext.Provider>
);
}

View File

@@ -7,6 +7,6 @@ const firebaseConfig = {
storageBucket: "radio-otherway.appspot.com",
messagingSenderId: "47147490249",
appId: "1:47147490249:web:a84515b3ce1c481826e618",
measurementId: "G-12YB78EZM4"
measurementId: "G-12YB78EZM4",
};
export const app = initializeApp(firebaseConfig);

3
src/lib/auth/index.ts Normal file
View File

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

View File

@@ -44,8 +44,8 @@ export default function useFirebaseAuth() {
const profile: Profile = new Profile(
auth.currentUser.uid,
(savedProfile?.email || auth.currentUser.email) as string,
(savedProfile?.displayName || auth.currentUser.email) as string,
(savedProfile?.photoURL || auth.currentUser.email) as string,
(savedProfile?.displayName || auth.currentUser.displayName) as string,
(savedProfile?.photoURL || auth.currentUser.photoURL) as string,
savedProfile?.about as string,
savedProfile?.mobileNumber as string,
new Date(),

View File

@@ -0,0 +1 @@
export const isServer = typeof window === "undefined" ? false : true;

View File

@@ -0,0 +1,7 @@
import {
CollectionReference,
collection,
DocumentData,
} from "firebase/firestore";
import { firestore } from "../auth/firebase";

35
src/lib/db/converters.ts Normal file
View File

@@ -0,0 +1,35 @@
import { NotificationSchedule, Show } from "@/models";
import { DocumentData, QueryDocumentSnapshot, SnapshotOptions, Timestamp, WithFieldValue } from "firebase/firestore";
const showConverter = {
toFirestore(show: WithFieldValue<Show>): DocumentData {
return {
...show,
date: show.date
? Timestamp.fromDate(new Date(show.date as string))
: new Date(),
};
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): Show {
const data = snapshot.data(options)!;
return new Show(snapshot.id, data.title, data.date.toDate(), data.creator);
},
};
const noticeConverter = {
toFirestore(notice: WithFieldValue<NotificationSchedule>): DocumentData {
return notice;
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): NotificationSchedule {
const data = snapshot.data(options)!;
return new NotificationSchedule(
data.scheduleTimes.map((r: any) => r.toDate())
);
},
};
export { showConverter, noticeConverter };

View File

@@ -0,0 +1,18 @@
import { useFirestore, useFirestoreDocData } from "reactfire";
import { doc, DocumentReference } from "firebase/firestore";
import { Organization } from "~/lib/organizations/types/organization";
type Response = Organization & { id: string };
export function useFetchOrganization(organizationId: string) {
const firestore = useFirestore();
const organizationsPath = `/organizations`;
const ref = doc(
firestore,
organizationsPath,
organizationId
) as DocumentReference<Response>;
return useFirestoreDocData(ref, { idField: "id" });
}

View File

@@ -8,6 +8,7 @@ import {
QueryDocumentSnapshot,
SnapshotOptions,
Timestamp,
initializeFirestore,
} from "firebase/firestore";
const firebaseConfig = {
@@ -20,42 +21,10 @@ const firebaseConfig = {
measurementId: "G-12YB78EZM4",
};
export const firebaseApp = initializeApp(firebaseConfig);
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: show.date
? Timestamp.fromDate(new Date(show.date as string))
: new Date(),
};
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): Show {
const data = snapshot.data(options)!;
return new Show(snapshot.id, data.title, data.date.toDate(), data.creator);
},
};
const noticeConverter = {
toFirestore(notice: WithFieldValue<NotificationSchedule>): DocumentData {
return notice;
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): NotificationSchedule {
const data = snapshot.data(options)!;
return new NotificationSchedule(
data.scheduleTimes.map((r: any) => r.toDate())
);
},
};
// Import all your model types
import {
Show,
@@ -64,11 +33,15 @@ import {
Profile,
NotificationSchedule,
} from "@/models";
import { storage } from "../firebase";
import { noticeConverter, showConverter } from "./converters";
import { firestore } from "../auth/firebase";
// export all your collections
export const users = createCollection<Profile>("users");
export const shows =
createCollection<Show>("shows").withConverter(showConverter);
export const notificationSchedules =
createCollection<NotificationSchedule>("noticeSchedules").withConverter(
noticeConverter
@@ -77,6 +50,5 @@ export const notificationSchedules =
export const reminders = createCollection<Reminder>("reminders");
export const remindersProcessed =
createCollection<RemindersProcessed>("reminders");
export default firestore;
export { createCollection };
export { createCollection, firebaseConfig, storage };

View File

@@ -1,7 +1,7 @@
import firebase, { getApp, getApps, initializeApp } from "firebase/app";
import { getStorage } from "firebase/storage";
import "firebase/auth";
// import { getFirestore } from "firebase/firestore";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyDtk_Ym-AZroXsHvQVcdHXYyc_TvgycAWw",
@@ -10,8 +10,10 @@ 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);
export const storage = getStorage(app);
export const db = getFirestore();

View File

@@ -0,0 +1,2 @@
const getFileExtension = (file: File) => file.name.split(".").pop();
export { getFileExtension };

View File

@@ -1,3 +1,4 @@
import { firebaseLogging } from "./logRocket";
const logger = (() => {
const checkIfLogsEnabled = () => {
if (process.browser) {

View File

@@ -0,0 +1,12 @@
import LogRocket from "logrocket";
const useLogRocket = () => {
const logrocketId = process.env.NEXT_PUBLIC_LOGROCKET_ID || "";
setTimeout(() => {
LogRocket.init(logrocketId);
}, 100);
return null;
};
export default useLogRocket;

View File

@@ -1,7 +1,7 @@
import "firebase/messaging";
import localforage from "localforage";
import { getMessaging, getToken } from "firebase/messaging";
import { app } from "./firebase";
import { app } from "../../auth/firebase";
const firebaseCloudMessaging = {

View File

@@ -2,6 +2,7 @@ import Profile from "./profile";
import Show from "./show";
import Reminder from "./reminder";
import Notification from "./notification";
import Viewer from "./viewer";
import DeviceRegistration from "./deviceregistration";
import type { RemindersProcessed } from "./processes";
import NotificationSchedule from "./notificationSchedule";
@@ -11,7 +12,8 @@ export {
Show,
Reminder,
Notification,
type Viewer,
DeviceRegistration,
NotificationSchedule,
RemindersProcessed
RemindersProcessed,
};

5
src/models/viewer.ts Normal file
View File

@@ -0,0 +1,5 @@
export type Viewer = {
contact_id: string;
email: string;
name: string;
};

View File

@@ -26,7 +26,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = u.data();
if (user.mobileNumber) {
console.log("notify", "sending notification to ", user);
const message = `New show from ${show.creator} starting now!!\nhttps://mixcloud.com/live/radiootherway`;
const message = (process.env.WHATSAPP_SHOW_HOUR as string)
.replace("{{1}}", user.displayName as string)
.replace("{{2}}", show.creator);
await sendWhatsApp(user.mobileNumber, message);
}
});

View File

@@ -1,14 +1,12 @@
import { NextApiRequest, NextApiResponse } from "next";
import db, { shows } from "@/lib/db";
import { getDocs, query, where } from "@firebase/firestore";
import { StatusCodes } from "http-status-codes";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const q = query(
shows,
where("date", ">", new Date())
);
const q = query(shows, where("date", ">", new Date()));
const upcoming = await getDocs(q);
res.status(StatusCodes.OK).json(upcoming.docs.map(r => r.data()));
res.status(StatusCodes.OK).json(upcoming.docs.map((r) => r.data()));
res.end();
};
export default handler;

738
yarn.lock

File diff suppressed because it is too large Load Diff