added backend and frontend

This commit is contained in:
DivyamAgg24 2025-06-06 21:22:18 +05:30
commit 2ddb77b34a
80 changed files with 12621 additions and 0 deletions

39
LeadGen/.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
crew_agent

0
LeadGen/.npmrc Normal file
View File

84
LeadGen/README.md Normal file
View File

@ -0,0 +1,84 @@
# Turborepo starter
This Turborepo starter is maintained by the Turborepo core team.
## Using this example
Run the following command:
```sh
npx create-turbo@latest
```
## What's inside?
This Turborepo includes the following packages/apps:
### Apps and Packages
- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
### Utilities
This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
pnpm build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
pnpm dev
```
### Remote Caching
> [!TIP]
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks)
- [Caching](https://turborepo.com/docs/crafting-your-repository/caching)
- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching)
- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters)
- [Configuration Options](https://turborepo.com/docs/reference/configuration)
- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference)

24
LeadGen/apps/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,37 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.1",
"framer-motion": "^12.12.1",
"lucide-react": "^0.511.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,42 @@
/* #root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
} */

View File

@ -0,0 +1,19 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"
import { Home } from "./pages/Home"
import Layout from "./Layout/layout"
import { Lead } from "./pages/Lead"
function App() {
return <BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/agent" element={<Lead />}></Route>
</Routes>
</Layout>
</BrowserRouter>
}
export default App

View File

@ -0,0 +1,49 @@
import { Map } from 'lucide-react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
export const Topbar = () => {
return (
<header className="sticky top-0 z-10 backdrop-blur-md bg-white/70 dark:bg-gray-900/80 border-b border-gray-200 dark:border-gray-800 px-7 py-3">
<div className="container flex justify-between items-center">
<motion.div
className="flex items-center my-1 text-2xl gap-3"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1 }}
>
<Link className='flex gap-x-3' to={"/"}>
<Map className="h-8 w-8 text-[hsl(var(--primary))] " />
<h1 className=" font-bold bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--accent))] bg-clip-text text-transparent">
LeadGen
</h1>
</Link>
</motion.div>
{/* <motion.div
className="flex items-center gap-2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<ToggleButton
isToggled={theme === 'dark'}
onToggle={toggleTheme}
label={
<span className="sr-only">
{theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
</span>
}
className="ml-2"
/>
{theme === 'dark' ? (
<Moon className="h-4 w-4 text-gray-400" />
) : (
<Sun className="h-4 w-4 text-amber-500" />
)}
</motion.div> */}
</div>
</header>
);
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import {Topbar} from './Topbar';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 flex flex-col">
<Topbar />
<main className="flex-grow">{children}</main>
</div>
);
};
export default Layout;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,70 @@
import React from 'react';
import { motion } from 'framer-motion';
import { MapPin, Search } from 'lucide-react';
import { Button } from './UI/Button';
interface EmptyStateProps {
type: 'initial' | 'no-results' | 'location-error';
message?: string;
onAction?: () => void;
actionLabel?: string;
}
export function EmptyState({
type,
message,
onAction,
actionLabel
}: EmptyStateProps) {
const illustrations = {
'initial': <Search className="h-16 w-16 text-muted-foreground/50" />,
'no-results': <Search className="h-16 w-16 text-muted-foreground/50" />,
'location-error': <MapPin className="h-16 w-16 text-muted-foreground/50" />,
};
const defaultMessages = {
'initial': 'Share your location to discover nearby places',
'no-results': 'No places found for your search criteria',
'location-error': 'Unable to access your location',
};
const defaultActionLabels = {
'initial': 'Get Started',
'no-results': 'Clear Filters',
'location-error': 'Try Again',
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="flex flex-col items-center justify-center p-8 text-center h-full"
>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ delay: 0.1, duration: 0.3 }}
className="bg-muted/30 dark:bg-muted/10 rounded-full p-6 mb-4"
>
{illustrations[type]}
</motion.div>
<h3 className="text-xl font-semibold mb-2">
{type === 'initial' ? 'Welcome to AI Place Explorer' : (
type === 'no-results' ? 'No Results Found' : 'Location Access Required'
)}
</h3>
<p className="text-muted-foreground mb-6 max-w-md">
{message || defaultMessages[type]}
</p>
{onAction && (
<Button onClick={onAction}>
{actionLabel || defaultActionLabels[type]}
</Button>
)}
</motion.div>
);
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Map, Moon, Sun } from 'lucide-react';
import { Button } from './UI/Button';
import { ToggleButton } from './UI/ToggleButton';
import { motion } from 'framer-motion';
import { useTheme } from '../hooks/useTheme';
export function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className="sticky top-0 z-10 backdrop-blur-md bg-white/70 dark:bg-gray-900/80 border-b border-gray-200 dark:border-gray-800 px-4 py-3">
<div className="container mx-auto flex justify-between items-center">
<motion.div
className="flex items-center gap-2"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<Map className="h-6 w-6 text-primary" />
<h1 className="text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
AI Place Explorer
</h1>
</motion.div>
<motion.div
className="flex items-center gap-2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<ToggleButton
isToggled={theme === 'dark'}
onToggle={toggleTheme}
label={
<span className="sr-only">
{theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
</span>
}
className="ml-2"
/>
{theme === 'dark' ? (
<Moon className="h-4 w-4 text-gray-400" />
) : (
<Sun className="h-4 w-4 text-amber-500" />
)}
</motion.div>
</div>
</header>
);
}

View File

@ -0,0 +1,131 @@
import React from 'react';
import { Star, MapPin, Phone, Globe, Clock, ExternalLink } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from './UI/Card';
import { Badge } from './UI/Badge';
import { Button } from './UI/Button';
import { Place } from '../types';
import { formatDistance, formatRating, formatPriceLevel, getCategoryIconName } from '../lib/utils';
interface PlaceCardProps {
place: Place;
index: number;
onViewDetails: (place: Place) => void;
}
export function PlaceCard({ place, index, onViewDetails }: PlaceCardProps) {
const CategoryIcon = getCategoryIconName(place.category);
return (
<Card
className="w-full h-full flex flex-col bg-card dark:bg-gray-800 hover:shadow-lg transition-shadow duration-200"
index={index}
>
{place.photos && place.photos.length > 0 && (
<div className="relative h-48 overflow-hidden">
<img
src={place.photos[0]}
alt={place.name}
className="w-full h-full object-cover"
/>
<div className="absolute top-2 right-2">
<Badge
variant="info"
className="flex items-center gap-1 font-medium"
>
<MapPin size={12} />
{formatDistance(place.distance)}
</Badge>
</div>
</div>
)}
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div>
<Badge
variant="secondary"
className="mb-2 capitalize"
>
{place.category}
</Badge>
<CardTitle className="text-xl">{place.name}</CardTitle>
</div>
{place.priceLevel && (
<Badge variant="outline">
{formatPriceLevel(place.priceLevel)}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">
{place.description}
</p>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-amber-500">
<Star size={16} className="fill-amber-500" />
<span className="font-medium">{formatRating(place.rating)}</span>
</div>
<div className="flex items-start gap-2">
<MapPin size={16} className="mt-0.5 text-gray-500" />
<span className="text-muted-foreground">{place.address}</span>
</div>
{place.openHours && (
<div className="flex items-start gap-2">
<Clock size={16} className="mt-0.5 text-gray-500" />
<span className="text-muted-foreground">{place.openHours}</span>
</div>
)}
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={() => onViewDetails(place)}
>
View Details
</Button>
<div className="flex gap-1">
{place.phoneNumber && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
>
<a href={`tel:${place.phoneNumber}`} aria-label="Call">
<Phone size={16} />
</a>
</Button>
)}
{place.website && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
>
<a
href={place.website}
target="_blank"
rel="noopener noreferrer"
aria-label="Visit website"
>
<ExternalLink size={16} />
</a>
</Button>
)}
</div>
</CardFooter>
</Card>
);
}

View File

@ -0,0 +1,210 @@
import React from 'react';
import { motion } from 'framer-motion';
import { X, Star, MapPin, Phone, Globe, Clock, ExternalLink } from 'lucide-react';
import { Button } from './UI/Button';
import { Badge } from './UI/Badge';
import { Place } from '../types';
import { formatRating, formatPriceLevel } from '../lib/utils';
interface PlaceDetailProps {
place: Place;
onClose: () => void;
}
export function PlaceDetail({ place, onClose }: PlaceDetailProps) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
initial={{ y: 20 }}
animate={{ y: 0 }}
className="bg-card dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="relative">
{/* Header image */}
{place.photos && place.photos.length > 0 ? (
<div className="relative h-64">
<img
src={place.photos[0]}
alt={place.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
{/* Close button */}
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 text-white bg-black/20 hover:bg-black/40"
onClick={onClose}
>
<X size={20} />
</Button>
{/* Title and badges overlay */}
<div className="absolute bottom-0 left-0 right-0 p-6 text-white">
<div className="flex flex-wrap gap-2 mb-2">
<Badge variant="secondary" className="capitalize">
{place.category}
</Badge>
{place.priceLevel && (
<Badge variant="outline" className="text-white border-white/50">
{formatPriceLevel(place.priceLevel)}
</Badge>
)}
</div>
<h1 className="text-2xl font-bold">{place.name}</h1>
</div>
</div>
) : (
<div className="flex justify-between items-center p-6">
<div>
<div className="flex flex-wrap gap-2 mb-2">
<Badge variant="secondary" className="capitalize">
{place.category}
</Badge>
{place.priceLevel && (
<Badge variant="outline">
{formatPriceLevel(place.priceLevel)}
</Badge>
)}
</div>
<h1 className="text-2xl font-bold">{place.name}</h1>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
>
<X size={20} />
</Button>
</div>
)}
</div>
{/* Content */}
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<div className="flex items-center gap-1 text-amber-500">
<Star size={18} className="fill-amber-500" />
<span className="font-medium">{formatRating(place.rating)}</span>
</div>
</div>
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">About</h2>
<p className="text-muted-foreground">
{place.description}
</p>
</div>
<div className="space-y-4 mb-6">
<h2 className="text-lg font-semibold">Details</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-start gap-2">
<MapPin size={18} className="mt-0.5 text-gray-500" />
<span className="text-muted-foreground">{place.address}</span>
</div>
{place.openHours && (
<div className="flex items-start gap-2">
<Clock size={18} className="mt-0.5 text-gray-500" />
<span className="text-muted-foreground">{place.openHours}</span>
</div>
)}
{place.phoneNumber && (
<div className="flex items-start gap-2">
<Phone size={18} className="mt-0.5 text-gray-500" />
<a
href={`tel:${place.phoneNumber}`}
className="text-muted-foreground hover:text-primary transition-colors"
>
{place.phoneNumber}
</a>
</div>
)}
{place.website && (
<div className="flex items-start gap-2">
<Globe size={18} className="mt-0.5 text-gray-500" />
<a
href={place.website}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary transition-colors"
>
Website
<ExternalLink size={14} className="inline ml-1" />
</a>
</div>
)}
</div>
</div>
{/* Gallery */}
{place.photos && place.photos.length > 1 && (
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">Photos</h2>
<div className="grid grid-cols-3 gap-2">
{place.photos.slice(1).map((photo, index) => (
<div key={index} className="aspect-square overflow-hidden rounded-md">
<img
src={photo}
alt={`${place.name} photo ${index + 2}`}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
))}
</div>
</div>
)}
<div className="flex justify-end gap-2">
{place.website && (
<Button
variant="outline"
className="flex items-center gap-1"
asChild
>
<a
href={place.website}
target="_blank"
rel="noopener noreferrer"
>
Visit Website
<ExternalLink size={14} />
</a>
</Button>
)}
<Button
variant="default"
className="flex items-center gap-1"
asChild
>
<a
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
`${place.name} ${place.address}`
)}`}
target="_blank"
rel="noopener noreferrer"
>
View on Google Maps
<MapPin size={14} />
</a>
</Button>
</div>
</div>
</motion.div>
</motion.div>
);
}

