├── .env.template ├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── ToS.md ├── firestore.rules ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── favicon.ico ├── icon.webp └── manifest.json ├── src ├── components │ ├── Layout.tsx │ ├── ProfileSetup.tsx │ └── TermsOfServicePopup.tsx ├── config │ ├── firebase-admin.ts │ └── firebase.ts ├── contexts │ └── AuthContext.tsx ├── lib │ ├── firebaseAnalytics.ts │ └── gtag.ts ├── middleware │ └── authMiddleware.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── hello.ts │ │ ├── matchmaking │ │ │ ├── cancel.ts │ │ │ ├── end-session.ts │ │ │ ├── join.ts │ │ │ ├── match.ts │ │ │ └── status.ts │ │ ├── profile │ │ │ └── update.ts │ │ └── sessions │ │ │ └── update-peer.ts │ ├── call-test.tsx │ ├── call │ │ └── [id].tsx │ ├── chat │ │ └── [id].tsx │ ├── email.tsx │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── index.tsx │ ├── profile.tsx │ └── tos.tsx ├── styles │ └── globals.css └── utils │ ├── api.ts │ └── media.ts ├── tailwind.config.ts └── tsconfig.json /.env.template: -------------------------------------------------------------------------------- 1 | # Firebase Config 2 | NEXT_PUBLIC_FIREBASE_API_KEY= 3 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 4 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 5 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= 6 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= 7 | NEXT_PUBLIC_FIREBASE_APP_ID= 8 | NEXT_PUBLIC_MEASUREMENT_ID= 9 | 10 | # Firebase Admin Config 11 | FIREBASE_ADMIN_PROJECT_ID= 12 | FIREBASE_ADMIN_CLIENT_EMAIL= 13 | FIREBASE_ADMIN_PRIVATE_KEY= 14 | FIREBASE_DATABASE_URL= 15 | 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ankushKun # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Call Me Maybe 🤙 2 | 3 | live video dating app 4 | 5 | - Lock in with a match for 15 minutes 6 | - No brainless swiping 7 | 8 | Upvote us on Product Hunt 9 | 10 |
11 | 12 | Call Me Maybe - Swipe less, vibe more. Call Me Maybe? | Product Hunt 13 | 14 | 15 | Static Badge 16 | 17 |
18 | 19 | 20 | 21 | 22 | ## Setup 23 | 24 | Make sure you have Node.js and npm installed on your machine before starting the setup. 25 | 26 | 1. Fork and Clone the repository: 27 | ```bash 28 | git clone https://github.com//random-retarded-3am-project call-me-maybe 29 | cd call-me-maybe 30 | ``` 31 | 32 | 2. Install the required dependencies: 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | 3. Copy env file 38 | ```bash 39 | mv .env.template .env.local 40 | ``` 41 | 42 | 4. Setup a new project with Google [Firebase](https://firebase.google.com/) and enable firestore, authentication and analytics. 43 | Go into settings and add a web application, you will get the necessasary keys. Add them to the `.env.local` 44 | 45 | 5. Run the project 46 | ``` 47 | npm run dev 48 | ``` 49 | the app will be available at http://localhost:3000 50 | 51 | --- 52 | 53 | A product of [weeblabs](https://weeblabs.com) -------------------------------------------------------------------------------- /ToS.md: -------------------------------------------------------------------------------- 1 | # Terms of Service for callmemaybe.xyz 2 | 3 | ## 1. Age Restrictions 4 | By using this service, you confirm that you are at least 18 years of age (or the legal age of majority in your jurisdiction, whichever is higher). We reserve the right to terminate accounts of users found to be underage. 5 | 6 | ## 2. Prohibited Content and Behavior 7 | Users are strictly prohibited from: 8 | - Displaying nudity, sexual content, or engaging in sexual behavior 9 | - Sharing sexually explicit or suggestive content 10 | - Harassing, threatening, or intimidating other users 11 | - Sharing illegal content of any kind 12 | - Soliciting or offering sexual services 13 | - Impersonating others 14 | - Violating the privacy of others 15 | 16 | ## 3. Monitoring and Enforcement 17 | - We reserve the right to monitor communications for compliance with these terms 18 | - Automated systems may be used to detect prohibited content 19 | - We may report illegal activity to law enforcement 20 | - Violations may result in immediate account termination without notice 21 | 22 | ## 4. User Responsibilities 23 | Users are solely responsible for: 24 | - All content they share or display 25 | - Ensuring their conduct complies with all applicable laws 26 | - Any interactions they have with other users 27 | - Safeguarding their account credentials 28 | 29 | ## 5. Disclaimers and Limitations of Liability 30 | - The service is provided "as is" without warranties of any kind 31 | - We are not responsible for user-generated content 32 | - We do not guarantee screening of users or content 33 | - We are not liable for any damages arising from use of the service 34 | - We are not responsible for user interactions that occur outside our platform 35 | 36 | ## 6. Indemnification 37 | Users agree to indemnify and hold harmless CallMeMaybe, its officers, directors, employees, and agents from any claims, damages, or liabilities arising from the user's violation of these terms or misuse of the service. 38 | 39 | ## 7. Privacy Policy 40 | Use of this service is also governed by our Privacy Policy, which is incorporated by reference. 41 | 42 | ## 8. Amendments 43 | We reserve the right to modify these terms at any time. Continued use of the service after changes constitutes acceptance of the modified terms. 44 | 45 | ## 9. Governing Law 46 | These terms shall be governed by the laws of India, without regard to conflict of law principles. 47 | 48 | ## 10. Consent to Electronic Communications 49 | By using this service, you consent to receive communications from us electronically. 50 | 51 | ## 11. Termination 52 | We reserve the right to terminate or suspend access to our service immediately, without prior notice or liability, for any reason. 53 | 54 | ## 12. User Acknowledgment 55 | BY USING THIS SERVICE, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE, UNDERSTAND THEM, AND AGREE TO BE BOUND BY THEM. -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | // Chat messages rules 5 | match /chats/{messageId} { 6 | allow read: if request.auth != null && 7 | exists(/databases/$(database)/documents/sessions/$(resource.data.sessionId)) && 8 | get(/databases/$(database)/documents/sessions/$(resource.data.sessionId)).data.participants.hasAny([request.auth.uid]); 9 | 10 | allow create: if request.auth != null && 11 | exists(/databases/$(database)/documents/sessions/$(request.resource.data.sessionId)) && 12 | get(/databases/$(database)/documents/sessions/$(request.resource.data.sessionId)).data.participants.hasAny([request.auth.uid]) && 13 | request.resource.data.senderId == request.auth.uid; 14 | } 15 | 16 | // Session rules 17 | match /sessions/{sessionId} { 18 | allow read: if request.auth != null && 19 | (resource.data.status in ['video', 'chat'] || resource.data.participants.hasAny([request.auth.uid])); 20 | 21 | allow update: if request.auth != null && 22 | resource.data.participants.hasAny([request.auth.uid]) && 23 | ( 24 | // Allow message updates 25 | (!request.resource.data.diff(resource.data).affectedKeys() 26 | .hasAny(['participants', 'startTime', 'endTime'])) || 27 | // Allow status and cooldown updates 28 | (request.resource.data.diff(resource.data).affectedKeys() 29 | .hasAll(['status', 'cooldownEnds']) && 30 | request.resource.data.status == 'cooldown') 31 | ); 32 | } 33 | 34 | // User rules 35 | match /users/{userId} { 36 | allow read: if request.auth != null && 37 | (request.auth.uid == userId || 38 | exists(request.auth.uid)); 39 | 40 | // Add write rule for initial profile setup 41 | allow write: if request.auth != null && 42 | request.auth.uid == userId && 43 | request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'gender']); 44 | } 45 | 46 | // Queue rules 47 | match /matchmaking_queue/{userId} { 48 | allow read: if request.auth != null; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'lh3.googleusercontent.com', 9 | }, 10 | { 11 | protocol: 'https', 12 | hostname: 'www.google.com', 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinklips", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@firebase/auth": "^1.9.0", 13 | "@firebase/firestore": "^4.7.8", 14 | "@vercel/analytics": "^1.5.0", 15 | "firebase": "^11.3.1", 16 | "firebase-admin": "^13.1.0", 17 | "framer-motion": "^12.4.2", 18 | "next": "14.2.20", 19 | "peerjs": "^1.5.4", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "react-icons": "^5.4.0", 23 | "react-markdown": "^9.0.3", 24 | "remark-gfm": "^4.0.1", 25 | "tailwindcss-animate": "^1.0.7" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20", 29 | "@types/react": "^18", 30 | "@types/react-dom": "^18", 31 | "postcss": "^8", 32 | "tailwindcss": "^3.4.1", 33 | "typescript": "^5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankushKun/random-retarded-3am-project/7f526d8df6c7ba9a245005c20e3e269e71717cdb/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankushKun/random-retarded-3am-project/7f526d8df6c7ba9a245005c20e3e269e71717cdb/public/icon.webp -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CallMeMaybe", 3 | "short_name": "CallMeMaybe", 4 | "description": "Connect with people through meaningful video conversations", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#111827", 8 | "theme_color": "#7C3AED", 9 | "icons": [ 10 | { 11 | "src": "/icon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/icon-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from '../contexts/AuthContext'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | import Head from 'next/head'; 5 | import { useEffect } from 'react'; 6 | import { useRouter } from 'next/router'; 7 | import { stopMediaStream } from '../utils/media'; 8 | 9 | export default function Layout({ children, title }: { children: React.ReactNode, title?: string }) { 10 | const { user, logout } = useAuth(); 11 | const router = useRouter(); 12 | const pageTitle = title ? `${title} | Call Me Maybe` : 'Call Me Maybe - Quick Video Chats'; 13 | 14 | useEffect(() => { 15 | const handleRouteChange = (url: string) => { 16 | if (!url.startsWith('/call/')) { 17 | stopMediaStream(); 18 | } 19 | }; 20 | 21 | router.events.on('routeChangeStart', handleRouteChange); 22 | 23 | return () => { 24 | router.events.off('routeChangeStart', handleRouteChange); 25 | }; 26 | }, [router]); 27 | 28 | return ( 29 |
30 | 31 | {pageTitle} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 84 | 85 |
86 | {children} 87 |
88 |
89 | ); 90 | } -------------------------------------------------------------------------------- /src/components/ProfileSetup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { getAuthHeader } from '../utils/api'; 3 | 4 | interface ProfileSetupProps { 5 | onComplete: () => void; 6 | } 7 | 8 | export default function ProfileSetup({ onComplete }: ProfileSetupProps) { 9 | const [name, setName] = useState(''); 10 | const [gender, setGender] = useState<'male' | 'female' | ''>(''); 11 | const [isSubmitting, setIsSubmitting] = useState(false); 12 | const [error, setError] = useState(null); 13 | 14 | const handleSubmit = async (e: React.FormEvent) => { 15 | e.preventDefault(); 16 | if (!name.trim() || !gender) { 17 | setError('Please fill in all fields'); 18 | return; 19 | } 20 | 21 | setIsSubmitting(true); 22 | setError(null); 23 | 24 | try { 25 | const response = await fetch('/api/profile/update', { 26 | method: 'POST', 27 | headers: await getAuthHeader(), 28 | body: JSON.stringify({ 29 | name: name.trim(), 30 | gender, 31 | }), 32 | }); 33 | 34 | if (!response.ok) { 35 | const data = await response.json(); 36 | throw new Error(data.error || 'Failed to update profile'); 37 | } 38 | 39 | onComplete(); 40 | } catch (error) { 41 | console.error('Error saving profile:', error); 42 | setError('Failed to save profile. Please try again.'); 43 | } finally { 44 | setIsSubmitting(false); 45 | } 46 | }; 47 | 48 | return ( 49 |
50 |
51 |
52 |

53 | Complete Your Profile 54 |

55 |

56 | Please tell us a bit about yourself 57 |

58 |
59 |
60 | {error && ( 61 |
62 | {error} 63 |
64 | )} 65 |
66 |
67 | 68 | setName(e.target.value)} 74 | className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-t-md focus:outline-none focus:ring-purple-500 focus:border-purple-500 focus:z-10 sm:text-sm dark:bg-gray-800" 75 | placeholder="Your name" 76 | /> 77 |
78 |
79 | 95 | 111 |
112 |
113 | 114 |
115 | 123 |
124 |
125 |
126 |
127 | ); 128 | } -------------------------------------------------------------------------------- /src/components/TermsOfServicePopup.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | 5 | const TermsOfServicePopup = () => { 6 | const [isVisible, setIsVisible] = useState(false); 7 | 8 | useEffect(() => { 9 | // Check if the user has already accepted the terms 10 | let accepted = localStorage.getItem('tosAccepted'); 11 | if (accepted) accepted = JSON.parse(accepted) 12 | if (!accepted) { 13 | setIsVisible(true); 14 | } 15 | }, []); 16 | 17 | const handleAccept = () => { 18 | localStorage.setItem('tosAccepted', 'true'); 19 | setIsVisible(false); 20 | }; 21 | 22 | if (!isVisible) return null; 23 | 24 | const router = useRouter(); 25 | if (router.pathname === '/tos') { 26 | return null; 27 | } 28 | 29 | return ( 30 |
31 |
32 |

Terms of Service

33 |
34 | Please read and accept our{' '} 35 | 36 |
Terms of Service
37 | {' '} 38 | to continue using the service. 39 |
40 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default TermsOfServicePopup; -------------------------------------------------------------------------------- /src/config/firebase-admin.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp, getApps, cert } from 'firebase-admin/app'; 2 | import { getAuth } from 'firebase-admin/auth'; 3 | import { getFirestore } from 'firebase-admin/firestore'; 4 | 5 | const firebaseAdminConfig = { 6 | credential: cert({ 7 | projectId: process.env.FIREBASE_ADMIN_PROJECT_ID, 8 | clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL, 9 | privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n') 10 | }) 11 | }; 12 | 13 | const app = getApps().length === 0 ? initializeApp(firebaseAdminConfig) : getApps()[0]; 14 | const auth = getAuth(app); 15 | const db = getFirestore(app); 16 | 17 | export { app, auth, db }; -------------------------------------------------------------------------------- /src/config/firebase.ts: -------------------------------------------------------------------------------- 1 | // import { getAnalytics } from 'firebase/analytics'; 2 | import { initializeApp, getApps } from 'firebase/app'; 3 | import { getAuth, GoogleAuthProvider } from 'firebase/auth'; 4 | import { getFirestore } from 'firebase/firestore'; 5 | 6 | const firebaseConfig = { 7 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 8 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 9 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 10 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 11 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 12 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 13 | measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID 14 | }; 15 | 16 | // Initialize Firebase 17 | const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; 18 | const auth = getAuth(app); 19 | const db = getFirestore(app); 20 | const googleProvider = new GoogleAuthProvider(); 21 | // const analytics = () => getAnalytics(app); 22 | 23 | export { app, auth, db, googleProvider }; -------------------------------------------------------------------------------- /src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from 'react'; 2 | import { 3 | User, 4 | signInWithPopup, 5 | signOut, 6 | onAuthStateChanged, 7 | createUserWithEmailAndPassword, 8 | signInWithEmailAndPassword 9 | } from 'firebase/auth'; 10 | import { auth, googleProvider, db } from '../config/firebase'; 11 | import { doc, setDoc, getDoc } from 'firebase/firestore'; 12 | import { useRouter } from 'next/router'; 13 | 14 | interface UserProfile { 15 | name?: string; 16 | gender?: 'male' | 'female'; 17 | } 18 | 19 | interface AuthContextType { 20 | user: User | null; 21 | loading: boolean; 22 | signInWithGoogle: () => Promise; 23 | logout: () => Promise; 24 | userProfile: UserProfile | null; 25 | profileComplete: boolean; 26 | signInWithEmail: (email: string, password: string) => Promise; 27 | signUpWithEmail: (email: string, password: string) => Promise; 28 | } 29 | 30 | const AuthContext = createContext({} as AuthContextType); 31 | 32 | export function AuthProvider({ children }: { children: React.ReactNode }) { 33 | const router = useRouter(); 34 | const [user, setUser] = useState(null); 35 | const [loading, setLoading] = useState(true); 36 | const [userProfile, setUserProfile] = useState(null); 37 | const [profileComplete, setProfileComplete] = useState(false); 38 | 39 | useEffect(() => { 40 | const unsubscribe = onAuthStateChanged(auth, async (user) => { 41 | if (user) { 42 | try { 43 | const userDocRef = doc(db, 'users', user.uid); 44 | const userDoc = await getDoc(userDocRef); 45 | 46 | if (!userDoc.exists()) { 47 | // Create initial user document 48 | await setDoc(userDocRef, { 49 | email: user.email, 50 | displayName: user.displayName, 51 | photoURL: user.photoURL, 52 | createdAt: new Date(), 53 | }); 54 | setProfileComplete(false); 55 | } else { 56 | // Check if profile is complete 57 | const userData = userDoc.data(); 58 | setUserProfile(userData); 59 | setProfileComplete(!!(userData.name && userData.gender)); 60 | } 61 | } catch (error) { 62 | console.error('Firestore error:', error); 63 | } 64 | } 65 | setUser(user); 66 | setLoading(false); 67 | }); 68 | 69 | return unsubscribe; 70 | }, []); 71 | 72 | const signInWithGoogle = async () => { 73 | try { 74 | await signInWithPopup(auth, googleProvider); 75 | } catch (error) { 76 | console.error('Error signing in with Google:', error); 77 | throw error; 78 | } 79 | }; 80 | 81 | const logout = async () => { 82 | try { 83 | // Sign out from Firebase 84 | await signOut(auth); 85 | 86 | // Clear states 87 | setUser(null); 88 | setUserProfile(null); 89 | setProfileComplete(false); 90 | 91 | // Redirect to homepage 92 | await router.push('/'); 93 | } catch (error) { 94 | console.error('Error signing out:', error); 95 | throw error; 96 | } 97 | }; 98 | 99 | const signInWithEmail = async (email: string, password: string) => { 100 | try { 101 | await signInWithEmailAndPassword(auth, email, password); 102 | } catch (error: any) { 103 | console.error('Error signing in with email:', error); 104 | if (error.code === 'auth/user-not-found' || error.code === 'auth/wrong-password') { 105 | throw new Error('Invalid email or password'); 106 | } 107 | throw error; 108 | } 109 | }; 110 | 111 | const signUpWithEmail = async (email: string, password: string) => { 112 | try { 113 | await createUserWithEmailAndPassword(auth, email, password); 114 | } catch (error: any) { 115 | console.error('Error signing up with email:', error); 116 | if (error.code === 'auth/email-already-in-use') { 117 | throw new Error('Email already in use'); 118 | } 119 | throw error; 120 | } 121 | }; 122 | 123 | return ( 124 | 134 | {!loading && children} 135 | 136 | ); 137 | } 138 | 139 | export const useAuth = () => useContext(AuthContext); -------------------------------------------------------------------------------- /src/lib/firebaseAnalytics.ts: -------------------------------------------------------------------------------- 1 | // import { analytics } from "../config/firebase"; 2 | // import { logEvent } from "firebase/analytics"; 3 | 4 | // // Helper function to log events 5 | // export const logFirebaseEvent = (eventName: string, eventParams?: { [key: string]: any }) => { 6 | // if (analytics) { 7 | // logEvent(analytics(), eventName, eventParams); 8 | // } 9 | // }; -------------------------------------------------------------------------------- /src/lib/gtag.ts: -------------------------------------------------------------------------------- 1 | // Global site tag (gtag.js) integration for Google Analytics 2 | 3 | export const GA_TRACKING_ID = 'G-ZBRY04PRF7'; // Replace with your actual tracking ID 4 | 5 | // Log page views 6 | export const pageview = (url: string) => { 7 | if (typeof window !== 'undefined' && window.gtag) { 8 | window.gtag('config', GA_TRACKING_ID, { 9 | page_path: url, 10 | }); 11 | } 12 | }; 13 | 14 | // Log specific events 15 | export const event = ({ 16 | action, 17 | category, 18 | label, 19 | value, 20 | }: { 21 | action: string; 22 | category: string; 23 | label: string; 24 | value: number; 25 | }) => { 26 | if (typeof window !== 'undefined' && window.gtag) { 27 | window.gtag('event', action, { 28 | event_category: category, 29 | event_label: label, 30 | value: value, 31 | }); 32 | } 33 | }; 34 | 35 | declare global { 36 | interface Window { 37 | gtag: (command: string, ...args: any[]) => void; 38 | } 39 | } -------------------------------------------------------------------------------- /src/middleware/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '../config/firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | export interface AuthenticatedRequest extends NextApiRequest { 5 | user: { 6 | uid: string; 7 | email: string; 8 | }; 9 | } 10 | 11 | export async function authMiddleware( 12 | req: AuthenticatedRequest, 13 | res: NextApiResponse, 14 | next: () => Promise 15 | ) { 16 | try { 17 | const authHeader = req.headers.authorization; 18 | if (!authHeader?.startsWith('Bearer ')) { 19 | return res.status(401).json({ error: 'Unauthorized' }); 20 | } 21 | 22 | const token = authHeader.split('Bearer ')[1]; 23 | const decodedToken = await auth.verifyIdToken(token); 24 | 25 | req.user = { 26 | uid: decodedToken.uid, 27 | email: decodedToken.email || '', 28 | }; 29 | 30 | await next(); 31 | } catch (error) { 32 | console.error('Auth error:', error); 33 | return res.status(401).json({ error: 'Unauthorized' }); 34 | } 35 | } -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { useRouter } from 'next/router'; 4 | import { useEffect } from 'react'; 5 | import { AuthProvider } from '../contexts/AuthContext'; 6 | import TermsOfServicePopup from '../components/TermsOfServicePopup'; 7 | import { Analytics } from "@vercel/analytics/react" 8 | import Script from "next/script"; 9 | import { GA_TRACKING_ID } from "../lib/gtag"; 10 | 11 | export default function App({ Component, pageProps }: AppProps) { 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | const handleRouteChange = (url: string) => { 16 | window.gtag("config", GA_TRACKING_ID, { 17 | page_path: url, 18 | }); 19 | } 20 | router.events.on("routeChangeComplete", handleRouteChange); 21 | return () => { 22 | router.events.off("routeChangeComplete", handleRouteChange); 23 | } 24 | }, [router.events]); 25 | 26 | return ( 27 | 28 | {/* Global Site Tag (gtag.js) - Google Analytics */} 29 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | {/* Global site tag (gtag.js) - Google Analytics */} 8 | 9 |