diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index 212f281..0000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -GEMINI_API_KEY = AIzaSyBqefsdar7xSAdcwOrBD1Ms9Br4HwhobX8 -FOURSQUARE_API_KEY = fsq3cRwyW6eIparfiN7ZENLK0nKHhRBVDp7U2whlXy3Jbjw= -GCP_PROJECT_ID = loca-bc18e -GOOGLE_PLACE_API = AIzaSyC1BX6Wcgrp6jibvldv2QJbEAyRzdWKWkc \ No newline at end of file diff --git a/.firebase/hosting.b3V0.cache b/.firebase/hosting.b3V0.cache new file mode 100644 index 0000000..d902f03 --- /dev/null +++ b/.firebase/hosting.b3V0.cache @@ -0,0 +1,47 @@ +next.svg,1720078907463,0665b9ab4493aa9d7e988b57024e28772ae543e804b8cf6b0a3633854b7c3c7f +index.html,1721108752590,18c1a30e632e106c2df9639946f964401ba16f4a59248209ae4d637c40d0e195 +404.html,1721108761920,2f33f881b7e806b53c45c679f4dc8bc2d60a3b4a5138b439fcea2bc6088048ff +favicon.ico,1721108752990,8f464a2201e59f4efcad2c7313c115aaebe88e82b0d96f746140b0375263a396 +vercel.svg,1720078907483,41e95a86eeec887b8b898097594cf4c4bc5da8f58faf05830e1229d7dcbeea59 +index.txt,1721108752580,b9c6297ec521a050b402ae498e0956d532f59158a3cd7ab472093fde72e41422 +_next/static/P1Nx25pI-8_chBsRCVYs2/_buildManifest.js,1721108744146,aab49d661705fbefd794fd4ea2a912544b2bf31823a8c67198f147a334c2b861 +_next/static/P1Nx25pI-8_chBsRCVYs2/_ssgManifest.js,1721108753470,dc28a4dc92fe352ed5d2201bd3972ce47691bc8e89e0400a68d1541d0567c6d5 +chat.txt,1721108752828,fa649c5ca646750484894a9fc08ddea483f432c1219f2dbb9abce91a98983b18 +chat.html,1721108752847,47868c81f21cb9727ce9c02a2a9f86c1f891b5031c9f10962ba5a6a3b7a55384 +_next/static/chunks/main-app-32fe7ebe92a0f16b.js,1721108744139,2a119469936135ceb11cece1397c0a921b38f0f0dd7cfbd1f738d3209c00e4c7 +_next/static/chunks/webpack-281d0fa22dd07a72.js,1721108744139,d891a562e16662afb0a81c2261944186138295290bf82a0aa148b04e9a3e7341 +_next/static/css/a81c10de441ea55b.css,1721108744147,d2df6bef3bda47d90c946988a8865d93298729340b482b8de72538a8648ca6ee +_next/static/media/4f2204fa15b9b11a-s.woff2,1721108744138,470dd2a44038d0be5a0e552823b1835df6c1e5a54ab84447f2c27e220485d66e +_next/static/media/logo-black.29edc37f.png,1721108737671,25df612956eefe829a4e21fb4da01429424792f0b4facdc5c79b20e04d9b70c7 +_next/static/chunks/726-acee4750eb23933c.js,1721108744145,011027c5ccfa6b0f635469b380440d3298c55c4999f76efd7a9aaf35dd7e825b +_next/static/chunks/195-b8abb1f443ec72c3.js,1721108744145,05874d09d126fea1cc1ef5cc996a524b89a71bdcba73ff317d03c47ff4a2bd93 +_next/static/chunks/app/page-4c38e96105ca817f.js,1721108744139,534a2d787730b569bd58e7ef06a20c86a362453ade6dcd9eae125b0aac201f6c +_next/static/chunks/app/layout-4f49777aec53c3fa.js,1721108744139,15e62102a586650db878767dcc6a8fb5e24b1d414e40d288f1a4dddeb60f706c +_next/static/chunks/pages/_app-6a626577ffa902a4.js,1721108744139,da4e577bd19ab14c18a4b8604793a7c5efecf88cabe7c531ccecb37eef84fdbb +_next/static/chunks/pages/_error-1be831200e60c5c0.js,1721108744139,cc6560c1f7aa97ce4c3af32428121dfb1d7d9a2f48aa99f3f6d9a9e2b8eab20d +_next/static/chunks/app/_not-found/page-1851a68c43307a99.js,1721108744140,1c65d7cb8f9c03005c333250d0de46931a8986ead877940b88af78a427d61350 +svg/logo-no-background.svg,1720078631740,ada015423e21cde2ef4bf6d4bbc6fd78e13ba143bea058570b2f5b9069a73f85 +svg/logo-white.svg,1720078631711,7ee1ae93e08fc765fbc680a7c9b51c0a60825d78eab90cdbd9aaa59f84dad962 +svg/logo-black.svg,1720078631636,8331392aa00eb6a9e5998195da0ae5de868aec08183f01656c9f777e5f973224 +svg/logo-color.svg,1720078631678,f38c2285da5e1b0d2439f7e4149c67ce1d18ae936022ac427914552a9e480bf7 +_next/static/chunks/app/chat/page-bfb32560dc196295.js,1721108744139,a3160c783b9d3ea605d96dc69ee270a976bfabd1b520f40862a369bd5c0d12ac +png/logo-white.png,1720078631841,8f464a2201e59f4efcad2c7313c115aaebe88e82b0d96f746140b0375263a396 +png/logo-black.png,1720078631796,25df612956eefe829a4e21fb4da01429424792f0b4facdc5c79b20e04d9b70c7 +png/logo-color.png,1720078631811,ac7b833f9aa87744217f72e7be6b11c9011c0d41a9694f2b30ca0a2cc5521dff +pdf/logo-white.pdf,1720078631953,0c9a327a4c95db73370c30b77821ab5c02b4f3a670c66854af3fc6a75ca7624c +pdf/logo-no-background.pdf,1720078631874,e43b8d19d82c47c8a68fbffe045781e0454ea850d72807e8efea6960382d77b3 +pdf/logo-color.pdf,1720078631922,0455cc77ca63007a4e31867d0b36a995baab7c9f565887b4d364ced697a712eb +pdf/logo-black.pdf,1720078631895,1b2ab5c33aab53b59cc310812819be01548adaa078e08d4e581fedd664fcce1a +_next/static/media/logo-no-background.fef91868.png,1721108737671,601e801092f36f6e13c1b5e24407fe4e9b4bb07e7e1f2560377a4bddbd8f77d1 +_next/static/media/07a54048a9278940-s.p.woff2,1721108744138,5ae154dc68f402229e92a67a1d72ed02d607dfa68e4a392ffccb8df113575c92 +png/logo-no-background.png,1720078631772,601e801092f36f6e13c1b5e24407fe4e9b4bb07e7e1f2560377a4bddbd8f77d1 +_next/static/chunks/polyfills-78c92fac7aa8fdd8.js,1721108744145,93e304855c6f9653fea1a8f24da04735f940c4a2170e5af8a5e2de396f449d07 +_next/static/chunks/main-2bd947d30d4a370d.js,1721108744139,e40d1998f51567afa692bf51c99814019d0e655cb8e9a243043646aac87c4d00 +_next/static/chunks/46be18a3-8f27bb02c0a4aa3b.js,1721108744145,eab520951de32fde0d385efa06854d59cc630945b6d699026e709def67bddf61 +_next/static/chunks/23-63cd49dc8cd7bfb1.js,1721108744145,74880b4e07eedc25b4f3e4a113b7b9cd92e8268f962d0d801a61462db9f367dc +_next/static/chunks/446-0e1348509952c6a6.js,1721108744145,99fd14169dad741ee81cda5e5f07c86e15fa8c50b69f38519fb83a11cc8b2065 +_next/static/chunks/902-b1e1a75c353e4a16.js,1721108744145,ca05c6f5df500e508db6c88126a98c90c249cddd0c07a7cc001400def1867712 +_next/static/chunks/framework-f66176bb897dc684.js,1721108744139,b95536ab177e1a3e5dec8e463446de6222de3061fc82bfebcf738302e614056d +_next/static/chunks/fd9d1056-82fc2a82826c61b9.js,1721108744145,9bfaeeb9c40935b045e33d970fb381a6325b01f2f1483dfc0d434035f188b62d +_next/static/media/user.70030e23.png,1721108737671,0f924d0d073e90f208f43eefb79de4f3e580a5891a8681677c723e3d8c2e0be6 +png/user.png,1711460863825,0f924d0d073e90f208f43eefb79de4f3e580a5891a8681677c723e3d8c2e0be6 diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..9146f09 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "loca-bc18e" + } +} diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml new file mode 100644 index 0000000..974fcb9 --- /dev/null +++ b/.github/workflows/nextjs.yml @@ -0,0 +1,93 @@ +# Sample workflow for building and deploying a Next.js site to GitHub Pages +# +# To get started with Next.js see: https://nextjs.org/docs/getting-started +# +name: Deploy Next.js site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Detect package manager + id: detect-package-manager + run: | + if [ -f "${{ github.workspace }}/yarn.lock" ]; then + echo "manager=yarn" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=yarn" >> $GITHUB_OUTPUT + exit 0 + elif [ -f "${{ github.workspace }}/package.json" ]; then + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT + exit 0 + else + echo "Unable to determine package manager" + exit 1 + fi + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: ${{ steps.detect-package-manager.outputs.manager }} + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + # + # You may remove this line if you want to manage the configuration yourself. + static_site_generator: next + - name: Restore cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- + - name: Install dependencies + run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + - name: Build with Next.js + run: ${{ steps.detect-package-manager.outputs.runner }} next build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./out + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index fd3dbb5..06b2a7c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel @@ -34,3 +35,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +firebaseAdmin.json + +.vercel diff --git a/.idx/integrations.json b/.idx/integrations.json new file mode 100644 index 0000000..69f8234 --- /dev/null +++ b/.idx/integrations.json @@ -0,0 +1,7 @@ +{ + "firebase_hosting": {}, + "cloud_run_deploy": {}, + "gemini_api": {}, + "google_maps_platform": {}, + "secrets_manager": {} +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1b8ac88 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/.vscode/settings.json b/.vscode/settings.json index bf46335..acd02cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,20 @@ { - "cSpell.words": [ - "geocode", - "Horizonal", - "Loca", - "Superjson", - "vertexai" - ] -} \ No newline at end of file + "cSpell.words": [ + "admindb", + "autosize", + "Bookingaction", + "chatpage", + "Deafultchatpage", + "firstvisitpopup", + "geocode", + "googlemaps", + "Horizonal", + "Jsons", + "Loca", + "Superjson", + "vertexai", + "viewmore" + ], + "IDX.aI.enableInlineCompletion": true, + "IDX.aI.enableCodebaseIndexing": true +} diff --git a/DockerFile b/DockerFile new file mode 100644 index 0000000..1b99388 --- /dev/null +++ b/DockerFile @@ -0,0 +1,17 @@ +FROM node:latest + +WORKDIR /app + +COPY package.json . + +RUN npm install + +COPY . . + +RUN npm run build + +COPY .next ./.next. + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/README.md b/README.md index c403366..0f90655 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,92 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +Loca: Find Local Services Fast -## Getting Started +Welcome to Loca, your go-to solution for instantly connecting with local experts. Whether you need a plumber, electrician, or any other service provider, Loca is here to help you find the best professionals near you in no time. -First, run the development server: +Features -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` + • Instant Service Discovery: Quickly find local services based on your location. + • User-Friendly Interaction: Simple and intuitive interface for seamless user experience. + • Real-time Updates: Get real-time information on available service providers. + • Enhanced with Google Places API: Leveraging the power of Google Places API for accurate and comprehensive local service listings. -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Demo -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Here’s a quick demo of how Loca works: -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +User: “Hey Loca, any plumbers near me in Texas?” +Loca: “Yes! I found a great one nearby. Check it out and book now!” -## Learn More +#Getting Started -To learn more about Next.js, take a look at the following resources: +To get started with Loca, follow these steps: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +Prerequisites -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + • Node.js and npm installed on your machine. + • A Google Places API key. -## Deploy on Vercel +Installation -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + 1. Clone the repository: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +git clone https://github.com/git-create-devben/loca.git + + + 2. Navigate to the project directory: + +cd loca + + + 3. Install the required dependencies: + +npm install + + + +Configuration + + 1. Create a .env file in the root directory and add your Google Places API key: + +REACT_APP_GOOGLE_PLACES_API_KEY=your_api_key_here + + + +Running the Application + + 1. Start the development server: + +npm start + + + 2. Open your browser and go to http://localhost:3000 to see Loca in action. + +Usage + +When you open Loca, you’ll be greeted with a friendly welcome message: + +Welcome text: “Hey! What can I find for you today?” + +Simply type in your query, and Loca will provide you with a list of local service providers based on your location. + +Contribution + +We welcome contributions from the community! To contribute: + + 1. Fork the repository. + 2. Create a new branch (git checkout -b feature-branch). + 3. Make your changes. + 4. Commit your changes (git commit -m 'Add some feature'). + 5. Push to the branch (git push origin feature-branch). + 6. Open a Pull Request. + +License + +This project is licensed under the MIT License. See the LICENSE file for details. + +Acknowledgements + + • Thanks to the Google Places API for providing comprehensive local service listings. + +##Contact + +If you have any questions or suggestions, feel free to reach out to us at devbenofficial@gnail.com or benlad636@gmail.con. \ No newline at end of file diff --git a/app/api/create-session/route.ts b/app/api/create-session/route.ts new file mode 100644 index 0000000..071d70a --- /dev/null +++ b/app/api/create-session/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { adminAuth as auth } from "@/lib/firebaseAdmin"; + +export async function POST(req: NextRequest) { + const { idToken } = await req.json(); + + try { + const expiresIn = 60 * 60 * 24 * 5 * 1000; // 5 days + const sessionCookie = await auth.createSessionCookie(idToken, { + expiresIn, + }); + + const response = NextResponse.json({ status: "success" }); + response.cookies.set("session", sessionCookie, { + maxAge: expiresIn, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + }); + + return response; + } catch (error) { + console.error("Error creating session:", error); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } +} diff --git a/app/api/debug/route.ts b/app/api/debug/route.ts new file mode 100644 index 0000000..3f3116d --- /dev/null +++ b/app/api/debug/route.ts @@ -0,0 +1,11 @@ +// pages/api/debug.ts +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest, res: NextResponse) { + const test = { + PROJECT_GOOGLE: JSON.parse( + JSON.stringify(process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON), + ), + }; + return NextResponse.json({ HELLO: "HELLO WORLD", DEBUG: test }); +} diff --git a/app/api/gemini/route.ts b/app/api/gemini/route.ts index 027caa4..2d23768 100644 --- a/app/api/gemini/route.ts +++ b/app/api/gemini/route.ts @@ -1,133 +1,100 @@ -import { NextRequest, NextResponse } from 'next/server'; -import axios from 'axios'; -import { - VertexAI, - HarmCategory, - HarmBlockThreshold, -} from '@google-cloud/vertexai'; - -// Initialize Vertex AI with your Cloud project and location -const vertexAI = new VertexAI({ - project: process.env.GCP_PROJECT_ID, - location: 'us-central1', -}); - -// Function to get local services from Google Places API -async function getLocalServices(query: string, latitude: string, longitude: string) { - const apiKey = process.env.GOOGLE_PLACE_API; - - if (!apiKey) { - console.error('Google Places API key is not set'); - throw new Error('Google Places API key is not configured'); - } +import { NextRequest, NextResponse } from "next/server"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { getLocalServices } from "@/lib/getLocationServices"; + +import { faqs } from "@/app/faq/data"; +import { adminAuth, admindb } from "@/lib/firebaseAdmin"; +import { extractServiceKeywords } from "@/lib/extractServiceKeywords"; + +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ""); +export async function POST(req: NextRequest) { try { - const response = await axios.get('https://maps.googleapis.com/maps/api/place/nearbysearch/json', { - params: { - location: `${latitude},${longitude}`, - radius: 5000, // Search within 5km radius - type: 'business', // This can be adjusted based on the specific types you're interested in - keyword: query, - key: apiKey - }, - timeout: 5000, // Reduced timeout for faster failure - }); + const sessionCookie = req.cookies.get("session")?.value; + if (!sessionCookie) { + return NextResponse.json({ error: "No session cookie found" }, { status: 401 }); + } - if (response.data.status === 'REQUEST_DENIED') { - console.error('Google Places API request denied:', response.data.error_message); - throw new Error(`Google Places API request denied: ${response.data.error_message}`); + const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie, true); + const userDoc = await admindb.collection("users").doc(decodedClaims.uid).get(); + const userData = userDoc.data(); + const userName = userData?.name || "User"; + + const { userMessage, latitude, longitude, conversationHistory } = await req.json(); + + if (!userMessage) { + return new Response(JSON.stringify({ error: "Missing required field: userMessage" }), { status: 400 }); } - return response.data.results.slice(0, 5).map((place: any) => ({ - name: place.name, - address: place.vicinity, - rating: place.rating, - user_ratings_total: place.user_ratings_total, - place_id: place.place_id - })); + const encoder = new TextEncoder(); + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); + + const writeChunk = async (chunk: { type: string; data: any }) => { + await writer.write(encoder.encode(JSON.stringify(chunk) + "\n")); + }; + + (async () => { + try { + const serviceKeywords = extractServiceKeywords(userMessage); + let services = []; + if (serviceKeywords) { + services = await getLocalServices(serviceKeywords, latitude, longitude); + await writeChunk({ type: "services", data: services }); + } + + const model = genAI.getGenerativeModel({ model: "gemini-1.5-pro" }); + + // Construct the conversation history + let conversationContext = conversationHistory ? conversationHistory.join("\n") : ""; + conversationContext += `\nUser: ${userMessage}\n`; + + const prompt = `You are Loca, a local AI service finder. You're talking to ${userName}. + Here is some important information about you (Loca) in the form of FAQs: + ${JSON.stringify(faqs)} + + Previous conversation: + ${conversationContext} + + ${services.length > 0 + ? `Here are some available services related to "${serviceKeywords}": ${JSON.stringify(services)}. Provide a helpful response based on this information, highlighting the best options for ${userName}.` + : serviceKeywords + ? `The user asked about "${serviceKeywords}", but I couldn't find any services. Please provide general information or suggestions about this type of service. and let them know mispelled may occur for not showing services if keyword doesn't match the keyword in out keyword database only say this if you think ${userName} is looking for specific service provider` + : `Provide a response about "${userMessage}" for ${userName}, keeping in mind the conversation context. If they are asking about local services and you don't have location data, suggest how they might find them tell them to make sure they're location is turn on in settings of their phone or laptop and tell thwm you can only provide services provider near them because that's your unique and let them know mispelled may occur for not showing services if keyword doesn't match the keyword in out keyword database only say this if you think ${userName} is looking for specific service provider.` + } + + Remember the context of the conversation and respond appropriately to follow-up questions or comments.`; + + const result = await model.generateContentStream(prompt); + + for await (const chunk of result.stream) { + const chunkText = chunk.text(); + await writeChunk({ type: "text", data: chunkText }); + } + + if (services.length > 0) { + await writeChunk({ type: "services", data: services }); + } + } catch (error) { + console.error(" ---- Server Error:", error); + await writeChunk({ + type: "error", + data: "An error occurred while processing your request.", + }); + } finally { + writer.close(); + } + })(); + + return new Response(stream.readable, { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); } catch (error) { - console.error('Google Places API Error:', error); - if (axios.isAxiosError(error) && error.response) { - throw new Error(`Google Places API Error: ${error.response.status} - ${error.response.data.error_message}`); - } else { - throw new Error('Failed to fetch local services'); - } + console.error("Error verifying token or fetching user data:", error); + return new Response(JSON.stringify({ error: "Invalid token or user data not found" }), { status: 403 }); } } -export async function POST(req: NextRequest) { - const { userMessage, latitude, longitude } = await req.json(); - - if (!userMessage || !latitude || !longitude) { - return NextResponse.json({ error: 'Missing required fields: userMessage, latitude, or longitude' }, { status: 400 }); - } - - try { - const model = vertexAI.getGenerativeModel({ model: 'gemini-1.5-pro-001' }); - const chat = model.startChat({ - generationConfig: { - maxOutputTokens: 1024, - temperature: 0.7, - topP: 1, - }, - safetySettings: [ - { - category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, - }, - { - category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, - threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, - }, - { - category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, - threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, - }, - { - category: HarmCategory.HARM_CATEGORY_HARASSMENT, - threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, - }, - ], - }); +// Function to extract service keywords from the input string - console.log('Fetching local services'); - let services; - try { - services = await getLocalServices(userMessage, latitude, longitude); - console.log('Received local services'); - } catch (error) { - console.error('Error fetching local services:', error); - services = []; - } - console.log('Sending message to Vertex AI'); - const contextMessage = `You are to act as a loca an AI local service finder build by devben. User is looking for local services: "${userMessage}". ${ - services.length > 0 - ? `Here are some available services: ${JSON.stringify(services)}. Please provide a helpful response based on this information, highlighting and bold the best options based on ratings and number of reviews. and If ${userMessage} don&t sound like they are looking for local service respond casually for example text like "hello what can you do" you knew you had to reply casually ` - : `Unfortunately, we couldn't find any local services matching the query. Please provide a general response about ${userMessage} and suggest how the user might find local services.` - }`; - - const result = await chat.sendMessage(contextMessage); - const response = await result.response; - console.log('Received response from Vertex AI'); - - const vertexResponseText = response.candidates?.[0]?.content?.parts?.[0]?.text || 'No response from Vertex AI'; - - return NextResponse.json({ - vertexResponse: vertexResponseText, - services - }, { status: 200 }); - } catch (error: any) { - console.error('Server Error:', error.message); - let errorMessage = 'An unexpected error occurred'; - let statusCode = 500; - if (error.message.includes('Google Places API Error')) { - errorMessage = 'Error fetching local services. Please try again later.'; - statusCode = 503; // Service Unavailable - } else if (error.message.includes('Vertex AI')) { - errorMessage = 'Error communicating with AI service. Please try again later.'; - statusCode = 503; - } - return NextResponse.json({ error: errorMessage }, { status: statusCode }); - } -} \ No newline at end of file diff --git a/app/api/verify-session/route.ts b/app/api/verify-session/route.ts new file mode 100644 index 0000000..86ef702 --- /dev/null +++ b/app/api/verify-session/route.ts @@ -0,0 +1,13 @@ +import { NextRequest, NextResponse } from "next/server"; +import { adminAuth as auth } from "@/lib/firebaseAdmin"; + +export async function GET(req: NextRequest) { + const sessionCookie = req.cookies.get("session")?.value || ""; + + try { + const decodedClaims = await auth.verifySessionCookie(sessionCookie, true); + return NextResponse.json({ authenticated: true, user: decodedClaims }); + } catch (error) { + return NextResponse.json({ authenticated: false }, { status: 401 }); + } +} diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 37bf295..c505247 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -1,17 +1,136 @@ -"use client" +"use client"; import Main from "@/components/main"; import Sidebar from "@/components/sidebar"; +import { useEffect, useRef, useState } from "react"; +import { auth } from "@/lib/firebase"; +import local from "@/public/png/logo-black.png"; +import Image from "next/image"; +import FirstVisitPopup from "@/components/firstvisitpopup"; +import { SignOut } from "@/lib/signIn"; +import { LogOut, MenuIcon, X } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { doc, getDoc } from "firebase/firestore"; +import { db } from "@/lib/firebase"; +import { motion, AnimatePresence } from 'framer-motion'; + +interface UserData { + name?: string; + email?: string; + photoURL?: string; +} export default function Chat() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const router = useRouter(); + const sidebarRef = useRef(null); + + useEffect(() => { + const verifySession = async () => { + try { + const res = await fetch("/api/verify-session"); + if (res.ok) { + const data = await res.json(); + if (data.authenticated) { + // Fetch user data from Firestore + const userDoc = await getDoc(doc(db, "users", data.user.uid)); + if (userDoc.exists()) { + setUser(userDoc.data() as UserData); + } else { + throw new Error("User document not found"); + } + } else { + throw new Error("Not authenticated"); + } + } else { + throw new Error("Failed to verify session"); + } + } catch (error) { + console.error("Error verifying session:", error); + router.push("/"); + } finally { + setLoading(false); + } + }; + + verifySession(); + }, [router]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) { + setIsSidebarOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + if (loading) { return ( -
-
- -
-
-
-
- -
+
+
+