View File

@ -0,0 +1,118 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Utensils, Coffee, GlassWater, Building2, ShoppingBag, Landmark, Trees as Tree, Dumbbell, MapPin, SlidersHorizontal } from 'lucide-react';
import { Button } from './UI/Button';
import { Badge } from './UI/Badge';
import { PlaceCategory } from '../types';
interface CategoryOption {
value: PlaceCategory;
label: string;
icon: React.ReactNode;
}
const categories: CategoryOption[] = [
{ value: 'restaurant', label: 'Restaurants', icon: <Utensils size={16} /> },
{ value: 'cafe', label: 'Cafes', icon: <Coffee size={16} /> },
{ value: 'bar', label: 'Bars', icon: <GlassWater size={16} /> },
{ value: 'hotel', label: 'Hotels', icon: <Building2 size={16} /> },
{ value: 'store', label: 'Stores', icon: <ShoppingBag size={16} /> },
{ value: 'attraction', label: 'Attractions', icon: <Landmark size={16} /> },
{ value: 'park', label: 'Parks', icon: <Tree size={16} /> },
{ value: 'gym', label: 'Gyms', icon: <Dumbbell size={16} /> },
{ value: 'other', label: 'Other', icon: <MapPin size={16} /> },
];
interface PlaceFiltersProps {
selectedCategories: PlaceCategory[];
onCategoryChange: (categories: PlaceCategory[]) => void;
searchRadius: number;
onRadiusChange: (radius: number) => void;
}
export function PlaceFilters({
selectedCategories,
onCategoryChange,
searchRadius,
onRadiusChange,
}: PlaceFiltersProps) {
const toggleCategory = (category: PlaceCategory) => {
if (selectedCategories.includes(category)) {
onCategoryChange(selectedCategories.filter(c => c !== category));
} else {
onCategoryChange([...selectedCategories, category]);
}
};
const clearCategories = () => {
onCategoryChange([]);
};
const handleRadiusChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onRadiusChange(parseInt(event.target.value, 10));
};
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-4"
>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center gap-2">
<SlidersHorizontal size={18} className="text-primary" />
Filters
</h2>
{selectedCategories.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearCategories}
className="text-xs text-muted-foreground"
>
Clear All
</Button>
)}
</div>
<div>
<label className="block text-sm font-medium mb-2">
Search Radius: {searchRadius / 1000} km
</label>
<input
type="range"
min="500"
max="5000"
step="500"
value={searchRadius}
onChange={handleRadiusChange}
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground mt-1">
<span>0.5 km</span>
<span>5 km</span>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">Categories</label>
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<Badge
key={category.value}
variant={selectedCategories.includes(category.value) ? 'default' : 'outline'}
className="cursor-pointer flex items-center gap-1 py-1 px-2"
onClick={() => toggleCategory(category.value)}
>
{category.icon}
{category.label}
</Badge>
))}
</div>
</div>
</div>
</motion.div>
);
}

View File

@ -0,0 +1,103 @@
import React, { useState, useCallback } from 'react';
import Map, { Marker, NavigationControl, Popup } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { LocationData, Place } from '../types';
import { formatRating } from '../lib/utils';
// Mapbox public token - this is safe to expose in client-side code
const MAPBOX_TOKEN = 'pk.eyJ1IjoiZXhhbXBsZS11c2VyIiwiYSI6ImNrd2VzZ3l2NDJnMXMyb28xYm9yamVsZWsifQ.UZIGHXxFoS6eR_zNGCG5yw';
interface PlaceMapProps {
userLocation: LocationData | null;
places: Place[];
onMarkerClick: (place: Place) => void;
selectedPlace: Place | null;
}
export function PlaceMap({
userLocation,
places,
onMarkerClick,
selectedPlace
}: PlaceMapProps) {
const [viewport, setViewport] = useState({
latitude: userLocation?.latitude || 40.7128,
longitude: userLocation?.longitude || -74.0060,
zoom: 13
});
const handleMarkerClick = useCallback((place: Place) => {
onMarkerClick(place);
}, [onMarkerClick]);
return (
<div className="h-full w-full rounded-lg overflow-hidden shadow-lg">
<Map
{...viewport}
mapboxAccessToken={MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
onMove={evt => setViewport(evt.viewState)}
>
{/* User location marker */}
{userLocation && (
<Marker
longitude={userLocation.longitude}
latitude={userLocation.latitude}
anchor="center"
>
<div className="relative">
<div className="absolute -translate-x-1/2 -translate-y-1/2 h-6 w-6 bg-blue-500 rounded-full border-2 border-white shadow-md flex items-center justify-center">
<div className="h-2 w-2 bg-white rounded-full"></div>
</div>
<div className="absolute -translate-x-1/2 -translate-y-1/2 h-12 w-12 bg-blue-500/30 rounded-full animate-ping-slow"></div>
</div>
</Marker>
)}
{/* Place markers */}
{places.map(place => (
<Marker
key={place.id}
longitude={place.coordinates.longitude}
latitude={place.coordinates.latitude}
anchor="bottom"
onClick={() => handleMarkerClick(place)}
>
<div className={`h-8 w-8 flex items-center justify-center rounded-full cursor-pointer transform transition-all duration-200 ${
selectedPlace?.id === place.id
? 'bg-primary text-white scale-125'
: 'bg-white text-gray-700 hover:scale-110'
} shadow-md border border-gray-300`}>
<span className="text-xs font-bold">{place.rating.toFixed(1)}</span>
</div>
</Marker>
))}
{/* Selected place popup */}
{selectedPlace && (
<Popup
anchor="bottom"
longitude={selectedPlace.coordinates.longitude}
latitude={selectedPlace.coordinates.latitude}
onClose={() => onMarkerClick(null as unknown as Place)}
closeButton={true}
closeOnClick={false}
className="z-10"
>
<div className="p-2 max-w-xs">
<h3 className="font-semibold text-sm">{selectedPlace.name}</h3>
<div className="text-xs flex items-center gap-1 text-amber-500 mt-1">
<span className="font-medium">{formatRating(selectedPlace.rating)}</span>
</div>
<p className="text-xs text-gray-600 mt-1 line-clamp-2">
{selectedPlace.address}
</p>
</div>
</Popup>
)}
<NavigationControl position="bottom-right" />
</Map>
</div>
);
}

View File

@ -0,0 +1,189 @@
import React, { useState } from 'react';
import { AnimatePresence } from 'framer-motion';
import { useLocation } from '../hooks/useLocation';
import { usePlaces } from '../hooks/usePlaces';
import { Place } from '../types';
import { exportToExcel } from '../lib/utils';
import { ArrowLeft, Download } from 'lucide-react';
import { PlaceFilters } from './PlaceFilters';
import { PlaceCard } from './PlaceCard';
import { PlaceMap } from './PlaceMap';
import { PlaceDetail } from './PlaceDetail';
import { EmptyState } from './EmptyState';
import { Spinner } from './UI/Spinner';
import { Button } from './UI/Button';
interface PlacesExplorerProps {
onBack: () => void;
}
export function PlacesExplorer({ onBack }: PlacesExplorerProps) {
const {
location,
error: locationError,
loading: locationLoading,
refreshLocation
} = useLocation();
const {
places,
loading: placesLoading,
error: placesError,
fetchPlaces,
selectedCategories,
setSelectedCategories,
searchRadius,
setSearchRadius,
} = usePlaces();
const [selectedPlace, setSelectedPlace] = useState<Place | null>(null);
const [showMap, setShowMap] = useState<boolean>(false);
const handleExportData = () => {
if (places.length) {
exportToExcel(places);
}
};
const renderContent = () => {
if (locationError) {
return (
<EmptyState
type="location-error"
message={locationError}
onAction={refreshLocation}
actionLabel="Retry"
/>
);
}
if (locationLoading) {
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<Spinner size="lg" className="mb-4" />
<p className="text-muted-foreground">Getting your location...</p>
</div>
);
}
if (placesLoading) {
return (
<div className="flex flex-col items-center justify-center h-full p-8">
<Spinner size="lg" className="mb-4" />
<p className="text-muted-foreground">Finding places near you...</p>
</div>
);
}
if (placesError) {
return (
<EmptyState
type="no-results"
message={placesError}
onAction={() => fetchPlaces({ location: location!, radius: searchRadius })}
actionLabel="Retry"
/>
);
}
if (places.length === 0) {
return (
<EmptyState
type="no-results"
message="Try adjusting your filters or increasing the search radius to find more places."
onAction={() => {
setSelectedCategories([]);
setSearchRadius(2000);
}}
actionLabel="Reset Filters"
/>
);
}
return (
<div className="flex-1 w-full">
{showMap ? (
<div className="h-[calc(100vh-200px)] w-full rounded-lg overflow-hidden">
<PlaceMap
userLocation={location}
places={places}
onMarkerClick={setSelectedPlace}
selectedPlace={selectedPlace}
/>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{places.map((place, index) => (
<PlaceCard
key={place.id}
place={place}
index={index}
onViewDetails={setSelectedPlace}
/>
))}
</div>
)}
</div>
);
};
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<Button
variant="ghost"
onClick={onBack}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Home
</Button>
{places.length > 0 && (
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={handleExportData}
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
Export to Excel
</Button>
<Button
variant="ghost"
onClick={() => setShowMap(!showMap)}
>
{showMap ? 'Show List' : 'Show Map'}
</Button>
</div>
)}
</div>
<div className="flex flex-col lg:flex-row gap-4">
<div className="w-full lg:w-64 flex-shrink-0">
<PlaceFilters
selectedCategories={selectedCategories}
onCategoryChange={setSelectedCategories}
searchRadius={searchRadius}
onRadiusChange={setSearchRadius}
/>
</div>
<div className="flex-1">
{renderContent()}
</div>
</div>
<AnimatePresence>
{selectedPlace && (
<PlaceDetail
place={selectedPlace}
onClose={() => setSelectedPlace(null)}
/>
)}
</AnimatePresence>
</div>
);
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
outline: 'text-foreground border border-input hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/80',
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100',
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100',
info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,51 @@
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm',
destructive: 'bg-red-500 text-white hover:bg-red-600 shadow-sm',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-12 rounded-md px-6',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@ -0,0 +1,111 @@
import React from 'react';
import { cn } from '../../lib/utils';
import { motion } from 'framer-motion';
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
variant?: 'default' | 'outline' | 'ghost';
index?: number;
}
export function Card({
children,
className,
variant = 'default',
index = 0,
...props
}: CardProps) {
const variantClasses = {
default: 'bg-card text-card-foreground shadow-md',
outline: 'border border-border bg-transparent',
ghost: 'bg-transparent hover:bg-muted/50',
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
className={cn(
'rounded-lg overflow-hidden',
variantClasses[variant],
className
)}
{...props}
>
{children}
</motion.div>
);
}
interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function CardHeader({ children, className, ...props }: CardHeaderProps) {
return (
<div
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
>
{children}
</div>
);
}
interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
children: React.ReactNode;
}
export function CardTitle({ children, className, ...props }: CardTitleProps) {
return (
<h3
className={cn('font-semibold text-lg tracking-tight', className)}
{...props}
>
{children}
</h3>
);
}
interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
children: React.ReactNode;
}
export function CardDescription({ children, className, ...props }: CardDescriptionProps) {
return (
<p
className={cn('text-sm text-muted-foreground', className)}
{...props}
>
{children}
</p>
);
}
interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function CardContent({ children, className, ...props }: CardContentProps) {
return (
<div className={cn('p-6 pt-0', className)} {...props}>
{children}
</div>
);
}
interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function CardFooter({ children, className, ...props }: CardFooterProps) {
return (
<div
className={cn('flex items-center p-6 pt-0', className)}
{...props}
>
{children}
</div>
);
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { cn } from '../../lib/utils';
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
size?: 'sm' | 'md' | 'lg';
variant?: 'default' | 'light';
}
const sizeClasses = {
sm: 'h-4 w-4 border-2',
md: 'h-6 w-6 border-2',
lg: 'h-8 w-8 border-3',
};
const variantClasses = {
default: 'border-primary/30 border-t-primary',
light: 'border-white/30 border-t-white',
};
export function Spinner({
size = 'md',
variant = 'default',
className,
...props
}: SpinnerProps) {
return (
<div
className={cn(
'animate-spin rounded-full',
sizeClasses[size],
variantClasses[variant],
className
)}
{...props}
/>
);
}

