mirror of
https://github.com/fergalmoran/radio-otherway.git
synced 2025-12-22 09:50:29 +00:00
Merge branch '@feature/images' into develop
This commit is contained in:
97
.eslintrc.js
Normal file
97
.eslintrc.js
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
15
.firebase/hosting.cHVibGlj.cache
Normal file
15
.firebase/hosting.cHVibGlj.cache
Normal 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
|
||||
20
.github/workflows/firebase-hosting-merge.yml
vendored
Normal file
20
.github/workflows/firebase-hosting-merge.yml
vendored
Normal 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
|
||||
17
.github/workflows/firebase-hosting-pull-request.yml
vendored
Normal file
17
.github/workflows/firebase-hosting-pull-request.yml
vendored
Normal 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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
}
|
||||
images: {
|
||||
domains: ["firebasestorage.googleapis.com"],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
||||
31
package.json
31
package.json
@@ -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
89
public/index.html
Normal 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…</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>
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
53
src/components/providers/FirebaseProvider.tsx
Normal file
53
src/components/providers/FirebaseProvider.tsx
Normal 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]);
|
||||
}
|
||||
14
src/components/providers/ThemeProvider.tsx
Normal file
14
src/components/providers/ThemeProvider.tsx
Normal 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;
|
||||
2
src/components/providers/index.ts
Normal file
2
src/components/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import ThemeProvider from "./ThemeProvider";
|
||||
export { ThemeProvider };
|
||||
@@ -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";
|
||||
|
||||
232
src/components/widgets/inputs/FirebaseImageUploader.tsx
Normal file
232
src/components/widgets/inputs/FirebaseImageUploader.tsx
Normal 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;
|
||||
@@ -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])}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
3
src/lib/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import useFirebaseAuth from "./useFirebaseAuth";
|
||||
|
||||
export { useFirebaseAuth };
|
||||
@@ -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(),
|
||||
|
||||
1
src/lib/core/generic/index.ts
Normal file
1
src/lib/core/generic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isServer = typeof window === "undefined" ? false : true;
|
||||
7
src/lib/db/collections.ts
Normal file
7
src/lib/db/collections.ts
Normal 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
35
src/lib/db/converters.ts
Normal 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 };
|
||||
18
src/lib/db/hooks/useGetUserProfile.tsx
Normal file
18
src/lib/db/hooks/useGetUserProfile.tsx
Normal 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" });
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
2
src/lib/util/fileUtils.ts
Normal file
2
src/lib/util/fileUtils.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
const getFileExtension = (file: File) => file.name.split(".").pop();
|
||||
export { getFileExtension };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { firebaseLogging } from "./logRocket";
|
||||
const logger = (() => {
|
||||
const checkIfLogsEnabled = () => {
|
||||
if (process.browser) {
|
||||
|
||||
12
src/lib/util/logging/logRocket.tsx
Normal file
12
src/lib/util/logging/logRocket.tsx
Normal 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;
|
||||
@@ -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 = {
|
||||
@@ -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
5
src/models/viewer.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Viewer = {
|
||||
contact_id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user