From d3ae3516c5b951bfeb22358e786147d4f9c8b6d0 Mon Sep 17 00:00:00 2001 From: Matiss Date: Mon, 4 May 2026 22:39:04 +0100 Subject: [PATCH] Initialize authenticated webapp --- .gitignore | 3 + README.md | 52 +- components.json | 21 + convex/_generated/api.d.ts | 45 ++ convex/_generated/api.js | 23 + convex/_generated/dataModel.d.ts | 58 ++ convex/_generated/server.d.ts | 143 ++++ convex/_generated/server.js | 93 +++ convex/auth.config.ts | 10 + eslint.config.mjs | 1 + package-lock.json | 874 +++++++++++++++++++++- package.json | 13 +- src/app/globals.css | 64 +- src/app/layout.tsx | 48 +- src/app/maps/page.tsx | 9 + src/app/overview/page.tsx | 110 +++ src/app/page.tsx | 64 +- src/app/sign-in/[[...sign-in]]/page.tsx | 14 + src/app/sign-up/[[...sign-up]]/page.tsx | 14 + src/components/app-sidebar.tsx | 86 +++ src/components/business-map.tsx | 390 ++++++++++ src/components/convex-client-provider.tsx | 21 + src/components/google-places-map.tsx | 204 +++++ src/components/ui/button.tsx | 54 ++ src/components/ui/card.tsx | 42 ++ src/lib/utils.ts | 6 + src/proxy.ts | 16 + 27 files changed, 2376 insertions(+), 102 deletions(-) create mode 100644 components.json create mode 100644 convex/_generated/api.d.ts create mode 100644 convex/_generated/api.js create mode 100644 convex/_generated/dataModel.d.ts create mode 100644 convex/_generated/server.d.ts create mode 100644 convex/_generated/server.js create mode 100644 convex/auth.config.ts create mode 100644 src/app/maps/page.tsx create mode 100644 src/app/overview/page.tsx create mode 100644 src/app/sign-in/[[...sign-in]]/page.tsx create mode 100644 src/app/sign-up/[[...sign-up]]/page.tsx create mode 100644 src/components/app-sidebar.tsx create mode 100644 src/components/business-map.tsx create mode 100644 src/components/convex-client-provider.tsx create mode 100644 src/components/google-places-map.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/lib/utils.ts create mode 100644 src/proxy.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..e2764f9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/README.md b/README.md index e215bc4..e04f07f 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,32 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Kairas Webapp -## Getting Started +Next.js App Router app with Clerk authentication, Convex client wiring, and shadcn-style UI components. -First, run the development server: +## Setup + +Copy `.env.example` to `.env.local` and fill in the Clerk keys: + +```bash +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= +CLERK_SECRET_KEY= +``` + +Create or link a Convex deployment: + +```bash +npx convex dev +``` + +That command will set `NEXT_PUBLIC_CONVEX_URL`. Configure `CLERK_JWT_ISSUER_DOMAIN` in Convex using your Clerk Frontend API URL, then rerun `npx convex dev` so Convex syncs `convex/auth.config.ts`. + +## Development ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Routes: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- `/sign-in` is public. +- `/sign-up` is public. +- `/` is protected and redirects unauthenticated users to sign in. diff --git a/components.json b/components.json new file mode 100644 index 0000000..3289f23 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts new file mode 100644 index 0000000..5abfa11 --- /dev/null +++ b/convex/_generated/api.d.ts @@ -0,0 +1,45 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; + +declare const fullApi: ApiFromModules<{}>; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; + +export declare const components: {}; diff --git a/convex/_generated/api.js b/convex/_generated/api.js new file mode 100644 index 0000000..44bf985 --- /dev/null +++ b/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi, componentsGeneric } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; +export const components = componentsGeneric(); diff --git a/convex/_generated/dataModel.d.ts b/convex/_generated/dataModel.d.ts new file mode 100644 index 0000000..a850cc1 --- /dev/null +++ b/convex/_generated/dataModel.d.ts @@ -0,0 +1,58 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { AnyDataModel } from "convex/server"; +import type { GenericId } from "convex/values"; + +/** + * No `schema.ts` file found! + * + * This generated code has permissive types like `Doc = any` because + * Convex doesn't know your schema. If you'd like more type safety, see + * https://docs.convex.dev/using/schemas for instructions on how to add a + * schema file. + * + * After you change a schema, rerun codegen with `npx convex dev`. + */ + +/** + * The names of all of your Convex tables. + */ +export type TableNames = string; + +/** + * The type of a document stored in Convex. + */ +export type Doc = any; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = AnyDataModel; diff --git a/convex/_generated/server.d.ts b/convex/_generated/server.d.ts new file mode 100644 index 0000000..bec05e6 --- /dev/null +++ b/convex/_generated/server.d.ts @@ -0,0 +1,143 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/convex/_generated/server.js b/convex/_generated/server.js new file mode 100644 index 0000000..bf3d25a --- /dev/null +++ b/convex/_generated/server.js @@ -0,0 +1,93 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export const httpAction = httpActionGeneric; diff --git a/convex/auth.config.ts b/convex/auth.config.ts new file mode 100644 index 0000000..59fcbd7 --- /dev/null +++ b/convex/auth.config.ts @@ -0,0 +1,10 @@ +import type { AuthConfig } from "convex/server"; + +export default { + providers: [ + { + domain: process.env.CLERK_JWT_ISSUER_DOMAIN!, + applicationID: "convex", + }, + ], +} satisfies AuthConfig; diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..5f790ec 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,7 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + "convex/_generated/**", ]), ]); diff --git a/package-lock.json b/package-lock.json index bb9a193..7300beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,25 @@ { - "name": "kairas-webapp-scaffold", + "name": "kairas-webapp", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "kairas-webapp-scaffold", + "name": "kairas-webapp", "version": "0.1.0", "dependencies": { + "@clerk/nextjs": "^7.3.0", + "@clerk/themes": "^2.4.57", + "@radix-ui/react-slot": "^1.2.4", + "@vis.gl/react-google-maps": "^1.8.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "convex": "^1.37.0", + "lucide-react": "^1.14.0", "next": "16.2.4", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -276,6 +285,136 @@ "node": ">=6.9.0" } }, + "node_modules/@clerk/backend": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.4.4.tgz", + "integrity": "sha512-EGdpxBG1joIYzRBtpzTq2FGyM2ErSSF2UB7FZXrHiSb6V/uRUcvVcX7IWvYeEyg1AG100gJWr0KpbutajVCLMA==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^4.9.0", + "standardwebhooks": "^1.0.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.9.0" + } + }, + "node_modules/@clerk/nextjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-7.3.0.tgz", + "integrity": "sha512-Dip2oOVc720amG9bBZT1L9y7FoDJjRyz8MaGtbp2SUrtRuvaVq0WhHYzGTI1Nqy5tHMcnzEOSG7KsGf4FwIvzw==", + "license": "MIT", + "dependencies": { + "@clerk/backend": "^3.4.4", + "@clerk/react": "^6.5.0", + "@clerk/shared": "^4.9.0", + "server-only": "0.0.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "peerDependencies": { + "next": "^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0", + "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", + "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + } + }, + "node_modules/@clerk/react": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.5.0.tgz", + "integrity": "sha512-TXYxlJ9EvPvJo6NKTy//RlqoZ4nhvmIjlU9cp9KG3buHD7UCby0It9dtaPHbAu5h4s426C7F6GID47LolaLEbg==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^4.9.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", + "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + } + }, + "node_modules/@clerk/shared": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.9.0.tgz", + "integrity": "sha512-YU9Fv6FK1EwxM7uz1hGPrLpvcHRhHm8Quuh5RL343oXakE+/JHtYy4OhwVgqYVA+j63pw+7lpghL3CYQTgk0hA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "^5.100.6", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", + "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/themes": { + "version": "2.4.57", + "resolved": "https://registry.npmjs.org/@clerk/themes/-/themes-2.4.57.tgz", + "integrity": "sha512-Nb3bO79rMTU/MPVTC/dde6LG27/IgOMKIYi5KSvAmO4ZUHlj0OWufu6CMvz5OYVZ0YdyMnTBU2aPGRUiRzO+2w==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^3.47.2", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + } + }, + "node_modules/@clerk/themes/node_modules/@clerk/shared": { + "version": "3.47.5", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.47.5.tgz", + "integrity": "sha512-rDVe73/VN2NZXhtrLRHshkUpQDrevAqDRxeXUl2M0IBEBkcl+VMHlV7fep53cVWo0b3gIqLk82pmmi+WoyF/xg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "csstype": "3.1.3", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0", + "swr": "2.3.4" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", + "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/themes/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -309,6 +448,422 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -453,6 +1008,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@googlemaps/js-api-loader": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-2.0.2.tgz", + "integrity": "sha512-bKVuTqatS8Jven5aFqVB7rCHF1VFEzpzyi0ruzO0GUR+A7m9oMqMgtnmpANj7kMYEvvhty8Fk7TnJ1MKjWHu+Q==", + "license": "Apache-2.0", + "dependencies": { + "@types/google.maps": "^3.53.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1240,6 +1804,39 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1247,6 +1844,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1527,6 +2130,16 @@ "tailwindcss": "4.2.4" } }, + "node_modules/@tanstack/query-core": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", + "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1545,6 +2158,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/google.maps": { + "version": "3.64.0", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.64.0.tgz", + "integrity": "sha512-dN0H6tB4lgLQLovcbPXFYYOEV41TpyyJghzb5jrzjB96FZmjeOghevVdC+BMGd6YqyCqXaggyEtqRXLRjzCBZA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1573,7 +2192,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2153,6 +2772,21 @@ "win32" ] }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.8.3.tgz", + "integrity": "sha512-DW7nEuvOJ299DmdBnvGiUARrgS/+sTEO1iJgG9J8YaErZqLoq7S4TJ22f3EjJvR4dti4L4gft43JEK77nnKXDw==", + "license": "MIT", + "dependencies": { + "@googlemaps/js-api-loader": "^2.0.2", + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", + "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2613,12 +3247,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2653,6 +3308,44 @@ "dev": true, "license": "MIT" }, + "node_modules/convex": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.37.0.tgz", + "integrity": "sha512-xGSx5edIsXCEex3OU2U2N0oyB/cOa9qGwKiImF9yOWqjqZgOkx39idtpdlwNBTBSt4S30oAvs4yeXY5xxPIX3A==", + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.18.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "@clerk/react": "^6.4.3", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "@clerk/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2672,7 +3365,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2797,6 +3490,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3040,6 +3742,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3473,7 +4216,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3520,6 +4262,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3754,6 +4502,12 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4424,6 +5178,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4858,6 +5621,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5379,6 +6151,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5644,6 +6431,12 @@ "semver": "bin/semver.js" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5866,6 +6659,22 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6065,6 +6874,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -6423,6 +7255,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6538,6 +7379,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 335b7c8..66fe0f5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "kairas-webapp-scaffold", + "name": "kairas-webapp", "version": "0.1.0", "private": true, "scripts": { @@ -9,9 +9,18 @@ "lint": "eslint" }, "dependencies": { + "@clerk/nextjs": "^7.3.0", + "@clerk/themes": "^2.4.57", + "@radix-ui/react-slot": "^1.2.4", + "@vis.gl/react-google-maps": "^1.8.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "convex": "^1.37.0", + "lucide-react": "^1.14.0", "next": "16.2.4", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..ed51b72 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,11 +3,49 @@ :root { --background: #ffffff; --foreground: #171717; + --card: #ffffff; + --card-foreground: #171717; + --popover: #ffffff; + --popover-foreground: #171717; + --primary: #18181b; + --primary-foreground: #fafafa; + --secondary: #f4f4f5; + --secondary-foreground: #18181b; + --muted: #f4f4f5; + --muted-foreground: #71717a; + --accent: #f4f4f5; + --accent-foreground: #18181b; + --destructive: #dc2626; + --destructive-foreground: #fafafa; + --border: #e4e4e7; + --input: #e4e4e7; + --ring: #18181b; + --radius: 0.5rem; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } @@ -16,11 +54,35 @@ :root { --background: #0a0a0a; --foreground: #ededed; + --card: #18181b; + --card-foreground: #fafafa; + --popover: #18181b; + --popover-foreground: #fafafa; + --primary: #fafafa; + --primary-foreground: #18181b; + --secondary: #27272a; + --secondary-foreground: #fafafa; + --muted: #27272a; + --muted-foreground: #a1a1aa; + --accent: #27272a; + --accent-foreground: #fafafa; + --destructive: #ef4444; + --destructive-foreground: #fafafa; + --border: #27272a; + --input: #27272a; + --ring: #d4d4d8; } } +* { + border-color: var(--border); +} + +html { + height: 100%; +} + body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..bb6ff32 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,14 @@ import type { Metadata } from "next"; +import { + ClerkProvider, + Show, + SignInButton, + SignUpButton, +} from "@clerk/nextjs"; +import { dark } from "@clerk/themes"; import { Geist, Geist_Mono } from "next/font/google"; +import { AppSidebar } from "@/components/app-sidebar"; +import { ConvexClientProvider } from "@/components/convex-client-provider"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +22,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Kairas App", + description: "Authenticated Kairas workspace.", }; export default function RootLayout({ @@ -27,7 +36,40 @@ export default function RootLayout({ lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - {children} + + + + +
+
+
+