View File

@ -0,0 +1,66 @@
import { motion } from 'framer-motion';
import { cn } from '../../lib/utils';
interface ToggleButtonProps {
isToggled: boolean;
onToggle: () => void;
label?: any;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const sizeClasses = {
sm: {
track: 'h-5 w-10',
thumb: 'h-4 w-4',
},
md: {
track: 'h-6 w-12',
thumb: 'h-5 w-5',
},
lg: {
track: 'h-7 w-14',
thumb: 'h-6 w-6',
},
};
export function ToggleButton({
isToggled,
onToggle,
label,
size = 'md',
className,
}: ToggleButtonProps) {
const { track, thumb } = sizeClasses[size];
return (
<div className={cn('flex items-center gap-2', className)}>
{label && (
<label className="text-sm font-medium cursor-pointer">{label}</label>
)}
<button
type="button"
role="switch"
aria-checked={isToggled}
onClick={onToggle}
className={cn(
'relative rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
track,
isToggled
? 'bg-primary'
: 'bg-gray-300 dark:bg-gray-600'
)}
>
<motion.span
layout
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className={cn(
'block rounded-full bg-white shadow-md',
thumb,
isToggled ? 'translate-x-full' : ''
)}
/>
</button>
</div>
);
}

View File

@ -0,0 +1,60 @@
import { useState, useEffect, useCallback } from 'react';
import { LocationData } from '../types';
import { getCurrentPosition } from '../lib/utils';
interface UseLocationResult {
location: LocationData | null;
error: string | null;
loading: boolean;
refreshLocation: () => Promise<void>;
}
export function useLocation(): UseLocationResult {
const [location, setLocation] = useState<LocationData | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const refreshLocation = useCallback(async () => {
setLoading(true);
setError(null);
try {
const position = await getCurrentPosition();
setLocation({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
});
} catch (err) {
console.error('Error getting location:', err);
// Handle specific error types
if (err instanceof GeolocationPositionError) {
switch (err.code) {
case err.PERMISSION_DENIED:
setError('Location access was denied. Please enable location services.');
break;
case err.POSITION_UNAVAILABLE:
setError('Location information is unavailable. Please try again later.');
break;
case err.TIMEOUT:
setError('The request to get location timed out. Please try again.');
break;
default:
setError('An unknown error occurred while getting location.');
}
} else {
setError('Failed to get your location. Please try again.');
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
refreshLocation();
}, [refreshLocation]);
return { location, error, loading, refreshLocation };
}

View File

@ -0,0 +1,58 @@
import { useState, useCallback } from 'react';
import { AIQueryParams, Place, PlaceCategory } from '../types';
import { fetchNearbyPlaces } from '../services/mockAiService';
interface UsePlacesResult {
places: Place[];
loading: boolean;
error: string | null;
fetchPlaces: (params: AIQueryParams) => Promise<void>;
selectedCategories: PlaceCategory[];
setSelectedCategories: (categories: PlaceCategory[]) => void;
searchRadius: number;
setSearchRadius: (radius: number) => void;
}
export function usePlaces(): UsePlacesResult {
const [places, setPlaces] = useState<Place[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [selectedCategories, setSelectedCategories] = useState<PlaceCategory[]>([]);
const [searchRadius, setSearchRadius] = useState<number>(1000); // Default 1km
const fetchPlaces = useCallback(async (params: AIQueryParams) => {
setLoading(true);
setError(null);
try {
// Apply current filter states to the params
const queryParams: AIQueryParams = {
...params,
radius: searchRadius,
};
if (selectedCategories.length > 0) {
queryParams.categories = selectedCategories;
}
const data = await fetchNearbyPlaces(queryParams);
setPlaces(data);
} catch (err) {
console.error('Error fetching places:', err);
setError('Failed to fetch nearby places. Please try again.');
} finally {
setLoading(false);
}
}, [searchRadius, selectedCategories]);
return {
places,
loading,
error,
fetchPlaces,
selectedCategories,
setSelectedCategories,
searchRadius,
setSearchRadius,
};
}

View File

@ -0,0 +1,40 @@
import { useState, useEffect } from 'react';
import { type ThemeMode } from '../types';
export function useTheme() {
// Check for user's preference in localStorage or use system preference
const getInitialTheme = (): ThemeMode => {
const savedTheme = localStorage.getItem('theme') as ThemeMode | null;
if (savedTheme && (savedTheme === 'dark' || savedTheme === 'light')) {
return savedTheme;
}
// Check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
const [theme, setTheme] = useState<ThemeMode>(getInitialTheme);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// Update localStorage and apply theme when it changes
useEffect(() => {
localStorage.setItem('theme', theme);
// Apply theme to document
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
return { theme, toggleTheme };
}

View File

@ -0,0 +1,76 @@
@import "tailwindcss";
@layer base {
:root {
--background: 0 0% 100%;
--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%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 170 80% 30%;
--accent-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--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: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--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%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 170 80% 40%;
--accent-foreground: 222.2 47.4% 11.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--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: 224.3 76.3% 48%;
}
}
@layer base {
/* * {
@apply border-[hsl(var(--border))];
} */
body {
@apply bg-[hsl(var(--background))] text-[hsl(var(--foreground))];
font-feature-settings: "rlig" 1, "calt" 1;
}
}

View File

@ -0,0 +1,126 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// import { utils, writeFile } from 'xlsx';
import type { Place, PlaceCategory } from '../types';
/**
* Combines multiple class names with Tailwind CSS classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Format a distance in meters to a readable format
*/
export function formatDistance(meters: number): string {
if (meters < 1000) {
return `${Math.round(meters)}m`;
}
return `${(meters / 1000).toFixed(1)}km`;
}
/**
* Format rating to display stars (e.g., "4.5/5")
*/
export function formatRating(rating: number): string {
return `${rating.toFixed(1)}/5`;
}
/**
* Format price level to display dollar signs
*/
export function formatPriceLevel(level?: number): string {
if (!level) return 'N/A';
return '$'.repeat(level);
}
/**
* Checks if geolocation is supported by the browser
*/
export function isGeolocationSupported(): boolean {
return 'geolocation' in navigator;
}
/**
* Get current position as a promise
*/
export function getCurrentPosition(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => {
if (!isGeolocationSupported()) {
reject(new Error('Geolocation is not supported by your browser'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position),
(error) => reject(error),
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
});
}
/**
* Export places to an Excel file
*/
// export function exportToExcel(places: Place[], filename = 'nearby-places.xlsx'): void {
// // Create a new workbook
// const workbook = utils.book_new();
// // Format the data for Excel
// const data = places.map((place) => ({
// Name: place.name,
// Category: place.category,
// Address: place.address,
// Rating: place.rating,
// Distance: formatDistance(place.distance),
// 'Price Level': formatPriceLevel(place.priceLevel),
// Website: place.website || 'N/A',
// Phone: place.phoneNumber || 'N/A',
// Latitude: place.coordinates.latitude,
// Longitude: place.coordinates.longitude,
// }));
// // Convert the data to a worksheet
// const worksheet = utils.json_to_sheet(data);
// // Add the worksheet to the workbook
// utils.book_append_sheet(workbook, worksheet, 'Nearby Places');
// // Generate the Excel file
// writeFile(workbook, filename);
// }
/**
* Simple debounce function
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return function(...args: Parameters<T>): void {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
/**
* Get a category icon name for Lucide icons
*/
export function getCategoryIconName(category: PlaceCategory): string {
switch (category) {
case 'restaurant': return 'utensils';
case 'cafe': return 'coffee';
case 'bar': return 'glass';
case 'hotel': return 'bed';
case 'store': return 'shopping-bag';
case 'attraction': return 'landmark';
case 'park': return 'tree';
case 'gym': return 'dumbbell';
default: return 'map-pin';
}
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,77 @@
import { motion } from 'framer-motion';
import { Map, ArrowRight } from 'lucide-react';
import { Button } from '../components/UI/Button';
import { useNavigate } from 'react-router-dom';
export const Home = () => {
const navigate = useNavigate()
return (
<div className="max-w-4xl mx-auto py-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-12"
>
<div className="flex justify-center mb-6">
<Map className="h-16 w-16 text-[hsl(var(--primary))]" />
</div>
{/* <h1 className="text-4xl font-bold mb-4 bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--accent))] bg-clip-text text-transparent">
</h1> */}
<div className='text-4xl font-bold mb-4 text-white'>
Generate Leads with just a prompt
</div>
<p className="text-xl text-[hsl(var(--muted-foreground))] mb-8">
Discover and analyze nearby places using AI-powered location intelligence
</p>
<Button
size="lg"
className="text-lg px-8 py-6 rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] hover:bg-[hsl(var(--primary))]/90" onClick={()=>navigate("/agent")}
>
Start Exploring
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</motion.div>
<div className="grid md:grid-cols-3 gap-8 mt-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
>
<h3 className="text-lg font-semibold mb-2">Smart Discovery</h3>
<p className="text-[hsl(var(--muted-foreground))]">
AI agent intelligently identifies and categorizes places around your location.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
>
<h3 className="text-lg font-semibold mb-2">Detailed Analysis</h3>
<p className="text-[hsl(var(--muted-foreground))]">
Get comprehensive information about each location, including ratings, hours, and more.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
>
<h3 className="text-lg font-semibold mb-2">Export & Share</h3>
<p className="text-[hsl(var(--muted-foreground))]">
Download your discoveries in Excel format for further analysis and sharing.
</p>
</motion.div>
</div>
</div>
);
}

View File

@ -0,0 +1,165 @@
import axios from "axios"
import { motion } from "framer-motion";
import { useState } from "react"
interface ApiResponse {
success: boolean;
message: string;
filename?: string;
file_id?: string;
places_count?: number;
}
export const Lead = () => {
const [prompt, setPrompt] = useState("")
const [aiResponse, setAIResponse] = useState<ApiResponse | null>(null)
const [loading, setLoading] = useState(false)
const [downloading, setDownloading] = useState(false)
const handleClick = async () => {
if (!prompt.trim()) {
alert("Please enter a prompt");
return;
}
setLoading(true);
setAIResponse(null);
try {
const response = await axios.post<ApiResponse>("http://localhost:3000/v1/generate", {
user_query: prompt
});
setAIResponse(response.data);
} catch (error) {
console.error("Error:", error);
setAIResponse({
success: false,
message: "Failed to connect to the server. Please try again."
});
} finally {
setLoading(false);
}
}
const handleDownload = async (fileId: string, filename: string) => {
setDownloading(true);
try {
const response = await axios.get(`http://localhost:3000/v1/download/${fileId}`, {
responseType: 'blob',
});
// Create blob link to download
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
// Append to html link element page
document.body.appendChild(link);
// Start download
link.click();
// Clean up and remove the link
link.parentNode?.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Download error:", error);
alert("Failed to download file. Please try again.");
} finally {
setDownloading(false);
}
}
return (
<div className="ml-5 mt-5">
<motion.div className="flex justify-center text-3xl mb-6 font-bold" initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}>
Begin your lead generation now
</motion.div>
<motion.div className="flex justify-center mb-6" initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}>
<input
placeholder="Enter your prompt here..."
className="outline-none rounded mx-2 focus:border px-2 w-1/4 py-2 border border-gray-300"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
disabled={loading}
/>
<button
className={`px-3 py-1 rounded-lg text-lg text-white ${
loading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-700'
}`}
onClick={handleClick}
disabled={loading}
>
{loading ? "Generating..." : "Generate"}
</button>
</motion.div>
{/* Response Display */}
{aiResponse && (
<div className="max-w-4xl mx-auto mt-6">
<div className={`p-4 rounded-lg ${
aiResponse.success ? 'bg-green-100 border-green-500' : 'bg-red-100 border-red-500'
} border`}>
<h3 className={`text-lg font-semibold mb-2 ${
aiResponse.success ? 'text-green-800' : 'text-red-800'
}`}>
{aiResponse.success ? 'Success!' : 'Error'}
</h3>
<p className={aiResponse.success ? 'text-green-700' : 'text-red-700'}>
{aiResponse.message}
</p>
{aiResponse.success && aiResponse.filename && (
<div className="mt-4">
<div className="text-green-700 mb-3">
<p><strong>File:</strong> {aiResponse.filename}</p>
<p><strong>Places found:</strong> {aiResponse.places_count}</p>
</div>
{aiResponse.file_id && (
<button
onClick={() => handleDownload(aiResponse.file_id!, aiResponse.filename!)}
disabled={downloading}
className={`px-4 py-2 rounded-lg text-white font-medium ${
downloading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700'
}`}
>
{downloading ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Downloading...
</span>
) : (
<span className="flex items-center">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Download Excel File
</span>
)}
</button>
)}
</div>
)}
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,192 @@
import { AIQueryParams, Place, PlaceCategory } from '../types';
// Mock data to simulate AI response
const mockPlaces: Place[] = [
{
id: '1',
name: 'Central Park Cafe',
category: 'cafe',
address: '123 Park Avenue, New York, NY',
description: 'A cozy cafe in the heart of Central Park offering organic coffee and pastries.',
rating: 4.7,
distance: 350,
coordinates: { latitude: 40.7812, longitude: -73.9665 },
openHours: '7:00 AM - 7:00 PM',
priceLevel: 2,
website: 'https://example.com/centralparkcafe',
phoneNumber: '+1 212-555-0123',
photos: [
'https://images.pexels.com/photos/1855214/pexels-photo-1855214.jpeg',
'https://images.pexels.com/photos/302899/pexels-photo-302899.jpeg'
]
},
{
id: '2',
name: 'Urban Bistro',
category: 'restaurant',
address: '456 Fifth Avenue, New York, NY',
description: 'Modern bistro serving contemporary American cuisine with seasonal ingredients.',
rating: 4.5,
distance: 550,
coordinates: { latitude: 40.7549, longitude: -73.9840 },
openHours: '11:00 AM - 10:00 PM',
priceLevel: 3,
website: 'https://example.com/urbanbistro',
phoneNumber: '+1 212-555-0124',
photos: [
'https://images.pexels.com/photos/67468/pexels-photo-67468.jpeg',
'https://images.pexels.com/photos/262978/pexels-photo-262978.jpeg'
]
},
{
id: '3',
name: 'Skyline Hotel',
category: 'hotel',
address: '789 Broadway, New York, NY',
description: 'Luxury boutique hotel with stunning views of the Manhattan skyline.',
rating: 4.8,
distance: 750,
coordinates: { latitude: 40.7589, longitude: -73.9851 },
openHours: 'Open 24 hours',
priceLevel: 4,
website: 'https://example.com/skylinehotel',
phoneNumber: '+1 212-555-0125',
photos: [
'https://images.pexels.com/photos/261102/pexels-photo-261102.jpeg',
'https://images.pexels.com/photos/271624/pexels-photo-271624.jpeg'
]
},
{
id: '4',
name: 'City Park',
category: 'park',
address: '321 Park Lane, New York, NY',
description: 'Expansive urban park with walking trails, picnic areas, and playgrounds.',
rating: 4.6,
distance: 450,
coordinates: { latitude: 40.7729, longitude: -73.9732 },
website: 'https://example.com/citypark',
phoneNumber: '+1 212-555-0126',
photos: [
'https://images.pexels.com/photos/1108701/pexels-photo-1108701.jpeg',
'https://images.pexels.com/photos/863963/pexels-photo-863963.jpeg'
]
},
{
id: '5',
name: 'Fashion Outlet',
category: 'store',
address: '654 Shopping Avenue, New York, NY',
description: 'Premium fashion outlet featuring designer brands at discounted prices.',
rating: 4.2,
distance: 900,
coordinates: { latitude: 40.7539, longitude: -73.9910 },
openHours: '10:00 AM - 9:00 PM',
priceLevel: 3,
website: 'https://example.com/fashionoutlet',
phoneNumber: '+1 212-555-0127',
photos: [
'https://images.pexels.com/photos/331990/pexels-photo-331990.jpeg',
'https://images.pexels.com/photos/1488463/pexels-photo-1488463.jpeg'
]
},
{
id: '6',
name: 'Downtown Fitness Center',
category: 'gym',
address: '987 Health Street, New York, NY',
description: 'Full-service fitness center with state-of-the-art equipment and classes.',
rating: 4.4,
distance: 650,
coordinates: { latitude: 40.7509, longitude: -73.9780 },
openHours: '5:00 AM - 11:00 PM',
priceLevel: 2,
website: 'https://example.com/downtownfitness',
phoneNumber: '+1 212-555-0128',
photos: [
'https://images.pexels.com/photos/1954524/pexels-photo-1954524.jpeg',
'https://images.pexels.com/photos/4162487/pexels-photo-4162487.jpeg'
]
},
{
id: '7',
name: 'Historic Museum',
category: 'attraction',
address: '135 History Lane, New York, NY',
description: 'Museum showcasing the rich history and cultural heritage of the city.',
rating: 4.7,
distance: 1200,
coordinates: { latitude: 40.7713, longitude: -73.9740 },
openHours: '9:00 AM - 5:00 PM',
priceLevel: 2,
website: 'https://example.com/historicmuseum',
phoneNumber: '+1 212-555-0129',
photos: [
'https://images.pexels.com/photos/2372978/pexels-photo-2372978.jpeg',
'https://images.pexels.com/photos/2570063/pexels-photo-2570063.jpeg'
]
},
{
id: '8',
name: 'Riverside Bar & Lounge',
category: 'bar',
address: '246 River Street, New York, NY',
description: 'Upscale bar with craft cocktails and panoramic river views.',
rating: 4.3,
distance: 1400,
coordinates: { latitude: 40.7623, longitude: -73.9836 },
openHours: '4:00 PM - 2:00 AM',
priceLevel: 3,
website: 'https://example.com/riversidebar',
phoneNumber: '+1 212-555-0130',
photos: [
'https://images.pexels.com/photos/274192/pexels-photo-274192.jpeg',
'https://images.pexels.com/photos/1267696/pexels-photo-1267696.jpeg'
]
}
];
// Simulates an AI call with random delay (300-1500ms)
function simulateAiProcessing<T>(data: T): Promise<T> {
const delay = Math.floor(Math.random() * 1200) + 300;
return new Promise(resolve => setTimeout(() => resolve(data), delay));
}
// Filter places based on query parameters
function filterPlaces(params: AIQueryParams): Place[] {
let filtered = [...mockPlaces];
// Adjust distances based on user location
filtered = filtered.map(place => ({
...place,
distance: Math.floor(Math.random() * params.radius * 0.8) + 100 // Random distance within radius
}));
// Filter by category if specified
if (params.categories && params.categories.length > 0) {
filtered = filtered.filter(place =>
params.categories?.includes(place.category as PlaceCategory)
);
}
// Filter by radius
filtered = filtered.filter(place => place.distance <= params.radius);
// Apply limit if specified
if (params.limit && params.limit > 0 && params.limit < filtered.length) {
filtered = filtered.slice(0, params.limit);
}
// Sort by distance
return filtered.sort((a, b) => a.distance - b.distance);
}
export async function fetchNearbyPlaces(params: AIQueryParams): Promise<Place[]> {
console.log('Fetching places with params:', params);
// Filter places based on query parameters
const filteredPlaces = filterPlaces(params);
// Simulate AI processing delay
return simulateAiProcessing(filteredPlaces);
}

View File

@ -0,0 +1,50 @@
export interface Place {
id: string;
name: string;
category: PlaceCategory;
address: string;
description: string;
rating: number;
distance: number; // in meters
coordinates: {
latitude: number;
longitude: number;
};
openHours?: string;
photos?: string[];
priceLevel?: number; // 1-4, where 1 is least expensive
website?: string;
phoneNumber?: string;
}
export type PlaceCategory =
| 'restaurant'
| 'cafe'
| 'bar'
| 'hotel'
| 'store'
| 'attraction'
| 'park'
| 'gym'
| 'other';
export interface LocationData {
latitude: number;
longitude: number;
accuracy?: number;
}
export interface AIQueryParams {
location: LocationData;
radius: number; // in meters
categories?: PlaceCategory[];
limit?: number;
}
export type ThemeMode = 'light' | 'dark';
export type FilterOption = {
id: string;
label: string;
value: string;
};

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(),
tailwindcss()
],
})

8718
LeadGen/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
LeadGen/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "LeadGen",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
},
"devDependencies": {
"prettier": "^3.5.3",
"turbo": "^2.5.3",
"typescript": "5.8.2"
},
"engines": {
"node": ">=18"
},
"packageManager": "npm@11.3.0",
"workspaces": [
"apps/*",
"packages/*"
]
}

View File

@ -0,0 +1,3 @@
# `@turbo/eslint-config`
Collection of internal eslint configurations.

View File

@ -0,0 +1,32 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
import onlyWarn from "eslint-plugin-only-warn";
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ["dist/**"],
},
];

View File

@ -0,0 +1,49 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import pluginNext from "@next/eslint-plugin-next";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use Next.js.
*
* @type {import("eslint").Linter.Config[]}
* */
export const nextJsConfig = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
},
},
},
{
plugins: {
"@next/next": pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
...pluginNext.configs["core-web-vitals"].rules,
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

View File

@ -0,0 +1,24 @@
{
"name": "@repo/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./base": "./base.js",
"./next-js": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@next/eslint-plugin-next": "^15.3.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-turbo": "^2.5.0",
"globals": "^16.1.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.32.0"
}
}

View File

@ -0,0 +1,39 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use React.
*
* @type {import("eslint").Linter.Config[]} */
export const config = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

View File

@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
}
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
}
}