Loading...

+
); + } + + if (!user) { + router.push("/"); + return null; + } + + const image = user?.photoURL as string; + + return ( +
+ +
+ +
+
+ +
+
+
+
+ + {isSidebarOpen && ( + + + setIsSidebarOpen(false)} /> + + )} + +
+ ); } \ No newline at end of file diff --git a/app/faq/data.ts b/app/faq/data.ts new file mode 100644 index 0000000..6621c15 --- /dev/null +++ b/app/faq/data.ts @@ -0,0 +1,92 @@ +export const faqs = [ + { + emoji: "❓", + question: "What is Loca?", + answer: + "Loca is a Localized Service Finder that helps you discover and book local services based on your needs. It uses natural language processing to provide relevant information quickly.", + }, + { + emoji: "🔍", + question: "How do I search for a service?", + answer: + "Simply type your query in the search bar, and Loca will provide you with a list of available local services matching your criteria.", + }, + { + emoji: "📞", + question: "Can I contact the service providers directly?", + answer: + "Yes, you can! Loca provides the contact number for each service provider, allowing you to call them directly for more information or to book a service.", + }, + { + emoji: "🖥️", + question: "Can I visit the service provider's website?", + answer: + "Yes, Loca includes links to the service providers' websites, where available, so you can learn more about their offerings and services.", + }, + { + emoji: "📅", + question: "Can I book a service through Loca?", + answer: + "Absolutely! Loca allows you to book services directly through the platform, making it easy and convenient to arrange appointments.", + }, + { + emoji: "🔄", + question: "How frequently is the service information updated?", + answer: + "Loca updates service information regularly to ensure accuracy and relevance, providing you with the latest details available.", + }, + { + emoji: "🌐", + question: "Is Loca available in multiple languages?", + answer: + "Currently, Loca primarily supports English, but we are working on adding more language options to serve a wider audience.", + }, + { + emoji: "🛠️", + question: "What types of services can I find on Loca?", + answer: + "Loca offers a wide range of services, including healthcare, beauty, home maintenance, and more. Whatever you need, Loca can help you find it.", + }, + { + emoji: "💸", + question: "Are there any costs associated with using Loca?", + answer: + "Loca is free to use for finding and booking services. However, the cost of the services themselves depends on the provider.", + }, + { + emoji: "📧", + question: "How can I provide feedback or suggest improvements?", + answer: + "We welcome your feedback! You can contact us through our support page or email us directly with your suggestions and comments.", + }, + { + emoji: "👥", + question: "Can businesses register their services on Loca?", + answer: + "Yes, businesses can register to offer their services on Loca. Please visit our 'For Business' section to learn more and get started.", + }, + { + emoji: "📈", + question: "How does Loca prioritize the services shown?", + answer: + "Loca uses a combination of user ratings, relevance, and proximity to prioritize services, ensuring you get the best options available.", + }, + { + emoji: "🔐", + question: "Is my data safe with Loca?", + answer: + "We take your privacy seriously. Loca uses advanced security measures to protect your data and ensure it is used only for service-related purposes.", + }, + { + emoji: "🚀", + question: "How quickly can I expect to find and book a service?", + answer: + "Loca is designed for speed and convenience. You can typically find and book a service within minutes, depending on availability and your specific needs.", + }, + { + emoji: "🏆", + question: "What makes Loca different from other service finders?", + answer: + "Loca stands out with its localized approach, natural language processing, and easy-to-use interface, making it simple to find and book the best local services.", + }, +]; diff --git a/app/faq/page.tsx b/app/faq/page.tsx new file mode 100644 index 0000000..053da20 --- /dev/null +++ b/app/faq/page.tsx @@ -0,0 +1,45 @@ +"use client"; +import Navbar from "@/components/navbar"; +import React, { useState } from "react"; +import { Accordion } from "@mantine/core"; +import { faqs } from "./data"; +const FAQ = () => { + const [open, setOpen] = useState(false); + const items = faqs.map((item) => ( + + + {item.question} + + {item.answer} + + )); + return ( +
+
+ +
+

+ What is Loca AI? +

+ + {items} + +
+
+
+ ); +}; + +export default FAQ; diff --git a/app/globals.css b/app/globals.css index 6a06f83..d68cf45 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,133 +1,139 @@ @tailwind base; - @tailwind components; - @tailwind utilities; +@tailwind components; +@tailwind utilities; - @layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; +:root { + min-height: 100vh; +} +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: #caccce; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: #caccce; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; - --radius: 0.5rem; - } + --radius: 0.5rem; + } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - } + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; } +} - - @layer base { - * { - @apply border-border box-border p-0 m-0 ; - } - body { - @apply bg-background text-foreground p-0 m-0 bg-black ; - } +@layer base { + * { + @apply border-border; } - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } + body { + @apply bg-background text-foreground font-body; } - - .animate-fadeIn { - animation: fadeIn 0.5s ease-in-out; + + h1, h2, h3, h4, h5, h6 { + @apply font-heading; } - - @keyframes extend { - from { - transform: scale(0.9); - opacity: 0.7; - } - to { - transform: scale(1); - opacity: 1; - } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.animate-fadeIn { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes extend { + from { + transform: scale(0.9); + opacity: 0.7; } - - @keyframes collapse { - from { - transform: scale(1); - opacity: 1; - } - to { - transform: scale(0.9); - opacity: 0.7; - } + to { + transform: scale(1); + opacity: 1; } - - .animate-extend { - animation: extend 0.5s ease-in-out; +} + +@keyframes collapse { + from { + transform: scale(1); + opacity: 1; } - - .animate-collapse { - animation: collapse 0.5s ease-in-out; + to { + transform: scale(0.9); + opacity: 0.7; } - +} + +.animate-extend { + animation: extend 0.5s ease-in-out; +} - /* Customize the scrollbar */ +.animate-collapse { + animation: collapse 0.5s ease-in-out; +} + +/* Customize the scrollbar */ .section::-webkit-scrollbar { width: 6px; /* Set the width of the scrollbar */ } /* Track (background) color */ .section::-webkit-scrollbar-track { - background-color: #1111; + background-color: #ffffff23; border-radius: 100px; /* Rounded corners for the track */ } @@ -136,5 +142,3 @@ background-color: #1111; border-radius: 100px; /* Rounded corners for the thumb */ } - - diff --git a/app/layout.tsx b/app/layout.tsx index 024718d..7cd3f1c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,21 @@ import type { Metadata } from "next"; -import { Inter, Outfit } from "next/font/google"; +import { Manrope } from 'next/font/google' +import { cn } from '@/lib/utils' import "./globals.css"; import { Toaster } from "react-hot-toast"; +import LocalMantineProvider from "@/provider/mantineProvider"; -const inter = Outfit({ subsets: ["latin"] }); +const fontHeading = Manrope({ + subsets: ['latin'], + display: 'swap', + variable: '--font-heading', +}) + +const fontBody = Manrope({ + subsets: ['latin'], + display: 'swap', + variable: '--font-body', +}) export const metadata: Metadata = { title: "Loca", @@ -17,8 +29,15 @@ export default function RootLayout({ }>) { return ( - - {children} + + + {children} + + ); } diff --git a/app/page.tsx b/app/page.tsx index 3320695..06a1eda 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,7 @@ import HomePage from "@/components/home"; export default function Home() { return (
- +
); } diff --git a/auth.ts b/auth.ts index aef0b9f..067f4ec 100644 --- a/auth.ts +++ b/auth.ts @@ -1,5 +1,4 @@ -import NextAuth from "next-auth" - -export const { handlers, signIn, signOut, auth } = NextAuth({ - providers: [], -}) \ No newline at end of file +export async function getServerSideProps(context: { req: { cookies: any } }) { + console.log("Cookies in getServerSideProps:", context.req.cookies); + return { props: {} }; +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100644 index 1e09da0..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/components.json b/components.json index 15f2b02..fa674c9 100644 --- a/components.json +++ b/components.json @@ -14,4 +14,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/components/CardCarousel.tsx b/components/CardCarousel.tsx new file mode 100644 index 0000000..93b1728 --- /dev/null +++ b/components/CardCarousel.tsx @@ -0,0 +1,203 @@ +import { auth } from "@/lib/firebase"; +import { onAuthStateChanged } from "firebase/auth"; +import { useState, useEffect } from "react"; +import { CardComponent } from "./home"; +import Image from "next/image"; +import Logo from "@/public/png/logo-no-background.png"; +import Typewriter from "typewriter-effect"; +import local from "@/public/png/logo-black.png"; +import { motion, AnimatePresence } from "framer-motion"; + +export const CardCarousel = () => { + const [showResponse, setShowResponse] = useState(false); + const [showCard, setShowCard] = useState(false); + const [text, setText] = useState(true); + const [user, setUser] = useState(auth.currentUser); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (currentUser) => { + setUser(currentUser); + }); + + return () => unsubscribe(); + }, []); + + const image = user?.photoURL || local; + const robotText = `Hi ${user?.displayName}, Yes! I found a great one nearby. Check it out and book now.`; + + return ( + + +
+
+ Loca logo + +
+ + {!showResponse && text && ( + +

+ Loca AI +

+
+ )} +
+ {showResponse && ( + + { + t.typeString(robotText) + .callFunction(() => { + setShowCard(true); + }) + .start(); + }} + /> + + )} + + {showCard && } + +
+
+
+ + Loca logo +
+ { + t.typeString( + `

Hey Loca, any plumber near
me in Texas

`, + ) + .callFunction(() => { + setShowResponse(true); + setText(false); + }) + .start(); + }} + /> +
+
+
+ +
+
+ Loca logo + +
+ + {!showResponse && text && ( + +

+ Loca AI +

+
+ )} +
+ {showResponse && ( + + { + t.typeString(robotText) + .callFunction(() => { + setShowCard(true); + }) + .start(); + }} + /> + + )} + + {showCard && } + +
+
+
+ + Loca logo +
+ { + t.typeString( + `

Hey Loca, any plumber near
me in Texas

`, + ) + .callFunction(() => { + setShowResponse(true); + setText(false); + }) + .start(); + }} + /> +
+
+
+
+ ); +}; diff --git a/components/Deafultchatpage.tsx b/components/Deafultchatpage.tsx new file mode 100644 index 0000000..715ed38 --- /dev/null +++ b/components/Deafultchatpage.tsx @@ -0,0 +1,43 @@ +import { CardCarousel } from "./CardCarousel"; +import { motion } from "framer-motion"; + +export const DefaultChatPage = ({ user }: { user: string }) => { + return ( + + + + Hello {user} + + + What can I find for you today? + + + + + + + ); +}; diff --git a/components/LocalServiceCard.tsx b/components/LocalServiceCard.tsx new file mode 100644 index 0000000..c3bafb2 --- /dev/null +++ b/components/LocalServiceCard.tsx @@ -0,0 +1,48 @@ +import { Button } from "@/components/ui/button"; +import { Link } from "lucide-react"; +import { Booking } from "./booking"; + +export const LocalServiceCard: React.FC = ({ + name, + address, + rating, + user_ratings_total, + place_id, + phone_number, + website, + email, +}) => { + return ( +
+

+ Name:{" "} + {name} +

+

+ Address:{" "} + {address} +

+

+ Rating:{" "} + {rating} ({user_ratings_total} reviews) +

+ {/* */} + +
+ ); +}; diff --git a/components/booking.tsx b/components/booking.tsx new file mode 100644 index 0000000..ddcc58a --- /dev/null +++ b/components/booking.tsx @@ -0,0 +1,377 @@ +"use client"; + +import * as React from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { useMediaQuery } from "@custom-react-hooks/all"; +import { useClipboard } from "@mantine/hooks"; +import { CopyCheckIcon, CopyIcon, CopyleftIcon } from "lucide-react"; +import Link from "next/link"; +import { BookingForm } from "./bookingForm"; + +const fadeInUp = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, +}; + +const staggerChildren = { + visible: { transition: { staggerChildren: 0.1 } }, +}; + +export function Booking({ + mapLink, + locationName, + providerName, + providerEmail, + providerWebsite, + providerPhone, +}: { + mapLink: string; + locationName: string; + providerName: string; + providerEmail?: string; + providerPhone?: string; + providerWebsite?: string; +}) { + const [open, setOpen] = React.useState(false); + const isDesktop = useMediaQuery("(min-width: 768px)"); + const [mapError, setMapError] = React.useState(false); + const clipboard = useClipboard({ timeout: 500 }); + React.useEffect(() => { + setMapError(false); + }, [mapLink]); + + const handleMapError = () => { + setMapError(true); + }; + + const ContentWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + if (isDesktop) { + return ( + + + + + + + + + + + Contact {providerName} + + We provide a few options for you to book + + + + +
+
+
+
+ +
+ +
+ {clipboard.copied ? ( + + ) : ( + clipboard.copy(providerPhone)} /> + )} +
+
+
+
+ +
+ +
+ {clipboard.copied ? ( + + ) : ( + clipboard.copy(providerEmail)} /> + )} +
+
+
+
+
+ -{" "} +
+ {!mapError ? ( + + ) : ( +
+

Unable to load map. Please check the link below:

+ + Open Map + +
+ )} + +
+ + + + + {locationName} +
+
+
+
+ + OR + +
+
+ {/* */} + + + ReadMore on How we use Loca to Book you a service provider + + {/* + Booking by loca is still in development and will be available + soon.. + */} +
+
+
+
+
+ ); + } + return ( + + + + + + + + + + + Contact {providerName} + + We provide a few options for you to book + + + + +
+
+
+
+ +
+ +
+ {clipboard.copied ? ( + + ) : ( + clipboard.copy(providerPhone)} /> + )} +
+
+
+
+ +
+ +
+ {clipboard.copied ? ( + + ) : ( + clipboard.copy(providerEmail)} /> + )} +
+
+
+
+
+ -{" "} +
+ {!mapError ? ( + + ) : ( +
+

Unable to load map. Please check the link below:

+ + Open Map + +
+ )} + +
+ + + + + {locationName} +
+
+
+
+ + OR + +
+
+ {/* */} + + + ReadMore on How we use Loca to Book you a service provider + + {/* + Booking by loca is still in development and will be available + soon.. + */} +
+
+
+
+
+ ); +} diff --git a/components/bookingForm.tsx b/components/bookingForm.tsx new file mode 100644 index 0000000..741c4b0 --- /dev/null +++ b/components/bookingForm.tsx @@ -0,0 +1,160 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { db } from "@/lib/firebase"; +import { collection, addDoc } from "firebase/firestore"; + +export function BookingForm() { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [formData, setFormData] = useState({ + name: "", + email: "", + message: "", + }); + const [errors, setErrors] = useState({ + name: "", + email: "", + message: "", + }); + + const handleChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + setErrors((prev) => ({ ...prev, [name]: "" })); + }; + + const validate = () => { + let isValid = true; + const newErrors = { name: "", email: "", message: "" }; + + if (formData.name.length < 2) { + newErrors.name = "Name is required"; + isValid = false; + } + if (!/^\S+@\S+$/.test(formData.email)) { + newErrors.email = "Invalid email"; + isValid = false; + } + if (formData.message.length < 2) { + newErrors.message = "Message is required"; + isValid = false; + } + + setErrors(newErrors); + return isValid; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (validate()) { + try { + const docRef = await addDoc(collection(db, "bookings"), { + ...formData, + createdAt: new Date(), + }); + alert("Thanks for using book with loca demo this feature will be available soon"); + console.log("Document written with ID: ", docRef.id); + router.push("/chat"); + setIsOpen(false); + setFormData({ name: "", email: "", message: "" }); + } catch (e) { + console.error("Error adding document: ", e); + alert("An error occurred while submitting the form"); + } + } + }; + + return ( + <> + + {isOpen && ( +
+
+
+

Booking Form

+ +
+
+
+ + + {errors.name && ( +

{errors.name}

+ )} +
+
+ + + {errors.email && ( +

{errors.email}

+ )} +
+
+ +