+ Kairas +

+

+ Maps Studio +

+
+ +
+
+
+ +
{children}
+
+
+ ); } diff --git a/src/app/maps/page.tsx b/src/app/maps/page.tsx new file mode 100644 index 0000000..671d207 --- /dev/null +++ b/src/app/maps/page.tsx @@ -0,0 +1,9 @@ +import { auth } from "@clerk/nextjs/server"; + +import { BusinessMap } from "@/components/business-map"; + +export default async function MapsPage() { + await auth.protect(); + + return ; +} diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx new file mode 100644 index 0000000..20add9d --- /dev/null +++ b/src/app/overview/page.tsx @@ -0,0 +1,110 @@ +import { auth } from "@clerk/nextjs/server"; +import { ArrowRight, Building2, LineChart, MapPinned } from "lucide-react"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +const stats = [ + ["42", "Mapped leads"], + ["18", "Priority prospects"], + ["7", "Markets tracked"], +]; + +const cards = [ + { + title: "Research local markets", + description: + "Drop into any area and pull businesses, ratings, websites, and review context from Google Maps.", + icon: MapPinned, + }, + { + title: "Qualify business profiles", + description: + "Use website availability, rating count, and recent customer language to prioritize outreach.", + icon: Building2, + }, + { + title: "Track opportunities", + description: + "This sample dashboard is ready to connect to Convex collections when you add saved places.", + icon: LineChart, + }, +]; + +export default async function OverviewPage() { + await auth.protect(); + + return ( +
+
+
+
+

+ Kairas Maps Studio +

+

+ A sample workspace for local business research. +

+

+ This overview page is a basic sample site: metrics, workflow + cards, and a clear entry point into the full-screen maps tool. +

+
+ +
+
+ + + + Pipeline Snapshot + + Placeholder metrics for the first dashboard pass. + + + + {stats.map(([value, label]) => ( +
+ {label} + {value} +
+ ))} +
+
+
+ +
+ {cards.map((card) => { + const Icon = card.icon; + + return ( + + +
+ +
+ {card.title} + {card.description} +
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..4bb2145 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; +import { redirect } from "next/navigation"; export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); + redirect("/overview"); } diff --git a/src/app/sign-in/[[...sign-in]]/page.tsx b/src/app/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..f6d5dc2 --- /dev/null +++ b/src/app/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,14 @@ +import { SignIn } from "@clerk/nextjs"; + +export default function SignInPage() { + return ( +
+ +
+ ); +} diff --git a/src/app/sign-up/[[...sign-up]]/page.tsx b/src/app/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..d94434f --- /dev/null +++ b/src/app/sign-up/[[...sign-up]]/page.tsx @@ -0,0 +1,14 @@ +import { SignUp } from "@clerk/nextjs"; + +export default function SignUpPage() { + return ( +
+ +
+ ); +} diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx new file mode 100644 index 0000000..95829b9 --- /dev/null +++ b/src/components/app-sidebar.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Show, UserButton } from "@clerk/nextjs"; +import { BarChart3, Map, Menu, Search, Settings } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { cn } from "@/lib/utils"; + +const navItems = [ + { + href: "/overview", + label: "Overview", + icon: BarChart3, + }, + { + href: "/maps", + label: "Maps", + icon: Map, + }, +]; + +export function AppSidebar() { + const pathname = usePathname(); + + return ( + + + +
+
+ + Kairas +
+ +
+
+ ); +} diff --git a/src/components/business-map.tsx b/src/components/business-map.tsx new file mode 100644 index 0000000..1ee419a --- /dev/null +++ b/src/components/business-map.tsx @@ -0,0 +1,390 @@ +"use client"; + +import { + APIProvider, + Map, + Marker, + useMap, + useMapsLibrary, +} from "@vis.gl/react-google-maps"; +import { + ExternalLink, + Globe2, + LocateFixed, + MapPin, + MousePointer2, + Search, + Star, +} from "lucide-react"; +import { FormEvent, useMemo, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type BusinessReview = { + author: string; + rating: number | null; + text: string | null; + when: string | null; + url: string | null; +}; + +type BusinessResult = { + id: string; + name: string; + address: string | undefined; + rating: number | undefined; + ratingCount: number | undefined; + website: string | undefined; + googleMapsUrl: string | undefined; + reviews: BusinessReview[]; + position: google.maps.LatLngLiteral; +}; + +const defaultCenter = { lat: 53.3498, lng: -6.2603 }; +const fallbackRadius = 1500; + +export function BusinessMap() { + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; + const mapId = process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID; + + if (!apiKey) { + return ( +
+
+ +

Google Maps is not configured

+

+ Add `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` to `.env.local`, enable Maps + JavaScript API and Places API, then restart the dev server. +

+
+
+ ); + } + + return ( + + + + ); +} + +function BusinessMapContent({ mapId }: { mapId?: string }) { + const map = useMap(); + const places = useMapsLibrary("places"); + const [pointer, setPointer] = + useState(defaultCenter); + const [query, setQuery] = useState("restaurants"); + const [radius, setRadius] = useState(fallbackRadius); + const [results, setResults] = useState([]); + const [selected, setSelected] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const center = useMemo( + () => selected?.position ?? pointer, + [pointer, selected], + ); + + function placePointer(position: google.maps.LatLngLiteral) { + setPointer(position); + setSelected(null); + map?.panTo(position); + } + + async function findBusinesses(event?: FormEvent) { + event?.preventDefault(); + + if (!places || !map || query.trim().length === 0) { + return; + } + + setIsLoading(true); + setError(null); + + try { + const { Place } = places; + const { places: foundPlaces } = await Place.searchByText({ + textQuery: `${query} near ${pointer.lat}, ${pointer.lng}`, + fields: [ + "id", + "displayName", + "formattedAddress", + "googleMapsURI", + "location", + "rating", + "reviews", + "userRatingCount", + "websiteURI", + ], + maxResultCount: 12, + locationBias: { + center: pointer, + radius, + }, + }); + + const nextResults = foundPlaces + .map((place) => { + const location = place.location; + + if (!place.id || !place.displayName || !location) { + return null; + } + + return { + id: place.id, + name: place.displayName, + address: place.formattedAddress ?? undefined, + rating: place.rating ?? undefined, + ratingCount: place.userRatingCount ?? undefined, + website: place.websiteURI ?? undefined, + googleMapsUrl: place.googleMapsURI ?? undefined, + reviews: (place.reviews ?? []).slice(0, 3).map((review) => ({ + author: review.authorAttribution?.displayName ?? "Google user", + rating: review.rating, + text: review.text, + when: review.relativePublishTimeDescription, + url: review.googleMapsURI, + })), + position: location.toJSON(), + } satisfies BusinessResult; + }) + .filter((place): place is BusinessResult => place !== null); + + setResults(nextResults); + setSelected(nextResults[0] ?? null); + + if (nextResults[0]) { + map.panTo(nextResults[0].position); + map.setZoom(14); + } + } catch (caughtError) { + setError( + caughtError instanceof Error + ? caughtError.message + : "Google Places search failed.", + ); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+ { + if (event.detail.latLng) { + placePointer(event.detail.latLng); + } + }} + > + + {results.map((business) => ( + setSelected(business)} + /> + ))} + + +
+
+ + Drop a pointer +
+

+ Click anywhere on the map, then search for local businesses around + that point. +

+
+
+ + +
+ ); +} + +function BusinessCard({ + business, + isSelected, + onSelect, +}: { + business: BusinessResult; + isSelected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} diff --git a/src/components/convex-client-provider.tsx b/src/components/convex-client-provider.tsx new file mode 100644 index 0000000..8dcbd9e --- /dev/null +++ b/src/components/convex-client-provider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useAuth } from "@clerk/nextjs"; +import { ConvexReactClient } from "convex/react"; +import { ConvexProviderWithClerk } from "convex/react-clerk"; +import type { ReactNode } from "react"; + +const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; +const convex = convexUrl ? new ConvexReactClient(convexUrl) : null; + +export function ConvexClientProvider({ children }: { children: ReactNode }) { + if (!convex) { + return children; + } + + return ( + + {children} + + ); +} diff --git a/src/components/google-places-map.tsx b/src/components/google-places-map.tsx new file mode 100644 index 0000000..338bcf2 --- /dev/null +++ b/src/components/google-places-map.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { + APIProvider, + Map, + Marker, + useMap, + useMapsLibrary, +} from "@vis.gl/react-google-maps"; +import { MapPin, Search } from "lucide-react"; +import { FormEvent, useMemo, useState } from "react"; + +import { Button } from "@/components/ui/button"; + +type PlaceResult = { + id: string; + name: string; + address: string | undefined; + rating: number | undefined; + position: google.maps.LatLngLiteral; +}; + +const defaultCenter = { lat: 53.3498, lng: -6.2603 }; + +export function GooglePlacesMap() { + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; + const mapId = process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID; + + if (!apiKey) { + return ( +
+
+ +

Google Maps is not configured

+

+ Add `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` to `.env.local`, enable Maps + JavaScript API and Places API, then restart the dev server. +

+
+
+ ); + } + + return ( + + + + ); +} + +function PlacesMapContent({ mapId }: { mapId?: string }) { + const map = useMap(); + const places = useMapsLibrary("places"); + const [query, setQuery] = useState("coffee near Dublin"); + const [results, setResults] = useState([]); + const [selected, setSelected] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const center = useMemo( + () => selected?.position ?? results[0]?.position ?? defaultCenter, + [results, selected], + ); + + async function searchPlaces(event: FormEvent) { + event.preventDefault(); + + if (!places || !map || query.trim().length === 0) { + return; + } + + setIsLoading(true); + setError(null); + + try { + const { Place } = places; + const { places: foundPlaces } = await Place.searchByText({ + textQuery: query, + fields: ["id", "displayName", "formattedAddress", "location", "rating"], + maxResultCount: 6, + locationBias: map.getBounds() ?? undefined, + }); + + const nextResults = foundPlaces + .map((place) => { + const location = place.location; + + if (!place.id || !place.displayName || !location) { + return null; + } + + return { + id: place.id, + name: place.displayName, + address: place.formattedAddress ?? undefined, + rating: place.rating ?? undefined, + position: location.toJSON(), + } satisfies PlaceResult; + }) + .filter((place): place is PlaceResult => place !== null); + + setResults(nextResults); + setSelected(nextResults[0] ?? null); + + if (nextResults.length > 0) { + map.panTo(nextResults[0].position); + map.setZoom(13); + } + } catch (caughtError) { + setError( + caughtError instanceof Error + ? caughtError.message + : "Google Places search failed.", + ); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+ + {results.map((place) => ( + setSelected(place)} + /> + ))} + +
+ + +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..9ad7a14 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex h-10 shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: ButtonProps) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..e69e0fa --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"h3">) { + return ( +

+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return

; +} + +export { Card, CardHeader, CardTitle, CardDescription, CardContent }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..8afbe7c --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,16 @@ +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; + +const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"]); + +export default clerkMiddleware(async (auth, req) => { + if (!isPublicRoute(req)) { + await auth.protect(); + } +}); + +export const config = { + matcher: [ + "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + "/(api|trpc)(.*)", + ], +};