View File

@ -0,0 +1,9 @@
{
"name": "@repo/typescript-config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx"
}
}

View File

@ -0,0 +1,4 @@
import { config } from "@repo/eslint-config/react-internal";
/** @type {import("eslint").Linter.Config} */
export default config;

View File

@ -0,0 +1,27 @@
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./*": "./src/*.tsx"
},
"scripts": {
"lint": "eslint . --max-warnings 0",
"generate:component": "turbo gen react-component",
"check-types": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@turbo/gen": "^2.5.0",
"@types/node": "^22.15.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"eslint": "^9.27.0",
"typescript": "5.8.2"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}

View File

@ -0,0 +1,20 @@
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
className?: string;
appName: string;
}
export const Button = ({ children, className, appName }: ButtonProps) => {
return (
<button
className={className}
onClick={() => alert(`Hello from your ${appName} app!`)}
>
{children}
</button>
);
};

View File

@ -0,0 +1,27 @@
import { type JSX } from "react";
export function Card({
className,
title,
children,
href,
}: {
className?: string;
title: string;
children: React.ReactNode;
href: string;
}): JSX.Element {
return (
<a
className={className}
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
rel="noopener noreferrer"
target="_blank"
>
<h2>
{title} <span>-&gt;</span>
</h2>
<p>{children}</p>
</a>
);
}

View File

@ -0,0 +1,11 @@
import { type JSX } from "react";
export function Code({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}): JSX.Element {
return <code className={className}>{children}</code>;
}

View File

@ -0,0 +1,8 @@
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,30 @@
import type { PlopTypes } from "@turbo/gen";
// Learn more about Turborepo Generators at https://turborepo.com/docs/guides/generating-code
export default function generator(plop: PlopTypes.NodePlopAPI): void {
// A simple generator to add a new React component to the internal UI library
plop.setGenerator("react-component", {
description: "Adds a new react component",
prompts: [
{
type: "input",
name: "name",
message: "What is the name of the component?",
},
],
actions: [
{
type: "add",
path: "src/{{kebabCase name}}.tsx",
templateFile: "templates/component.hbs",
},
{
type: "append",
path: "package.json",
pattern: /"exports": {(?<insertion>)/g,
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
},
],
});
}

View File

@ -0,0 +1,8 @@
export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<h1>{{ pascalCase name }} Component</h1>
{children}
</div>
);
};

21
LeadGen/turbo.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}

2
backend/.env Normal file
View File

@ -0,0 +1,2 @@
FOURSQUARE_SERVICE_TOKEN="fsq3DFXKFtQkVsgKt5Bul/u1nhzEpVzxpyixL4y9ahSm5cc="
GEMINI_API_KEY="AIzaSyAGXspPFiKkmicShhkyuGnUvqxuaWbBtKE"

2
backend/.env.example Normal file
View File

@ -0,0 +1,2 @@
FOURSQUARE_SERVICE_TOKEN=""
GEMINI_API_KEY=""

6
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
crew_agent
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

View File

@ -0,0 +1,18 @@
from .agent import (
task_picker,
location_finder,
place_researcher,
data_processor,
# recommendations_expert,
# data_analyzer
)
# Export all agent instances
__all__ = [
'task_picker',
'location_finder',
'place_researcher',
# 'recommendations_expert',
# 'data_analyzer',
'data_processor'
]

Binary file not shown.

Binary file not shown.

102
backend/agents/agent.py Normal file
View File

@ -0,0 +1,102 @@
from crewai import Agent, LLM
from tools import (
GetUserLocationTool,
SearchNearPointTool,
PlaceSnapTool,
GetPlaceDetailsTool,
SearchNearRegionTool
)
# llm = LLM(
# model="gemini/gemini-2.0-flash",
# )
# task_picker = Agent(
# role="Expert Decision Maker",
# goal="Decide what information the user wants based on the query and choose the appropriate task to be performed",
# backstory="As an expert decision maker, you are able to take accurate decisions on what task the user is asking you to perform (whether the user has asked to get information on a location based on their location, or if they have given the name of the location and want information based on that place)",
# llm=llm,
# verbose=True
# )
# location_finder = Agent(
# role="Location Specialist",
# goal="Determine the user's location or understand the location mentioned in the query",
# backstory="You are an expert in location services and geodata. You can accurately determine where users are located or understand locations they're interested in.",
# tools=[GetUserLocationTool()],
# llm=llm,
# verbose=True
# )
# place_researcher = Agent(
# role="Place Information Researcher",
# goal="Research and provide detailed information about places",
# backstory="You are an expert at finding and analyzing information about places. You can discover what's around a location, what's good to eat, interesting attractions, and other place-specific details.",
# tools=[SearchNearRegionTool(), SearchNearPointTool(), PlaceSnapTool(), GetPlaceDetailsTool()],
# llm=llm,
# verbose=True
# )
# data_processor = Agent(
# role="Data Processing Specialist",
# goal="Process place data and organize it for Excel export",
# backstory="You are specialized in processing location data and organizing it into structured formats suitable for export. You ensure all required fields are captured and formatted properly.",
# llm=llm,
# verbose=True
# )
# recommendations_expert = Agent(
# role="Local Recommendations Expert",
# goal="Provide personalized recommendations based on location and user preferences",
# backstory="You are an expert in providing customized recommendations for places to visit, eat, or explore based on user preferences and location data.",
# llm=llm,
# verbose=True
# )
# data_analyzer = Agent(
# role="Geospatial Data Analyzer",
# goal="Analyze and extract meaningful insights from location data",
# backstory="You are a specialist in analyzing location-based data to extract patterns, trends, and insights that can help users better understand their surroundings.",
# llm=llm,
# verbose=True
# )
llm = LLM(
model="gemini/gemini-2.0-flash",
)
task_picker = Agent(
role="Expert Decision Maker",
goal="Decide what information the user wants based on the query and choose the appropriate task to be performed",
backstory="As an expert decision maker, you are able to take accurate decisions on what task the user is asking you to perform (whether the user has asked to get information on a location based on their location, or if they have given the name of the location and want information based on that place)",
llm=llm,
verbose=True
)
# Create the agents
location_finder = Agent(
role="Location Specialist",
goal="Determine the user's location or understand the location mentioned in the query",
backstory="You are an expert in location services and geodata. You can accurately determine where users are located or understand locations they're interested in.",
tools=[GetUserLocationTool()],
llm=llm,
verbose=True
)
place_researcher = Agent(
role="Place Information Researcher",
goal="Research and provide detailed information about surrounding places",
backstory="You are an expert at finding and analyzing information about places. You can discover what's around a location and gather complete details about each place.",
tools=[SearchNearRegionTool(), SearchNearPointTool(), PlaceSnapTool(), GetPlaceDetailsTool()],
llm=llm,
verbose=True
)
data_processor = Agent(
role="Data Processing Specialist",
goal="Process place data and organize it for Excel export",
backstory="You are specialized in processing location data and organizing it into structured formats suitable for export. You ensure all required fields are captured and formatted properly. You will return a properly formatted JSON array of places with all the required details.",
llm=llm,
verbose=True
)

184
backend/app.py Normal file
View File

@ -0,0 +1,184 @@
from flask import Flask, render_template, request, jsonify, send_file
from flask_cors import CORS
import pandas as pd
import json
import re
from typing import List, Dict, Any
from tasks import search_surrounding_places
from crewai import Crew
from agents import *
import os
import tempfile
import traceback
app = Flask(__name__)
CORS(app)
class JsonExtractor:
"""Helper class to extract JSON from agent output text"""
@staticmethod
def extract_json(text):
"""Extract JSON data from text that might contain markdown code blocks"""
# Try to find JSON in code blocks
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
if json_match:
json_str = json_match.group(1)
else:
# If no code blocks, try to find anything that looks like a JSON array or object
json_match = re.search(r'(\[[\s\S]*\]|\{[\s\S]*\})', text)
if json_match:
json_str = json_match.group(1)
else:
return None
try:
return json.loads(json_str)
except json.JSONDecodeError:
try:
# Sometimes the JSON might have trailing commas which are invalid
# Try to clean it up by removing trailing commas
cleaned_json = re.sub(r',\s*([}\]])', r'\1', json_str)
return json.loads(cleaned_json)
except:
return None
def export_to_excel(place_data: List[Dict[str, str]], filename: str = "surrounding_places.xlsx"):
"""Export the place data to an Excel file"""
if not place_data:
print("No place data to export")
return False
try:
df = pd.DataFrame(place_data)
# Reorder columns for better readability
column_order = ["name", "description", "location", "distance", "tel", "email", "website"]
available_columns = [col for col in column_order if col in df.columns]
# Add any columns that might be in the data but not in our order list
available_columns.extend([col for col in df.columns if col not in column_order])
df = df[available_columns]
df.to_excel(filename, index=False)
print(f"Successfully exported {len(place_data)} places to {filename}")
return True
except Exception as e:
print(f"Error exporting to Excel: {e}")
return False
def main(prompt):
tasks = search_surrounding_places()
crew = Crew(
agents=[location_finder, place_researcher, data_processor],
tasks=tasks,
# verbose=2
)
result = crew.kickoff(inputs={"user_query": prompt})
place_data = JsonExtractor.extract_json(result.raw)
# If we got valid data from the agent's output, return it
if place_data and isinstance(place_data, list) and len(place_data) > 0:
# Ensure the data is standardized
standardized_data = []
for place in place_data:
standardized_place = {
"name": place.get("name", ""),
"description": place.get("description", ""),
"fsq_id": place.get("fsq_id", ""),
"distance": place.get("distance", ""),
"location": place.get("location", ""),
"tel": place.get("tel", ""),
"email": place.get("email", ""),
"website": place.get("website", "")
}
standardized_data.append(standardized_place)
else:
place_data = []
if place_data:
# Generate a unique filename with timestamp
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"surrounding_places_{timestamp}.xlsx"
# Create the file in a temp directory or app directory
filepath = os.path.join(tempfile.gettempdir(), filename)
success = export_to_excel(place_data, filepath)
if success:
return {
"success": True,
"message": f"Found {len(place_data)} places. File ready for download.",
"filename": filename,
"filepath": filepath,
"places_count": len(place_data)
}
else:
return {
"success": False,
"message": "Error occurred while exporting to Excel"
}
else:
return {
"success": False,
"message": "No places found or error occurred during search."
}
generated_files = {}
@app.route("/v1/generate", methods=["POST"])
def run_agent():
try:
data = request.get_json()
user_query = data.get('user_query', '')
# Remove JSON.stringify wrapping if present
if user_query.startswith('"') and user_query.endswith('"'):
user_query = json.loads(user_query)
print(f"Processing query: {user_query}")
# Call the main function
result = main(user_query)
# If successful, store the file path for download
if result.get('success') and 'filepath' in result:
file_id = result['filename'].replace('.xlsx', '')
generated_files[file_id] = result['filepath']
result['file_id'] = file_id
# Remove filepath from response (don't expose server paths)
del result['filepath']
return jsonify(result)
except Exception as e:
print(f"Error in run_agent: {str(e)}")
print(traceback.format_exc())
return jsonify({
"success": False,
"message": f"An error occurred: {str(e)}"
}), 500
@app.route("/v1/download/<file_id>", methods=["GET"])
def download_file(file_id):
try:
if file_id not in generated_files:
return jsonify({"error": "File not found"}), 404
filepath = generated_files[file_id]
if not os.path.exists(filepath):
return jsonify({"error": "File no longer exists"}), 404
return send_file(
filepath,
as_attachment=True,
download_name=f"{file_id}.xlsx",
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
except Exception as e:
print(f"Error in download_file: {str(e)}")
return jsonify({"error": "Download failed"}), 500
if __name__ == "__main__":
app.run(debug=True, port=3000, host="localhost")

27
backend/main.py Normal file
View File

@ -0,0 +1,27 @@
from crewai import Crew
from agents import task_picker, location_finder, place_researcher, data_processor, data_analyzer, recommendations_expert
from tasks import search_surrounding_places
def get_location_crew(user_query):
tasks = search_surrounding_places(user_query)
crew = Crew(
agents=[task_picker, location_finder, place_researcher, data_processor],
tasks=tasks,
# verbose=True
)
return crew
# Example of using the crew
def process_user_query(user_query):
crew = get_location_crew(user_query)
result = crew.kickoff()
return result
# Example usage
if __name__ == "__main__":
user_query = "Tell me about businesses near me"
result = process_user_query(user_query)
print("\nFinal Result:")
print(result)

Binary file not shown.

View File

@ -0,0 +1,5 @@
# /backend/tasks/__init__.py
from .task import search_surrounding_places
# Export task functions
__all__ = ['search_surrounding_places']

Binary file not shown.

Binary file not shown.

115
backend/tasks/task.py Normal file
View File

@ -0,0 +1,115 @@
from crewai import Task
from agents import *
from tools import *
from pydantic import BaseModel
from typing import Dict, List
class PlacesOutput(BaseModel):
results: List[Dict[str, str]]
# def create_tasks(user_query):
# task_analysis = Task(
# description=f"Analyze the user query: '{user_query}' and determine what type of location information they are seeking.",
# agent=agents.task_picker,
# expected_output="A clear determination of what the user is asking for: their current location info, recommendations near them, or info about a specific named location."
# )
# task_locate = Task(
# description=f"Based on the user query: '{user_query}', determine the location to focus on. Either get the user's current location or identify the location mentioned in the query.",
# agent=agents.location_finder,
# expected_output="Coordinates (latitude,longitude) or a location name that will be used for subsequent tasks."
# )
# task_gather_info = Task(
# description=f"Using the location from the previous task, gather relevant information about places based on the user query: '{user_query}'",
# agent=agents.place_researcher,
# expected_output="Detailed information about relevant places including details like descriptions, ratings, hours, etc. in a csv format",
# context=[task_analysis, task_locate],
# )
# # task_analyze_data = Task(
# # description=f"Analyze the gathered place information to extract patterns and insights relevant to the user query: '{user_query}'",
# # agent=agents.data_analyzer,
# # expected_output="Analysis of the place data highlighting key patterns, trends, or notable information.",
# # context=[task_gather_info]
# # )
# # task_provide_recommendations = Task(
# # description=f"Based on all the information gathered and analyzed, provide personalized recommendations that address the user query: '{user_query}'",
# # agent=agents.recommendations_expert,
# # expected_output="Personalized recommendations and a comprehensive response to the user's query about locations.",
# # context=[task_gather_info, task_analyze_data]
# # )
# return [task_analysis, task_locate, task_gather_info]
def search_surrounding_places() -> List[Dict[str, str]]:
"""Search for places surrounding the user's location and collect their details"""
task_analysis = Task(
description="Analyze the user query: {user_query} and determine what type of location information they are seeking. And also check if they have defined any radius or limits or queries",
agent=task_picker,
expected_output="A clear determination of what the user is asking for: their current location info, recommendations near them, or info about a specific named location."
)
task_locate = Task(
description="Based on the user query: {user_query}, determine the location to focus on. Either get the user's current location or identify the location mentioned in the query.",
agent=location_finder,
expected_output="Coordinates (latitude,longitude) or a location name that will be used for subsequent tasks."
)
# Task to get user's location
# get_location_task = Task(
# description="Determine the user's current location by getting their coordinates.",
# agent=location_finder,
# expected_output="Latitude and longitude coordinates of the user's current location.",
# context=[task_analysis]
# )
# Task to search for surrounding places
search_places_task = Task(
description="Using the location from the previous task, gather relevant information about places based on the user query: {user_query}",
agent=place_researcher,
expected_output="JSON data of surrounding places including fsq_id, name, category, and other available information.",
context=[task_locate]
)
# Task to get detailed information about each place
get_details_task = Task(
description="For each place found, gather detailed information including name, phone, email, address, and distance.",
agent=place_researcher,
expected_output="Complete JSON data with detailed information about each place.",
context=[search_places_task]
)
# Task to process the data for Excel export
process_data_task = Task(
description="""Process the gathered data and prepare it for Excel export.
You MUST return a valid JSON array containing objects with these fields:
- name: Name of the business/place
- description: A general description of the FSQ Place. Typically provided by the owner/claimant of the FSQ Place and/or updated by City Guide Superusers.
- distance: Distance from user's location in meters (numeric value only)
- location: An object containing none, some, or all of the following fields:
: address
: address_extended
: locality
: dma
: region
: postcode
: country
: admin_region
: post_town
: po_box
: cross_street
: formatted_address
- tel: Phone number if available
- email: Email if available (can be empty)
- website: Website URL if available
Format your response as a valid JSON array inside a code block.
""",
agent=data_processor,
expected_output="A JSON array containing normalized place details ready for Excel export.",
context=[get_details_task]
)
return [task_analysis, task_locate, search_places_task, get_details_task, process_data_task]

421
backend/temp.py Normal file
View File

@ -0,0 +1,421 @@
# import os
# import pandas as pd
# from typing import List, Dict, Any
# import argparse
# from crewai import Agent, Task, Crew, LLM
# from tools import (
# GetUserLocationTool,
# SearchNearPointTool,
# PlaceSnapTool,
# GetPlaceDetailsTool,
# SearchNearRegionTool
# )
# # Initialize the LLM
# llm = LLM(
# model="gemini/gemini-2.0-flash",
# )
# # Create the agents
# location_finder = Agent(
# role="Location Specialist",
# goal="Determine the user's location or understand the location mentioned in the query",
# backstory="You are an expert in location services and geodata. You can accurately determine where users are located or understand locations they're interested in.",
# tools=[GetUserLocationTool()],
# llm=llm,
# verbose=True
# )
# place_researcher = Agent(
# role="Place Information Researcher",
# goal="Research and provide detailed information about surrounding places",
# backstory="You are an expert at finding and analyzing information about places. You can discover what's around a location and gather complete details about each place. Do not use the word 'place' in query",
# tools=[SearchNearRegionTool(), SearchNearPointTool(), PlaceSnapTool(), GetPlaceDetailsTool()],
# llm=llm,
# verbose=True
# )
# data_processor = Agent(
# role="Data Processing Specialist",
# goal="Process place data and organize it for Excel export",
# backstory="You are specialized in processing location data and organizing it into structured formats suitable for export. You ensure all required fields are captured and formatted properly.",
# llm=llm,
# verbose=True
# )
# def extract_place_details(place_data: Dict[Any, Any]) -> Dict[str, str]:
# """Extract relevant details from place data for Excel export"""
# # Initialize with default values
# details = {
# "name": "",
# "phone": "",
# "email": "",
# "address": "",
# "distance": "",
# "rating": "",
# "category": "",
# "website": ""
# }
# try:
# # Extract basic info
# if "name" in place_data:
# details["name"] = place_data["name"]
# # Extract contact info
# if "tel" in place_data:
# details["phone"] = place_data["tel"]
# # Extract location/address
# if "location" in place_data:
# loc = place_data["location"]
# address_parts = []
# if "address" in loc:
# address_parts.append(loc["address"])
# if "locality" in loc:
# address_parts.append(loc["locality"])
# if "region" in loc:
# address_parts.append(loc["region"])
# if "postcode" in loc:
# address_parts.append(loc["postcode"])
# if "country" in loc:
# address_parts.append(loc["country"])
# details["address"] = ", ".join(filter(None, address_parts))
# # Extract distance info (if available)
# if "distance" in place_data:
# details["distance"] = f"{place_data['distance']} meters"
# # Extract rating (if available)
# if "rating" in place_data:
# details["rating"] = f"{place_data['rating']}/10"
# # Extract category
# if "categories" in place_data and len(place_data["categories"]) > 0:
# details["category"] = place_data["categories"][0].get("name", "")
# # Extract website
# if "website" in place_data:
# details["website"] = place_data["website"]
# # Email may not be directly available in the Foursquare API
# # It might be included in the description or other fields depending on the data
# except Exception as e:
# print(f"Error extracting place details: {e}")
# return details
# def search_surrounding_places(radius: int = 1000, limit: int = 20, query: str = "") -> List[Dict[str, str]]:
# """Search for places surrounding the user's location and collect their details"""
# # Task to get user's location
# get_location_task = Task(
# description="Determine the user's current location by getting their coordinates.",
# agent=location_finder,
# expected_output="Latitude and longitude coordinates of the user's current location."
# )
# # Task to search for surrounding places
# search_places_task = Task(
# description=f"Using the coordinates, search for {query if query else 'places'} within {radius} meters of the user's location.",
# agent=place_researcher,
# expected_output="JSON data of surrounding places including fsq_id, name, category, and other available information.",
# context=[get_location_task]
# )
# # Task to get detailed information about each place
# get_details_task = Task(
# description="For each place found, gather detailed information including name, phone, email, address, and distance.",
# agent=place_researcher,
# expected_output="Complete JSON data with detailed information about each place.",
# context=[search_places_task]
# )
# # Task to process the data for Excel export
# process_data_task = Task(
# description="Process the gathered data and prepare it for Excel export.",
# agent=data_processor,
# expected_output="A list of dictionaries containing cleaned and formatted place details ready for Excel export.",
# context=[get_details_task]
# )
# # Create and execute the crew
# crew = Crew(
# agents=[location_finder, place_researcher, data_processor],
# tasks=[get_location_task, search_places_task, get_details_task, process_data_task],
# )
# result = crew.kickoff()
# place_data = []
# try:
# # Extract the location coordinates
# coordinates = get_location_task.output
# if coordinates and "," in coordinates:
# lat, lng = coordinates.split(",")
# # Use SearchNearPointTool directly to get places
# search_tool = SearchNearPointTool()
# places_result = search_tool._run(
# query=query if query else "business OR restaurant OR shop OR cafe OR store",
# ll=coordinates,
# radius=radius,
# limit=limit
# )
# if places_result and "results" in places_result:
# for place in places_result["results"]:
# # Get details for each place
# if "fsq_id" in place:
# details_tool = GetPlaceDetailsTool()
# place_details = details_tool._run(id=place["fsq_id"])
# if place_details:
# # Combine the search result and details
# combined_data = {**place, **place_details}
# # Extract required fields
# extracted_details = extract_place_details(combined_data)
# place_data.append(extracted_details)
# except Exception as e:
# print(f"Error processing place data: {e}")
# return place_data
# def export_to_excel(place_data: List[Dict[str, str]], filename: str = "surrounding_places.xlsx"):
# """Export the place data to an Excel file"""
# if not place_data:
# print("No place data to export")
# return False
# try:
# df = pd.DataFrame(place_data)
# df.to_excel(filename, index=False)
# print(f"Successfully exported {len(place_data)} places to {filename}")
# return True
# except Exception as e:
# print(f"Error exporting to Excel: {e}")
# return False
# def main():
# parser = argparse.ArgumentParser(description="Search for surrounding places and export to Excel")
# parser.add_argument("--radius", type=int, default=1000, help="Search radius in meters (default: 1000)")
# parser.add_argument("--limit", type=int, default=20, help="Maximum number of places to search (default: 20)")
# parser.add_argument("--query", type=str, default="", help="Optional search query (e.g., 'restaurant', 'cafe')")
# parser.add_argument("--output", type=str, default="surrounding_places.xlsx", help="Output Excel file name")
# args = parser.parse_args()
# print(f"Searching for {args.query if args.query else 'places'} within {args.radius}m...")
# place_data = search_surrounding_places(radius=args.radius, limit=args.limit, query=args.query)
# if place_data:
# export_to_excel(place_data, args.output)
# print(f"Found {len(place_data)} places. Data exported to {args.output}")
# else:
# print("No places found or error occurred during search.")
# if __name__ == "__main__":
# main()
import pandas as pd
import json
import re
from typing import List, Dict, Any
import argparse
from crewai import Agent, Task, Crew, LLM
from tools import (
GetUserLocationTool,
SearchNearPointTool,
PlaceSnapTool,
GetPlaceDetailsTool,
SearchNearRegionTool
)
# Initialize the LLM
llm = LLM(
model="gemini/gemini-2.0-flash",
)
task_picker = Agent(
role="Expert Decision Maker",
goal="Decide what information the user wants based on the query and choose the appropriate task to be performed",
backstory="As an expert decision maker, you are able to take accurate decisions on what task the user is asking you to perform (whether the user has asked to get information on a location based on their location, or if they have given the name of the location and want information based on that place)",
llm=llm,
verbose=True
)
# Create the agents
location_finder = Agent(
role="Location Specialist",
goal="Determine the user's location or understand the location mentioned in the query",
backstory="You are an expert in location services and geodata. You can accurately determine where users are located or understand locations they're interested in.",
tools=[GetUserLocationTool()],
llm=llm,
verbose=True
)
place_researcher = Agent(
role="Place Information Researcher",
goal="Research and provide detailed information about surrounding places",
backstory="You are an expert at finding and analyzing information about places. You can discover what's around a location and gather complete details about each place.",
tools=[SearchNearRegionTool(), SearchNearPointTool(), PlaceSnapTool(), GetPlaceDetailsTool()],
llm=llm,
verbose=True
)
data_processor = Agent(
role="Data Processing Specialist",
goal="Process place data and organize it for Excel export",
backstory="You are specialized in processing location data and organizing it into structured formats suitable for export. You ensure all required fields are captured and formatted properly. You will return a properly formatted JSON array of places with all the required details.",
llm=llm,
verbose=True
)
class JsonExtractor:
"""Helper class to extract JSON from agent output text"""
@staticmethod
def extract_json(text):
"""Extract JSON data from text that might contain markdown code blocks"""
# Try to find JSON in code blocks
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
if json_match:
json_str = json_match.group(1)
else:
# If no code blocks, try to find anything that looks like a JSON array or object
json_match = re.search(r'(\[[\s\S]*\]|\{[\s\S]*\})', text)
if json_match:
json_str = json_match.group(1)
else:
return None
try:
return json.loads(json_str)
except json.JSONDecodeError:
try:
# Sometimes the JSON might have trailing commas which are invalid
# Try to clean it up by removing trailing commas
cleaned_json = re.sub(r',\s*([}\]])', r'\1', json_str)
return json.loads(cleaned_json)
except:
return None
def search_surrounding_places(prompt) -> List[Dict[str, str]]:
"""Search for places surrounding the user's location and collect their details"""
task_analysis = Task(
description="Analyze the user query: {user_query} and determine what type of location information they are seeking. And also check if they have defined any radius or limits or queries",
agent=task_picker,
expected_output="A clear determination of what the user is asking for: their current location info, recommendations near them, or info about a specific named location."
)
task_locate = Task(
description="Based on the user query: {user_query}, determine the location to focus on. Either get the user's current location or identify the location mentioned in the query.",
agent=location_finder,
expected_output="Coordinates (latitude,longitude) or a location name that will be used for subsequent tasks."
)
# Task to get user's location
# get_location_task = Task(
# description="Determine the user's current location by getting their coordinates.",
# agent=location_finder,
# expected_output="Latitude and longitude coordinates of the user's current location.",
# context=[task_analysis]
# )
# Task to search for surrounding places
search_places_task = Task(
description="Using the location from the previous task, gather relevant information about places based on the user query: {user_query}",
agent=place_researcher,
expected_output="JSON data of surrounding places including fsq_id, name, category, and other available information.",
context=[task_locate]
)
# Task to get detailed information about each place
get_details_task = Task(
description="For each place found, gather detailed information including name, phone, email, address, and distance.",
agent=place_researcher,
expected_output="Complete JSON data with detailed information about each place.",
context=[search_places_task]
)
# Task to process the data for Excel export
process_data_task = Task(
description="""Process the gathered data and prepare it for Excel export.
You MUST return a valid JSON array containing objects with these fields:
- name: Name of the business/place
- fsq_id: Foursquare ID
- distance: Distance from user's location in meters (numeric value only)
- address: Complete address
- phone: Phone number if available
- email: Email if available (can be empty)
- website: Website URL if available
Format your response as a valid JSON array inside a code block.
""",
agent=data_processor,
expected_output="A JSON array containing normalized place details ready for Excel export.",
context=[get_details_task]
)
# Create and execute the crew
crew = Crew(
agents=[location_finder, place_researcher, data_processor],
tasks=[task_analysis, task_locate, search_places_task, get_details_task, process_data_task],
# verbose=2
)
result = crew.kickoff(inputs={"user_query": prompt})
# Extract the JSON data from the result
place_data = JsonExtractor.extract_json(result.raw)
# If we got valid data from the agent's output, return it
if place_data and isinstance(place_data, list) and len(place_data) > 0:
# Ensure the data is standardized
standardized_data = []
for place in place_data:
standardized_place = {
"name": place.get("name", ""),
"fsq_id": place.get("fsq_id", ""),
"distance": place.get("distance", ""),
"address": place.get("address", ""),
"phone": place.get("phone", ""),
"email": place.get("email", ""),
"website": place.get("website", "")
}
standardized_data.append(standardized_place)
return standardized_data
def export_to_excel(place_data: List[Dict[str, str]], filename: str = "surrounding_places.xlsx"):
"""Export the place data to an Excel file"""
if not place_data:
print("No place data to export")
return False
try:
df = pd.DataFrame(place_data)
# Reorder columns for better readability
column_order = ["name", "address", "distance", "phone", "email", "website", "fsq_id"]
available_columns = [col for col in column_order if col in df.columns]
# Add any columns that might be in the data but not in our order list
available_columns.extend([col for col in df.columns if col not in column_order])
df = df[available_columns]
df.to_excel(filename, index=False)
print(f"Successfully exported {len(place_data)} places to {filename}")
return True
except Exception as e:
print(f"Error exporting to Excel: {e}")
return False
def main(prompt):
place_data = search_surrounding_places(prompt=prompt)
if place_data:
export_to_excel(place_data)
print(f"Found {len(place_data)} places. Data exported to surrounding_places.xlsx")
else:
print("No places found or error occurred during search.")

16
backend/tools/__init__.py Normal file
View File

@ -0,0 +1,16 @@
from .tools import (
SearchNearRegionTool,
SearchNearPointTool,
PlaceSnapTool,
GetPlaceDetailsTool,
GetUserLocationTool
)
# Export all tool classes
__all__ = [
'SearchNearRegionTool',
'SearchNearPointTool',
'PlaceSnapTool',
'GetPlaceDetailsTool',
'GetUserLocationTool'
]

Binary file not shown.

Binary file not shown.

127
backend/tools/tools.py Normal file
View File

@ -0,0 +1,127 @@
from typing import Type, Optional
from pydantic import BaseModel, Field
import requests
import os
from urllib.parse import urlencode
from crewai.tools import BaseTool
from pydantic import BaseModel
import requests
import geocoder
FSQ_API_BASE = "https://api.foursquare.com/v3"
FSQ_SERVICE_TOKEN = os.getenv("FOURSQUARE_SERVICE_TOKEN")
# Wrapper function to execute the API call
def submit_request(endpoint, params):
headers = {
"accept": "application/json",
"Authorization": f"{FSQ_SERVICE_TOKEN}",
# "X-Places-Api-Version": "2025-02-05"
}
encoded_params = urlencode(params)
url = f"{FSQ_API_BASE}{endpoint}?{encoded_params}"
print(url)
try:
response = requests.get(url, headers=headers)
print(response)
if response.status_code == 200:
return response.json()
else:
print('Error:', response.status_code)
return None
except requests.exceptions.RequestException as e:
print('Error:', e)
return None
class SeachNearRegionSchema(BaseModel):
query: str
near: str
limit: Optional[int] = Field(default=5, description="Maximum number of results to return")
class SearchNearRegionTool(BaseTool):
name: str = 'Search Near a Named Region Tool'
description: str ="""Search for places near a particular named region.
Has a geographic region (e.g., New Delhi, India) and looks for specific concepts (e.g., coffee shop, restaurants) in that geographic region"""
args_schema: Type[BaseModel] = SeachNearRegionSchema
def _run(self, query: str, near: str, limit: Optional[int] = 5):
params = {
"query": query,
"near": near,
"limit": limit
}
return submit_request("/places/search", params)
class SeachNearPointSchema(BaseModel):
query: str
ll: str = Field(description="Comma separated latitude and longitude pair (e.g., 40.74,-74.0)")
radius: int = Field(default=2000, description="Maximum radius within which to search", limit=100000)
limit: Optional[int] = Field(default=5, description="Maximum number of results to return")
class SearchNearPointTool(BaseTool):
name: str = 'Search Near a Point Tool'
description: str ="""Search for places near a particular point.
Looks for concepts (e.g., coffee shop, Hard Rock Cafe) using comma separated latitude and longitude pair (e.g., 40.74,-74.0) and radius which is defined in metres (e.g., 1000)"""
args_schema: Type[BaseModel] = SeachNearPointSchema
def _run(self, query: str, ll: str, radius: Optional[int] = 2000, limit: Optional[int] = 5):
params = {
"query": query,
"ll": ll,
"radius": radius,
"limit": limit
}
return submit_request("/places/search", params)
class PlaceSnapSchema(BaseModel):
ll: str = Field(description="Comma separated latitude and longitude pair (e.g., 40.74,-74.0)")
class PlaceSnapTool(BaseTool):
name: str = "Get the place tool"
description: str = """Get the most likely place the user is at based on their reported location. Takes in a comma separated latitude and longitude pair as input"""
args_schema: Type[BaseModel] = PlaceSnapSchema
def _run(self, ll: str):
params = {
"ll": ll,
"limit": 1
}
return submit_request("/geotagging/candidates", params)
class PlaceDetailsSchema(BaseModel):
id: str
class GetPlaceDetailsTool(BaseTool):
name: str = "Get details of a place tool"
description: str = """Get detailed information about a place based on the fsq_id (foursquare id), including:
description, phone, website, social media, hours, popular hours, rating (out of 10),
price, menu, top photos, top tips (mini-reviews from users), top tastes, and features
such as takes reservations."""
args_schema: Type[BaseModel] = PlaceDetailsSchema
def _run(self, id: str):
params = {
"fields": "name,description,location,distance,tel,email,website"
}
return submit_request(f"/places/{id}", params)
class GetUserLocationTool(BaseTool):
name: str = "Get user location tool"
description: str = """Get user's location. Returns latitude and longitude, or else reports it could not find location. Tries to guess
user's location based on ip address. Useful if the user has not provided their own precise location."""
def _run(self):
location = geocoder.ip('me')
if not location.ok:
return "I don't know where you are"
return f"{location.lat},{location.lng}"
if __name__ == "__main__":
tool = GetUserLocationTool()
result = tool._run()
print("Result: ", result)