From d88f55363ceca2f52c734fdb57b0b37ea6bc9d46 Mon Sep 17 00:00:00 2001 From: Jennifer Echenim Date: Tue, 8 Oct 2024 20:35:34 +0400 Subject: [PATCH] [Frontend] monorepo changes (#2084) chore: Monorepo updates and enhancements - Updated `404` component with default values - Updated package dependencies - feat: Added page links' variables and updated details pages - Refactored document page structure - fix: Resolved build error due to duplicate import - Switched package manager from `npm` to `pnpm` - Updated Dockerfile configuration for Tenscan frontend - fix: Display full address on desktop view - Added monorepo structure documentation for TEN frontend projects --- package.json | 25 + packages/README.md | 72 + .../apis/.config/openapi-codegen.config.ts | 28 + packages/apis/.config/p2o.config.json | 4 + packages/apis/package.json | 28 + packages/apis/src/api/client.ts | 166 + .../apis/src/api/codegen/digestComponents.ts | 1078 ++ .../apis/src/api/codegen/digestContext.ts | 99 + .../apis/src/api/codegen/digestFetcher.ts | 94 + packages/apis/src/api/response-types.ts | 38 + packages/apis/src/libs/index.ts | 18 + packages/apis/src/libs/observables/auth.ts | 5 + packages/apis/storage/digest-collection.json | 1287 ++ packages/apis/storage/digest-openapi.json | 1450 ++ packages/apis/tsconfig.json | 16 + packages/eslint-config/README.md | 3 + packages/eslint-config/library.js | 34 + packages/eslint-config/next.js | 35 + packages/eslint-config/package.json | 19 + packages/eslint-config/react-internal.js | 43 + packages/shared/package.json | 13 + packages/shared/src/data.helpers.ts | 37 + packages/shared/tsconfig.json | 8 + packages/typescript-config/base.json | 20 + packages/typescript-config/nextjs.json | 13 + packages/typescript-config/package.json | 9 + packages/typescript-config/react-library.json | 8 + packages/ui/.eslintrc.js | 12 + packages/ui/api/general.ts | 22 + packages/ui/api/index.ts | 90 + packages/ui/components/common/404.tsx | 38 + packages/ui/components/common/500.tsx | 32 + .../ui/components/common/connect-wallet.tsx | 94 + packages/ui/components/common/copy.tsx | 21 + .../data-table/data-table-column-header.tsx | 71 + .../data-table/data-table-faceted-filter.tsx | 143 + .../data-table/data-table-pagination.tsx | 144 + .../data-table/data-table-row-actions.tsx | 65 + .../common/data-table/data-table-toolbar.tsx | 67 + .../data-table/data-table-view-options.tsx | 59 + .../common/data-table/data-table.tsx | 211 + .../common/data-table/skeleton-loader.tsx | 22 + packages/ui/components/common/document.tsx | 50 + packages/ui/components/common/empty-state.tsx | 54 + packages/ui/components/common/error.tsx | 91 + .../ui/components/common/loading-list.tsx | 19 + .../ui/components/common/loading-state.tsx | 35 + .../ui/components/common/network-status.tsx | 40 + .../components/common/truncated-address.tsx | 70 + packages/ui/components/shared/alert.tsx | 64 + packages/ui/components/shared/avatar.tsx | 54 + packages/ui/components/shared/badge.tsx | 37 + packages/ui/components/shared/button.tsx | 79 + packages/ui/components/shared/calendar.tsx | 62 + packages/ui/components/shared/card.tsx | 86 + packages/ui/components/shared/checkbox.tsx | 28 + packages/ui/components/shared/command.tsx | 153 + packages/ui/components/shared/dialog.tsx | 120 + .../ui/components/shared/dropdown-menu.tsx | 198 + .../ui/components/shared/external-link.tsx | 25 + packages/ui/components/shared/input.tsx | 25 + packages/ui/components/shared/key-value.tsx | 29 + packages/ui/components/shared/label.tsx | 24 + packages/ui/components/shared/mode-toggle.tsx | 39 + .../ui/components/shared/navigation-menu.tsx | 128 + packages/ui/components/shared/popover.tsx | 29 + packages/ui/components/shared/react-icons.tsx | 5 + packages/ui/components/shared/select.tsx | 119 + packages/ui/components/shared/separator.tsx | 29 + packages/ui/components/shared/skeleton.tsx | 26 + packages/ui/components/shared/spinner.tsx | 11 + packages/ui/components/shared/table.tsx | 114 + packages/ui/components/shared/tabs.tsx | 53 + packages/ui/components/shared/textarea.tsx | 24 + packages/ui/components/shared/toast.tsx | 130 + packages/ui/components/shared/toaster.tsx | 33 + packages/ui/components/shared/tooltip.tsx | 28 + packages/ui/components/shared/use-toast.ts | 195 + packages/ui/hooks/useCopy.ts | 65 + packages/ui/hooks/useMediaQuery.ts | 17 + packages/ui/index.ts | 1 + packages/ui/lib/constants.ts | 12 + packages/ui/lib/enums/badge.ts | 7 + packages/ui/lib/enums/error.ts | 10 + packages/ui/lib/enums/network.ts | 20 + packages/ui/lib/enums/toast.ts | 7 + packages/ui/lib/enums/ui.ts | 4 + packages/ui/lib/interfaces/ui.ts | 41 + packages/ui/lib/interfaces/wallet.ts | 54 + packages/ui/lib/siteMetadata.ts | 16 + packages/ui/lib/types/common.ts | 39 + packages/ui/lib/types/ui.ts | 19 + packages/ui/lib/utils/index.ts | 105 + packages/ui/lib/utils/walletEvents.ts | 68 + packages/ui/lib/utils/walletUtils.ts | 56 + packages/ui/package.json | 57 + .../public/assets/images/black_logotype.png | Bin 0 -> 14589 bytes packages/ui/public/assets/images/clock.png | Bin 0 -> 10767 bytes packages/ui/public/assets/images/cover.png | Bin 0 -> 2950753 bytes packages/ui/public/assets/images/ten.svg | 8 + .../public/assets/images/white_logotype.png | Bin 0 -> 13581 bytes packages/ui/public/docs/privacy.json | 105 + packages/ui/public/docs/terms.json | 114 + packages/ui/public/favicon.ico | Bin 0 -> 1260 bytes .../public/favicon/android-chrome-192x192.png | Bin 0 -> 12554 bytes .../public/favicon/android-chrome-512x512.png | Bin 0 -> 42631 bytes .../ui/public/favicon/apple-touch-icon.png | Bin 0 -> 11948 bytes packages/ui/public/favicon/favicon-16x16.png | Bin 0 -> 739 bytes packages/ui/public/favicon/favicon-32x32.png | Bin 0 -> 1744 bytes packages/ui/public/favicon/favicon.ico | Bin 0 -> 15406 bytes packages/ui/public/favicon/site.webmanifest | 19 + packages/ui/routes/index.ts | 8 + packages/ui/routes/router.ts | 5 + packages/ui/services/walletService.ts | 192 + packages/ui/stores/wallet-store.ts | 33 + packages/ui/tsconfig.json | 8 + packages/ui/tsconfig.lint.json | 8 + packages/ui/turbo/generators/config.ts | 30 + .../turbo/generators/templates/component.hbs | 8 + pnpm-lock.yaml | 10052 ++++++++++++ pnpm-workspace.yaml | 4 + tools/bridge-frontend/package-lock.json | 13289 ++++++++++++++++ tsconfig.json | 3 + turbo.json | 9 + 124 files changed, 32540 insertions(+) create mode 100644 package.json create mode 100644 packages/README.md create mode 100644 packages/apis/.config/openapi-codegen.config.ts create mode 100644 packages/apis/.config/p2o.config.json create mode 100644 packages/apis/package.json create mode 100644 packages/apis/src/api/client.ts create mode 100644 packages/apis/src/api/codegen/digestComponents.ts create mode 100644 packages/apis/src/api/codegen/digestContext.ts create mode 100644 packages/apis/src/api/codegen/digestFetcher.ts create mode 100644 packages/apis/src/api/response-types.ts create mode 100644 packages/apis/src/libs/index.ts create mode 100644 packages/apis/src/libs/observables/auth.ts create mode 100644 packages/apis/storage/digest-collection.json create mode 100644 packages/apis/storage/digest-openapi.json create mode 100644 packages/apis/tsconfig.json create mode 100644 packages/eslint-config/README.md create mode 100644 packages/eslint-config/library.js create mode 100644 packages/eslint-config/next.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/eslint-config/react-internal.js create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/data.helpers.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/typescript-config/base.json create mode 100644 packages/typescript-config/nextjs.json create mode 100644 packages/typescript-config/package.json create mode 100644 packages/typescript-config/react-library.json create mode 100644 packages/ui/.eslintrc.js create mode 100644 packages/ui/api/general.ts create mode 100644 packages/ui/api/index.ts create mode 100644 packages/ui/components/common/404.tsx create mode 100644 packages/ui/components/common/500.tsx create mode 100644 packages/ui/components/common/connect-wallet.tsx create mode 100644 packages/ui/components/common/copy.tsx create mode 100644 packages/ui/components/common/data-table/data-table-column-header.tsx create mode 100644 packages/ui/components/common/data-table/data-table-faceted-filter.tsx create mode 100644 packages/ui/components/common/data-table/data-table-pagination.tsx create mode 100644 packages/ui/components/common/data-table/data-table-row-actions.tsx create mode 100644 packages/ui/components/common/data-table/data-table-toolbar.tsx create mode 100644 packages/ui/components/common/data-table/data-table-view-options.tsx create mode 100644 packages/ui/components/common/data-table/data-table.tsx create mode 100644 packages/ui/components/common/data-table/skeleton-loader.tsx create mode 100644 packages/ui/components/common/document.tsx create mode 100644 packages/ui/components/common/empty-state.tsx create mode 100644 packages/ui/components/common/error.tsx create mode 100644 packages/ui/components/common/loading-list.tsx create mode 100644 packages/ui/components/common/loading-state.tsx create mode 100644 packages/ui/components/common/network-status.tsx create mode 100644 packages/ui/components/common/truncated-address.tsx create mode 100644 packages/ui/components/shared/alert.tsx create mode 100644 packages/ui/components/shared/avatar.tsx create mode 100644 packages/ui/components/shared/badge.tsx create mode 100644 packages/ui/components/shared/button.tsx create mode 100644 packages/ui/components/shared/calendar.tsx create mode 100644 packages/ui/components/shared/card.tsx create mode 100644 packages/ui/components/shared/checkbox.tsx create mode 100644 packages/ui/components/shared/command.tsx create mode 100644 packages/ui/components/shared/dialog.tsx create mode 100644 packages/ui/components/shared/dropdown-menu.tsx create mode 100644 packages/ui/components/shared/external-link.tsx create mode 100644 packages/ui/components/shared/input.tsx create mode 100644 packages/ui/components/shared/key-value.tsx create mode 100644 packages/ui/components/shared/label.tsx create mode 100644 packages/ui/components/shared/mode-toggle.tsx create mode 100644 packages/ui/components/shared/navigation-menu.tsx create mode 100644 packages/ui/components/shared/popover.tsx create mode 100644 packages/ui/components/shared/react-icons.tsx create mode 100644 packages/ui/components/shared/select.tsx create mode 100644 packages/ui/components/shared/separator.tsx create mode 100644 packages/ui/components/shared/skeleton.tsx create mode 100644 packages/ui/components/shared/spinner.tsx create mode 100644 packages/ui/components/shared/table.tsx create mode 100644 packages/ui/components/shared/tabs.tsx create mode 100644 packages/ui/components/shared/textarea.tsx create mode 100644 packages/ui/components/shared/toast.tsx create mode 100644 packages/ui/components/shared/toaster.tsx create mode 100644 packages/ui/components/shared/tooltip.tsx create mode 100644 packages/ui/components/shared/use-toast.ts create mode 100644 packages/ui/hooks/useCopy.ts create mode 100644 packages/ui/hooks/useMediaQuery.ts create mode 100644 packages/ui/index.ts create mode 100644 packages/ui/lib/constants.ts create mode 100644 packages/ui/lib/enums/badge.ts create mode 100644 packages/ui/lib/enums/error.ts create mode 100644 packages/ui/lib/enums/network.ts create mode 100644 packages/ui/lib/enums/toast.ts create mode 100644 packages/ui/lib/enums/ui.ts create mode 100644 packages/ui/lib/interfaces/ui.ts create mode 100644 packages/ui/lib/interfaces/wallet.ts create mode 100644 packages/ui/lib/siteMetadata.ts create mode 100644 packages/ui/lib/types/common.ts create mode 100644 packages/ui/lib/types/ui.ts create mode 100644 packages/ui/lib/utils/index.ts create mode 100644 packages/ui/lib/utils/walletEvents.ts create mode 100644 packages/ui/lib/utils/walletUtils.ts create mode 100644 packages/ui/package.json create mode 100644 packages/ui/public/assets/images/black_logotype.png create mode 100644 packages/ui/public/assets/images/clock.png create mode 100644 packages/ui/public/assets/images/cover.png create mode 100644 packages/ui/public/assets/images/ten.svg create mode 100644 packages/ui/public/assets/images/white_logotype.png create mode 100644 packages/ui/public/docs/privacy.json create mode 100644 packages/ui/public/docs/terms.json create mode 100644 packages/ui/public/favicon.ico create mode 100644 packages/ui/public/favicon/android-chrome-192x192.png create mode 100644 packages/ui/public/favicon/android-chrome-512x512.png create mode 100644 packages/ui/public/favicon/apple-touch-icon.png create mode 100644 packages/ui/public/favicon/favicon-16x16.png create mode 100644 packages/ui/public/favicon/favicon-32x32.png create mode 100644 packages/ui/public/favicon/favicon.ico create mode 100644 packages/ui/public/favicon/site.webmanifest create mode 100644 packages/ui/routes/index.ts create mode 100644 packages/ui/routes/router.ts create mode 100644 packages/ui/services/walletService.ts create mode 100644 packages/ui/stores/wallet-store.ts create mode 100644 packages/ui/tsconfig.json create mode 100644 packages/ui/tsconfig.lint.json create mode 100644 packages/ui/turbo/generators/config.ts create mode 100644 packages/ui/turbo/generators/templates/component.hbs create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tools/bridge-frontend/package-lock.json create mode 100644 tsconfig.json create mode 100644 turbo.json diff --git a/package.json b/package.json new file mode 100644 index 0000000000..c31fa37d29 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "go-ten", + "version": "1.0.0", + "description": "go-ten", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ten-protocol/go-ten.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/ten-protocol/go-ten/issues" + }, + "homepage": "https://github.com/ten-protocol/go-ten#readme", + "dependencies": { + "turbo": "^1.13.3" + } +} diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000000..249f030219 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,72 @@ +# Monorepo Structure for TEN Frontend Projects + +This repository uses a monorepo setup to manage multiple frontend projects and shared resources such as components, utilities, and hooks. The structure enhances code reuse, maintainability, and collaboration across projects like **Tenscan**, **Gateway**, **Bridge**, and more. + +## Folder Structure + +```bash +📁 packages +├── 📁 apis - Server-side logic, API routes, and backend services +│ ├── 📁 .config - Configuration files for the APIs +│ ├── 📁 src - Source files for API logic +│ └── 📁 storage - Storage-related logic or utilities +├── 📁 eslint-config - Centralized ESLint configuration for all frontend projects +├── 📁 shared - Reusable components, hooks, and utilities shared across frontend apps +│ └── 📁 src - Main directory containing shared code +├── 📁 typescript-config - Centralized TypeScript configurations +│ ├── 📄 base.json - Base TypeScript configuration for general projects +│ ├── 📄 nextjs.json - Configuration specific to Next.js projects +│ └── 📄 react-library.json - Configuration for React libraries +└── 📁 ui - + ├── 📁 api - API logic consumed by the frontend + ├── 📁 components - Reusable React components used in the UI + ├── 📁 hooks - Custom hooks used across the frontend + ├── 📁 lib - Utility functions used across the frontend + ├── 📁 public - Static files such as images and assets + ├── 📁 services - External service interactions like APIs + ├── 📁 routes - Routing configuration and route-related logic + └── 📁 stores - Global state mgt +``` + +## Getting Started + +1. **Clone the Repository:** + + ```bash + git clone https://github.com/ten-protocol/go-ten.git + ``` + +2. **Install Dependencies:** + + ```bash + pnpm install + ``` + +3. **Navigate to the Project:** + + ```bash + Tenscan: cd tools/tenscan/frontend + Gateway: cd tools/walletextension/frontend + Bridge: cd tools/bridge-frontend + ``` + +4. **Run the Project:** + + ```bash + pnpm dev + ``` + + +## Built With + +- [Next.js](https://nextjs.org/) +- [Tailwind CSS](https://tailwindcss.com/) +- [TypeScript](https://www.typescriptlang.org/) + +## Contributing + +Contributions are welcome! Follow our [contribution guidelines](/docs/_docs/community/contributions.md). + +## License + +This project is licensed under the [GNU Affero General Public License v3.0](/LICENSE). \ No newline at end of file diff --git a/packages/apis/.config/openapi-codegen.config.ts b/packages/apis/.config/openapi-codegen.config.ts new file mode 100644 index 0000000000..c23d51bdab --- /dev/null +++ b/packages/apis/.config/openapi-codegen.config.ts @@ -0,0 +1,28 @@ +// @ts-expect-error +import { defineConfig } from "@openapi-codegen/cli"; +import { + generateReactQueryComponents, + generateSchemaTypes, +} from "@openapi-codegen/typescript"; + +export default defineConfig({ + digest: { + from: { + relativePath: "./storage/digest-openapi.json", + source: "file", + }, + outputDir: "./src/api/codegen", + // @ts-expect-error + to: async (context) => { + const filenamePrefix = "digest"; + + const { schemasFiles } = await generateSchemaTypes(context, { + filenamePrefix, + }); + return await generateReactQueryComponents(context, { + filenamePrefix, + schemasFiles, + }); + }, + }, +}); diff --git a/packages/apis/.config/p2o.config.json b/packages/apis/.config/p2o.config.json new file mode 100644 index 0000000000..fae75fdf03 --- /dev/null +++ b/packages/apis/.config/p2o.config.json @@ -0,0 +1,4 @@ +{ + "outputFormat": "json", + "operationId": "auto" +} diff --git a/packages/apis/package.json b/packages/apis/package.json new file mode 100644 index 0000000000..48c54d2460 --- /dev/null +++ b/packages/apis/package.json @@ -0,0 +1,28 @@ +{ + "name": "@repo/apis", + "version": "0.0.0", + "private": true, + "scripts": { + "sync": "npm run api:generate", + "api:generate": "pnpm run convert:collection && npm run generate:fetcher", + "generate:fetcher": "npx openapi-codegen -c .config/openapi-codegen.config.ts digest", + "convert:collection": "pnpm exec p2o ./storage/digest-collection.json -f ./storage/digest-openapi.json -o .config/p2o.config.json" + }, + "dependencies": { + "@repo/shared": "workspace:*", + "@repo/typescript-config": "workspace:*", + "destr": "^2.0.2", + "effect": "^2.4.1", + "ofetch": "1.3.3", + "ramda": "^0.29.1", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@openapi-codegen/cli": "^2.0.0", + "@openapi-codegen/typescript": "^8.0.0", + "@types/ramda": "^0.29.10", + "postman-to-openapi": "^3.0.1", + "typescript": "5.3.3", + "vite": "^5.0.11" + } +} diff --git a/packages/apis/src/api/client.ts b/packages/apis/src/api/client.ts new file mode 100644 index 0000000000..1100f8edb8 --- /dev/null +++ b/packages/apis/src/api/client.ts @@ -0,0 +1,166 @@ +import { safeStr } from "@repo/shared/src/data.helpers"; +import { Match } from "effect"; +import { FetchError } from "ofetch"; +import { has, identity, is } from "ramda"; +import { + ApiError, + ApiErrors, + Okay, + ResponseType, + SafeRes, + UnknownError, +} from "./response-types"; + +export const resolveUrl = ( + url: string, + queryParams: Record = {}, + pathParams: Record = {}, +) => { + let query = new URLSearchParams(queryParams).toString(); + if (query) query = `?${query}`; + + return url.replace(/\{\w*\}/g, (key) => pathParams[key.slice(1, -1)]) + query; +}; + +export async function safeFetchResponse( + promise: Promise, +): Promise> { + try { + const value = await promise; + return resolveFetchResponse(value); + } catch (err: unknown) { + return resolveFetchError(err); + } +} + +export const resolveFetchResponse = (response: T): SafeRes => { + const hasError = has("error", response) && response.error === true; + const hasMessage = has("msg", response); + + const messageIsStr = hasMessage && typeof response?.msg === "string"; + + if (UnacceptedError.validate(response)) { + return UnacceptedError.respond(response); + } + + if (Array.isArray(response)) return Okay(response); + + if (hasError && hasMessage && is(Object, response?.msg)) + // @ts-expect-error Still trying to response structure + return ValidationError(response?.msg); + + if (hasError && hasMessage && messageIsStr) + return ApiError({ + message: response.msg as string, + + error: response.error, + }); + + if (response === undefined || response === null) { + return UnknownError({ + message: "Something went wrong", + value: response, + }); + } + + return Okay(response); +}; + +// biome-ignore lint/suspicious/noExplicitAny: Error type must be any +export function resolveFetchError(error: any): ApiErrors { + if ( + ["Network Error", "NetworkError"].some((str) => + safeStr(error?.message).includes(str), + ) + ) { + return ApiError({ + message: "Seems like you're offline. Please check your network", + }); + } + + return handleErrorByType(error); +} + +const handleErrorByType = ResponseType.pipe( + Match.tag("ApiError", identity), + Match.tag("ValidationError", identity), + Match.tag("UnknownError", identity), + Match.orElse((err) => handleRandomError(err)), +); + +function handleRandomError(error: unknown) { + const err = error as FetchError; + if (err instanceof FetchError) { + const { status = 400 } = guessRequestError(err); + const reason_for_failure = err?.data?.message || err.message; + + if (status === 404) + return ApiError({ message: "404: Resource not found", error: err }); + + if (401 === status) { + return ApiError({ + message: "Unable to process request. You're unauthorized", + error: err, + }); + } + + if (403 === status) { + return ApiError({ + message: "You do not have permission to perform this operation", + error: err, + }); + } + + if (status >= 400 && status < 500) + return ApiError({ message: reason_for_failure, error: err }); + + if (status >= 500) + return ApiError({ message: reason_for_failure, error: err }); + } + + const err_msg = has("message", error) ? safeStr(error.message) : ""; + + if (err_msg.includes("")) { + return ApiError({ + message: "Something new wrong. Server didn't respond", + }); + } + + return ApiError({ message: err_msg }); + + function guessRequestError(err: FetchError) { + return err instanceof FetchError + ? err + : { status: 0, statusText: "UNKNOWN" }; + } +} + +export type ResponseResolver = { + validate: (data: unknown) => boolean; + respond: (data: T) => ApiErrors; + interceptResponse: (data: T) => T; +}; + +export const UnacceptedError: ResponseResolver = { + validate(response: unknown) { + return ( + has("accepted", response) && + has("message", response) && + response.accepted === false + ); + }, + + respond(response: unknown) { + return ApiError({ + // @ts-expect-error + message: response?.message ?? "Unknown Error...", + error: response, + }); + }, + + interceptResponse(response) { + if (!this.validate(response)) return response; + + throw this.respond(response); + }, +}; diff --git a/packages/apis/src/api/codegen/digestComponents.ts b/packages/apis/src/api/codegen/digestComponents.ts new file mode 100644 index 0000000000..d4eea5f3f6 --- /dev/null +++ b/packages/apis/src/api/codegen/digestComponents.ts @@ -0,0 +1,1078 @@ +/** + * Generated by @openapi-codegen + * + * @version 1.0.0 + */ +import * as reactQuery from "@tanstack/react-query"; +import { useDigestContext, DigestContext } from "./digestContext"; +import type * as Fetcher from "./digestFetcher"; +import { digestFetch } from "./digestFetcher"; + +export type AllTeamMembersError = Fetcher.ErrorWrapper; + +export type AllTeamMembersVariables = DigestContext["fetcherOptions"]; + +export const fetchAllTeamMembers = ( + variables: AllTeamMembersVariables, + signal?: AbortSignal +) => + digestFetch, AllTeamMembersError, undefined, {}, {}, {}>({ + url: "/org/team", + method: "get", + ...variables, + signal, + }); + +export const useAllTeamMembers = >( + variables: AllTeamMembersVariables, + options?: Omit< + reactQuery.UseQueryOptions, AllTeamMembersError, TData>, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery, AllTeamMembersError, TData>({ + queryKey: queryKeyFn({ + path: "/org/team", + operationId: "allTeamMembers", + variables, + }), + queryFn: ({ signal }) => + fetchAllTeamMembers({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type DeleteTeamMemberError = Fetcher.ErrorWrapper; + +export type DeleteTeamMemberVariables = DigestContext["fetcherOptions"]; + +export const fetchDeleteTeamMember = ( + variables: DeleteTeamMemberVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/org/team/c718b157-4fe4-4bb1-adc4-787e95b239a2", + method: "delete", + ...variables, + signal, + }); + +export const useDeleteTeamMember = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + DeleteTeamMemberError, + DeleteTeamMemberVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + undefined, + DeleteTeamMemberError, + DeleteTeamMemberVariables + >({ + mutationFn: (variables: DeleteTeamMemberVariables) => + fetchDeleteTeamMember({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type LoginOrgError = Fetcher.ErrorWrapper; + +export type LoginOrgVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchLoginOrg = ( + variables: LoginOrgVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + LoginOrgError, + Record, + {}, + {}, + {} + >({ url: "/org/auth/login", method: "post", ...variables, signal }); + +export const useLoginOrg = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + LoginOrgError, + LoginOrgVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + LoginOrgError, + LoginOrgVariables + >({ + mutationFn: (variables: LoginOrgVariables) => + fetchLoginOrg({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type LogoutOrgError = Fetcher.ErrorWrapper; + +export type LogoutOrgVariables = DigestContext["fetcherOptions"]; + +export const fetchLogoutOrg = ( + variables: LogoutOrgVariables, + signal?: AbortSignal +) => + digestFetch, LogoutOrgError, undefined, {}, {}, {}>({ + url: "/org/auth/logout", + method: "post", + ...variables, + signal, + }); + +export const useLogoutOrg = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + LogoutOrgError, + LogoutOrgVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + LogoutOrgError, + LogoutOrgVariables + >({ + mutationFn: (variables: LogoutOrgVariables) => + fetchLogoutOrg({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type ProfileError = Fetcher.ErrorWrapper; + +export type ProfileVariables = DigestContext["fetcherOptions"]; + +export const fetchProfile = ( + variables: ProfileVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/org/profile", + method: "get", + ...variables, + signal, + }); + +export const useProfile = ( + variables: ProfileVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/org/profile", + operationId: "profile", + variables, + }), + queryFn: ({ signal }) => + fetchProfile({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type GetMetricsError = Fetcher.ErrorWrapper; + +export type GetMetricsVariables = DigestContext["fetcherOptions"]; + +export const fetchGetMetrics = ( + variables: GetMetricsVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/metrics", + method: "get", + ...variables, + signal, + }); + +export const useGetMetrics = ( + variables: GetMetricsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/metrics", + operationId: "getMetrics", + variables, + }), + queryFn: ({ signal }) => + fetchGetMetrics({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type CustomerGrowthMetricsQueryParams = { + /** + * 1wk, 1mth, 1y, all. default: 1mth + */ + range?: string; +}; + +export type CustomerGrowthMetricsError = Fetcher.ErrorWrapper; + +export type CustomerGrowthMetricsVariables = { + queryParams?: CustomerGrowthMetricsQueryParams; +} & DigestContext["fetcherOptions"]; + +export const fetchCustomerGrowthMetrics = ( + variables: CustomerGrowthMetricsVariables, + signal?: AbortSignal +) => + digestFetch< + undefined, + CustomerGrowthMetricsError, + undefined, + {}, + CustomerGrowthMetricsQueryParams, + {} + >({ url: "/kpi/customers", method: "get", ...variables, signal }); + +export const useCustomerGrowthMetrics = ( + variables: CustomerGrowthMetricsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/kpi/customers", + operationId: "customerGrowthMetrics", + variables, + }), + queryFn: ({ signal }) => + fetchCustomerGrowthMetrics({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type CreateAMealCategoryError = Fetcher.ErrorWrapper; + +export type CreateAMealCategoryVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchCreateAMealCategory = ( + variables: CreateAMealCategoryVariables, + signal?: AbortSignal +) => + digestFetch< + undefined, + CreateAMealCategoryError, + Record, + {}, + {}, + {} + >({ url: "/categories/meals", method: "post", ...variables, signal }); + +export const useCreateAMealCategory = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + CreateAMealCategoryError, + CreateAMealCategoryVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + undefined, + CreateAMealCategoryError, + CreateAMealCategoryVariables + >({ + mutationFn: (variables: CreateAMealCategoryVariables) => + fetchCreateAMealCategory({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type GetAllCategoriesError = Fetcher.ErrorWrapper; + +export type GetAllCategoriesVariables = DigestContext["fetcherOptions"]; + +export const fetchGetAllCategories = ( + variables: GetAllCategoriesVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/categories/meals", + method: "get", + ...variables, + signal, + }); + +export const useGetAllCategories = ( + variables: GetAllCategoriesVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/categories/meals", + operationId: "getAllCategories", + variables, + }), + queryFn: ({ signal }) => + fetchGetAllCategories({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type DeleteCategoryError = Fetcher.ErrorWrapper; + +export type DeleteCategoryVariables = DigestContext["fetcherOptions"]; + +export const fetchDeleteCategory = ( + variables: DeleteCategoryVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/categories/05fa9976-2366-479b-8854-21727fb78152", + method: "delete", + ...variables, + signal, + }); + +export const useDeleteCategory = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + DeleteCategoryError, + DeleteCategoryVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + undefined, + DeleteCategoryError, + DeleteCategoryVariables + >({ + mutationFn: (variables: DeleteCategoryVariables) => + fetchDeleteCategory({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type GetIngredientsQueryParams = { + page?: number; + limit?: number; +}; + +export type GetIngredientsError = Fetcher.ErrorWrapper; + +export type GetIngredientsVariables = { + queryParams?: GetIngredientsQueryParams; +} & DigestContext["fetcherOptions"]; + +export const fetchGetIngredients = ( + variables: GetIngredientsVariables, + signal?: AbortSignal +) => + digestFetch< + undefined, + GetIngredientsError, + undefined, + {}, + GetIngredientsQueryParams, + {} + >({ url: "/ingredients", method: "get", ...variables, signal }); + +export const useGetIngredients = ( + variables: GetIngredientsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/ingredients", + operationId: "getIngredients", + variables, + }), + queryFn: ({ signal }) => + fetchGetIngredients({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type CreateMeasurementError = Fetcher.ErrorWrapper; + +export type CreateMeasurementVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchCreateMeasurement = ( + variables: CreateMeasurementVariables, + signal?: AbortSignal +) => + digestFetch< + undefined, + CreateMeasurementError, + Record, + {}, + {}, + {} + >({ url: "/ingredients", method: "post", ...variables, signal }); + +export const useCreateMeasurement = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + CreateMeasurementError, + CreateMeasurementVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + undefined, + CreateMeasurementError, + CreateMeasurementVariables + >({ + mutationFn: (variables: CreateMeasurementVariables) => + fetchCreateMeasurement({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type CreateMealError = Fetcher.ErrorWrapper; + +export type CreateMealVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchCreateMeal = ( + variables: CreateMealVariables, + signal?: AbortSignal +) => + digestFetch, {}, {}, {}>({ + url: "/dishes", + method: "post", + ...variables, + signal, + }); + +export const useCreateMeal = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + CreateMealError, + CreateMealVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + undefined, + CreateMealError, + CreateMealVariables + >({ + mutationFn: (variables: CreateMealVariables) => + fetchCreateMeal({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type GetMealsError = Fetcher.ErrorWrapper; + +export type GetMealsVariables = DigestContext["fetcherOptions"]; + +export const fetchGetMeals = ( + variables: GetMealsVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/dishes", + method: "get", + ...variables, + signal, + }); + +export const useGetMeals = ( + variables: GetMealsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/dishes", + operationId: "getMeals", + variables, + }), + queryFn: ({ signal }) => + fetchGetMeals({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type CreateARecipeError = Fetcher.ErrorWrapper; + +export type CreateARecipeVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchCreateARecipe = ( + variables: CreateARecipeVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + CreateARecipeError, + Record, + {}, + {}, + {} + >({ url: "/recipes", method: "post", ...variables, signal }); + +export const useCreateARecipe = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + CreateARecipeError, + CreateARecipeVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + CreateARecipeError, + CreateARecipeVariables + >({ + mutationFn: (variables: CreateARecipeVariables) => + fetchCreateARecipe({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type AllRecipesQueryParams = { + limit?: number; + page?: number; +}; + +export type AllRecipesError = Fetcher.ErrorWrapper; + +export type AllRecipesVariables = { + queryParams?: AllRecipesQueryParams; +} & DigestContext["fetcherOptions"]; + +export const fetchAllRecipes = ( + variables: AllRecipesVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + AllRecipesError, + undefined, + {}, + AllRecipesQueryParams, + {} + >({ url: "/recipes", method: "get", ...variables, signal }); + +export const useAllRecipes = >( + variables: AllRecipesVariables, + options?: Omit< + reactQuery.UseQueryOptions, AllRecipesError, TData>, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery, AllRecipesError, TData>({ + queryKey: queryKeyFn({ + path: "/recipes", + operationId: "allRecipes", + variables, + }), + queryFn: ({ signal }) => + fetchAllRecipes({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type GetARecipePathParams = { + recipeId: string; +}; + +export type GetARecipeError = Fetcher.ErrorWrapper; + +export type GetARecipeVariables = { + pathParams: GetARecipePathParams; +} & DigestContext["fetcherOptions"]; + +export const fetchGetARecipe = ( + variables: GetARecipeVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + GetARecipeError, + undefined, + {}, + {}, + GetARecipePathParams + >({ url: "/recipes/{recipeId}", method: "get", ...variables, signal }); + +export const useGetARecipe = >( + variables: GetARecipeVariables, + options?: Omit< + reactQuery.UseQueryOptions, GetARecipeError, TData>, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery, GetARecipeError, TData>({ + queryKey: queryKeyFn({ + path: "/recipes/{recipeId}", + operationId: "getARecipe", + variables, + }), + queryFn: ({ signal }) => + fetchGetARecipe({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type GetQuestionsError = Fetcher.ErrorWrapper; + +export type GetQuestionsVariables = DigestContext["fetcherOptions"]; + +export const fetchGetQuestions = ( + variables: GetQuestionsVariables, + signal?: AbortSignal +) => + digestFetch, GetQuestionsError, undefined, {}, {}, {}>({ + url: "/org/onboarding/questions", + method: "get", + ...variables, + signal, + }); + +export const useGetQuestions = >( + variables: GetQuestionsVariables, + options?: Omit< + reactQuery.UseQueryOptions, GetQuestionsError, TData>, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery, GetQuestionsError, TData>({ + queryKey: queryKeyFn({ + path: "/org/onboarding/questions", + operationId: "getQuestions", + variables, + }), + queryFn: ({ signal }) => + fetchGetQuestions({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type CreateQuestionError = Fetcher.ErrorWrapper; + +export type CreateQuestionVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchCreateQuestion = ( + variables: CreateQuestionVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + CreateQuestionError, + Record, + {}, + {}, + {} + >({ url: "/org/onboarding/questions", method: "post", ...variables, signal }); + +export const useCreateQuestion = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + CreateQuestionError, + CreateQuestionVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + CreateQuestionError, + CreateQuestionVariables + >({ + mutationFn: (variables: CreateQuestionVariables) => + fetchCreateQuestion({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type UpdateQuestionPathParams = { + questionId: string; +}; + +export type UpdateQuestionError = Fetcher.ErrorWrapper; + +export type UpdateQuestionVariables = { + body?: Record; + pathParams: UpdateQuestionPathParams; +} & DigestContext["fetcherOptions"]; + +export const fetchUpdateQuestion = ( + variables: UpdateQuestionVariables, + signal?: AbortSignal +) => + digestFetch< + Record, + UpdateQuestionError, + Record, + {}, + {}, + UpdateQuestionPathParams + >({ + url: "/org/onboarding/questions/{questionId}", + method: "post", + ...variables, + signal, + }); + +export const useUpdateQuestion = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + UpdateQuestionError, + UpdateQuestionVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + UpdateQuestionError, + UpdateQuestionVariables + >({ + mutationFn: (variables: UpdateQuestionVariables) => + fetchUpdateQuestion({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type GetUsersQueryParams = { + page?: number; + limit?: number; + range_created_at?: string; +}; + +export type GetUsersError = Fetcher.ErrorWrapper; + +export type GetUsersVariables = { + queryParams?: GetUsersQueryParams; +} & DigestContext["fetcherOptions"]; + +export const fetchGetUsers = ( + variables: GetUsersVariables, + signal?: AbortSignal +) => + digestFetch( + { url: "/users", method: "get", ...variables, signal } + ); + +export const useGetUsers = ( + variables: GetUsersVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/users", + operationId: "getUsers", + variables, + }), + queryFn: ({ signal }) => + fetchGetUsers({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type PaymentsQueryParams = { + /** + * (optional) + */ + user_id?: string; +}; + +export type PaymentsError = Fetcher.ErrorWrapper; + +export type PaymentsVariables = { + queryParams?: PaymentsQueryParams; +} & DigestContext["fetcherOptions"]; + +export const fetchPayments = ( + variables: PaymentsVariables, + signal?: AbortSignal +) => + digestFetch( + { url: "/payments", method: "get", ...variables, signal } + ); + +export const usePayments = ( + variables: PaymentsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/payments", + operationId: "payments", + variables, + }), + queryFn: ({ signal }) => + fetchPayments({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type LoginError = Fetcher.ErrorWrapper; + +export type LoginVariables = { + body?: Record; +} & DigestContext["fetcherOptions"]; + +export const fetchLogin = (variables: LoginVariables, signal?: AbortSignal) => + digestFetch, LoginError, Record, {}, {}, {}>( + { url: "/auth/login", method: "post", ...variables, signal } + ); + +export const useLogin = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + LoginError, + LoginVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + LoginError, + LoginVariables + >({ + mutationFn: (variables: LoginVariables) => + fetchLogin({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type LogoutError = Fetcher.ErrorWrapper; + +export type LogoutVariables = DigestContext["fetcherOptions"]; + +export const fetchLogout = (variables: LogoutVariables, signal?: AbortSignal) => + digestFetch, LogoutError, undefined, {}, {}, {}>({ + url: "/auth/logout", + method: "post", + ...variables, + signal, + }); + +export const useLogout = ( + options?: Omit< + reactQuery.UseMutationOptions< + Record, + LogoutError, + LogoutVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useDigestContext(); + return reactQuery.useMutation< + Record, + LogoutError, + LogoutVariables + >({ + mutationFn: (variables: LogoutVariables) => + fetchLogout({ ...fetcherOptions, ...variables }), + ...options, + }); +}; + +export type OnboardingQuestionsError = Fetcher.ErrorWrapper; + +export type OnboardingQuestionsVariables = DigestContext["fetcherOptions"]; + +export const fetchOnboardingQuestions = ( + variables: OnboardingQuestionsVariables, + signal?: AbortSignal +) => + digestFetch({ + url: "/onboarding/questions", + method: "get", + ...variables, + signal, + }); + +export const useOnboardingQuestions = ( + variables: OnboardingQuestionsVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" | "initialData" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = + useDigestContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({ + path: "/onboarding/questions", + operationId: "onboardingQuestions", + variables, + }), + queryFn: ({ signal }) => + fetchOnboardingQuestions({ ...fetcherOptions, ...variables }, signal), + ...options, + ...queryOptions, + }); +}; + +export type QueryOperation = + | { + path: "/org/team"; + operationId: "allTeamMembers"; + variables: AllTeamMembersVariables; + } + | { + path: "/org/profile"; + operationId: "profile"; + variables: ProfileVariables; + } + | { + path: "/metrics"; + operationId: "getMetrics"; + variables: GetMetricsVariables; + } + | { + path: "/kpi/customers"; + operationId: "customerGrowthMetrics"; + variables: CustomerGrowthMetricsVariables; + } + | { + path: "/categories/meals"; + operationId: "getAllCategories"; + variables: GetAllCategoriesVariables; + } + | { + path: "/ingredients"; + operationId: "getIngredients"; + variables: GetIngredientsVariables; + } + | { + path: "/dishes"; + operationId: "getMeals"; + variables: GetMealsVariables; + } + | { + path: "/recipes"; + operationId: "allRecipes"; + variables: AllRecipesVariables; + } + | { + path: "/recipes/{recipeId}"; + operationId: "getARecipe"; + variables: GetARecipeVariables; + } + | { + path: "/org/onboarding/questions"; + operationId: "getQuestions"; + variables: GetQuestionsVariables; + } + | { + path: "/users"; + operationId: "getUsers"; + variables: GetUsersVariables; + } + | { + path: "/payments"; + operationId: "payments"; + variables: PaymentsVariables; + } + | { + path: "/onboarding/questions"; + operationId: "onboardingQuestions"; + variables: OnboardingQuestionsVariables; + }; diff --git a/packages/apis/src/api/codegen/digestContext.ts b/packages/apis/src/api/codegen/digestContext.ts new file mode 100644 index 0000000000..b5818d0cb3 --- /dev/null +++ b/packages/apis/src/api/codegen/digestContext.ts @@ -0,0 +1,99 @@ +import type { QueryKey, UseQueryOptions } from "@tanstack/react-query"; +import { QueryOperation } from "./digestComponents"; + +export type DigestContext = { + fetcherOptions: { + /** + * Headers to inject in the fetcher + */ + headers?: {}; + /** + * Query params to inject in the fetcher + */ + queryParams?: {}; + }; + queryOptions: { + /** + * Set this to `false` to disable automatic refetching when the query mounts or changes query keys. + * Defaults to `true`. + */ + enabled?: boolean; + }; + /** + * Query key manager. + */ + queryKeyFn: (operation: QueryOperation) => QueryKey; +}; + +/** + * Context injected into every react-query hook wrappers + * + * @param queryOptions options from the useQuery wrapper + */ +export function useDigestContext< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + _queryOptions?: Omit< + UseQueryOptions, + "queryKey" | "queryFn" + >, +): DigestContext { + return { + fetcherOptions: {}, + queryOptions: {}, + queryKeyFn, + }; +} + +export const queryKeyFn = (operation: QueryOperation) => { + const queryKey: unknown[] = hasPathParams(operation) + ? operation.path + .split("/") + .filter(Boolean) + .map((i) => resolvePathParam(i, operation.variables.pathParams)) + : operation.path.split("/").filter(Boolean); + + if (hasQueryParams(operation)) { + queryKey.push(operation.variables.queryParams); + } + + if (hasBody(operation)) { + queryKey.push(operation.variables.body); + } + + return queryKey; +}; +// Helpers +const resolvePathParam = (key: string, pathParams: Record) => { + if (key.startsWith("{") && key.endsWith("}")) { + return pathParams[key.slice(1, -1)]; + } + return key; +}; + +const hasPathParams = ( + operation: QueryOperation, +): operation is QueryOperation & { + variables: { pathParams: Record }; +} => { + return Boolean((operation.variables as any).pathParams); +}; + +const hasBody = ( + operation: QueryOperation, +): operation is QueryOperation & { + variables: { body: Record }; +} => { + return Boolean((operation.variables as any).body); +}; + +const hasQueryParams = ( + operation: QueryOperation, +): operation is QueryOperation & { + variables: { queryParams: Record }; +} => { + return Boolean((operation.variables as any).queryParams); +}; diff --git a/packages/apis/src/api/codegen/digestFetcher.ts b/packages/apis/src/api/codegen/digestFetcher.ts new file mode 100644 index 0000000000..c432c87f52 --- /dev/null +++ b/packages/apis/src/api/codegen/digestFetcher.ts @@ -0,0 +1,94 @@ +import { resolveFetchError, UnacceptedError, resolveUrl } from "../client"; +import { FetchError, FetchOptions, ofetch } from "ofetch"; +import { authSubject } from "../../libs/observables/auth"; +import { destr } from "destr"; +import { ApiError } from "../response-types"; +import { DigestContext } from "./digestContext"; + +export type ErrorWrapper = + | TError + | { status: "unknown"; payload: string }; + +export type DigestFetcherOptions = { + url: string; + method: string; + body?: TBody; + headers?: THeaders; + queryParams?: TQueryParams; + pathParams?: TPathParams; + signal?: AbortSignal; +} & DigestContext["fetcherOptions"]; + +export async function digestFetch< + TData, + TError, + TBody extends Record | FormData | undefined | null, + THeaders extends {}, + TQueryParams extends {}, + TPathParams extends {}, +>( + params: DigestFetcherOptions, +): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + ...params.headers, + }; + + try { + return UnacceptedError.interceptResponse( + await baseFetcherFn({ + ...params, + query: params.queryParams, + headers, + url: resolveUrl(params.url, params.queryParams, params.pathParams), + }), + ); + } catch (error) { + return Promise.reject(resolveFetchError(error)); + } +} + +export async function baseFetcherFn( + params: { url: string } & FetchOptions, +): Promise { + try { + const { url, ...rest } = params; + const request = api(url, rest as FetchOptions<"json">); + + // @ts-expect-error + return Promise.race([ + new Promise((_, rej) => + setTimeout( + () => rej(ApiError({ message: "Request Timed out" })), + 30000, + ), + ), + request, + ]); + } catch (err) { + const xhrErr = err as FetchError; + if (xhrErr instanceof FetchError) { + if (xhrErr?.response?.status === 401) { + authSubject.next({ type: "unauthorized" }); + } + } + throw err; + } +} + +function resolveEnvVariable( + record: Record, + env_variable: string, +): string { + if (env_variable in record) { + return String(record[env_variable]); + } + return "--"; +} + +const url = resolveEnvVariable(import.meta.env || process.env, "VITE_API_URL"); + +const api = ofetch.create({ + baseURL: url, + parseResponse: destr, +}); diff --git a/packages/apis/src/api/response-types.ts b/packages/apis/src/api/response-types.ts new file mode 100644 index 0000000000..d0aa8ce394 --- /dev/null +++ b/packages/apis/src/api/response-types.ts @@ -0,0 +1,38 @@ +import { Data, Match } from "effect"; + +interface ApiError { + readonly _tag: "ApiError"; // the tag + message: string; + error?: unknown; +} + +interface ValidationError { + readonly _tag: "ValidationError"; // the tag + messages: Record; +} + +interface UnknownError { + readonly _tag: "UnknownError"; // the tag + message: string; + value: unknown; +} + +export interface Okay { + readonly _tag: "Okay"; // the tag + value: T; +} + +export const ApiError = Data.tagged("ApiError"); +export const ValidationError = Data.tagged("ValidationError"); +export const UnknownError = Data.tagged("UnknownError"); + +export const Okay = (data: T) => { + const Constructor = Data.case>(); + return Constructor({ _tag: "Okay", value: data }); +}; + +export type ApiErrors = ValidationError | ApiError | UnknownError; + +export type SafeRes = ApiErrors | Okay; + +export const ResponseType = Match.type>(); diff --git a/packages/apis/src/libs/index.ts b/packages/apis/src/libs/index.ts new file mode 100644 index 0000000000..fb8660726b --- /dev/null +++ b/packages/apis/src/libs/index.ts @@ -0,0 +1,18 @@ +import { destr } from "destr"; +import { is } from "ramda"; + +type AuthCredentials = { + id: string; + accessToken: string; + refreshToken: string; + expiresIn: number; + tokenType: string; +}; + +export function getAuthData(): AuthCredentials | undefined { + const auth_data = destr(localStorage.getItem("authentication")); + + if (!is(Object, auth_data)) return undefined; + + return auth_data; +} diff --git a/packages/apis/src/libs/observables/auth.ts b/packages/apis/src/libs/observables/auth.ts new file mode 100644 index 0000000000..8ea240789c --- /dev/null +++ b/packages/apis/src/libs/observables/auth.ts @@ -0,0 +1,5 @@ +import { Subject } from "rxjs"; + +type AuthEvents = { type: "unauthorized" }; + +export const authSubject = new Subject(); diff --git a/packages/apis/storage/digest-collection.json b/packages/apis/storage/digest-collection.json new file mode 100644 index 0000000000..217409b10f --- /dev/null +++ b/packages/apis/storage/digest-collection.json @@ -0,0 +1,1287 @@ +{ + "info": { + "_postman_id": "3008e2f7-16cb-4ab1-8822-4cded1fc9991", + "name": "Digest", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "2691360" + }, + "item": [ + { + "name": "Org", + "item": [ + { + "name": "Team", + "item": [ + { + "name": "All Team members", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/team", + "host": ["{{baseUrl}}"], + "path": ["org", "team"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3006/api/v1/org/team", + "protocol": "http", + "host": ["localhost"], + "port": "3006", + "path": ["api", "v1", "org", "team"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Thu, 11 Apr 2024 04:24:28 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "502" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"id\": \"81c7e4a4-19cf-48a7-bd02-f07fd239e8c4\",\n \"first_name\": \"Joseph\",\n \"password\": \"$argon2id$v=19$m=19456,t=2,p=1$4KmqjtSTFuUANlxHA+4Zxg$RXVfFWxe3Md4ZxtdMaz/lWRwsF9eM1eoWzgOerynlPM\",\n \"last_name\": \"Owonvwon\",\n \"email\": \"joseph.owonwo@gmail.com\",\n \"role\": \"OWNER\",\n \"created_at\": \"2024-04-11T01:52:52.330Z\",\n \"updated_at\": null,\n \"deleted_at\": null\n }\n ],\n \"meta\": {\n \"current_page\": 1,\n \"per_page\": 25,\n \"total\": 1\n }\n}" + } + ] + }, + { + "name": "Delete Team member", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/team/c718b157-4fe4-4bb1-adc4-787e95b239a2", + "host": ["{{baseUrl}}"], + "path": [ + "org", + "team", + "c718b157-4fe4-4bb1-adc4-787e95b239a2" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Auth", + "item": [ + { + "name": "Login - Org", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json()", + "", + "if (response?.data?.access_token)", + " pm.environment.set(\"accessToken\", response.data.access_token);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"joseph.owonwo@gmail.com\",\n \"password\": \"helloman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/auth/login", + "host": ["{{baseUrl}}"], + "path": ["org", "auth", "login"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"joseph.owonwo@gmail.com\",\n \"password\": \"helloman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:3006/api/v1/auth/login", + "protocol": "http", + "host": ["localhost"], + "port": "3006", + "path": ["api", "v1", "auth", "login"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Mon, 08 Apr 2024 03:18:00 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "158" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Login successful\",\n \"data\": {\n \"access_token\": \"6wfgh44amvxok4twm08v7cimg5k3065ay1oklioh\",\n \"expires\": \"2024-05-08T03:18:00.035Z\"\n }\n}" + } + ] + }, + { + "name": "Logout - Org", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/auth/logout", + "host": ["{{baseUrl}}"], + "path": ["org", "auth", "logout"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:3006/api/v1/auth/logout", + "protocol": "http", + "host": ["localhost"], + "port": "3006", + "path": ["api", "v1", "auth", "logout"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Wed, 10 Apr 2024 01:38:00 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "37" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Session terminated\"\n}" + } + ] + }, + { + "name": "Profile", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/profile", + "host": ["{{baseUrl}}"], + "path": ["org", "profile"] + } + }, + "response": [] + } + ] + }, + { + "name": "Metrics", + "item": [ + { + "name": "Get Metrics", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/metrics", + "host": ["{{baseUrl}}"], + "path": ["metrics"] + } + }, + "response": [] + }, + { + "name": "Customer Growth Metrics", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/kpi/customers?range=1mth", + "host": ["{{baseUrl}}"], + "path": ["kpi", "customers"], + "query": [ + { + "key": "range", + "value": "1mth", + "description": "1wk, 1mth, 1y, all. default: 1mth" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Categories", + "item": [ + { + "name": "Create a Meal Category", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Pastries\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/categories/meals", + "host": ["{{baseUrl}}"], + "path": ["categories", "meals"] + } + }, + "response": [] + }, + { + "name": "Get all categories", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Pastries\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/categories/meals", + "host": ["{{baseUrl}}"], + "path": ["categories", "meals"] + } + }, + "response": [] + }, + { + "name": "Delete Category", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/categories/05fa9976-2366-479b-8854-21727fb78152", + "host": ["{{baseUrl}}"], + "path": ["categories", "05fa9976-2366-479b-8854-21727fb78152"] + } + }, + "response": [] + } + ] + }, + { + "name": "Ingredients", + "item": [ + { + "name": "Get Ingredients", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/ingredients?page=1&limit=12", + "host": ["{{baseUrl}}"], + "path": ["ingredients"], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "12" + } + ] + } + }, + "response": [] + }, + { + "name": "Create Ingredients", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Vanilla Extract\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/ingredients", + "host": ["{{baseUrl}}"], + "path": ["ingredients"] + } + }, + "response": [] + }, + { + "name": "Create Measurement", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Vanilla Extract\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/ingredients", + "host": ["{{baseUrl}}"], + "path": ["ingredients"] + } + }, + "response": [] + } + ] + }, + { + "name": "Dishes", + "item": [ + { + "name": "Create Meal", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Peppered snail and crab shell\",\n \"description\": \"{{$randomLoremParagraph}}\",\n \"status\": \"DRAFT\",\n \"category\": [4],\n \"updated_at\": \"{{$isoTimestamp}}\",\n \"created_at\": \"{{$isoTimestamp}}\",\n \"video_url\": \"https://youtube.com/watch?q=ve9320a\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/dishes", + "host": ["{{baseUrl}}"], + "path": ["dishes"] + } + }, + "response": [] + }, + { + "name": "Get Meals", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Peppered snail and crab shell\",\n \"description\": \"{{$randomLoremParagraph}}\",\n \"status\": \"DRAFT\",\n \"category\": [4],\n \"updated_at\": \"{{$isoTimestamp}}\",\n \"created_at\": \"{{$isoTimestamp}}\",\n \"video_url\": \"https://youtube.com/watch?q=ve9320a\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/dishes", + "host": ["{{baseUrl}}"], + "path": ["dishes"] + } + }, + "response": [] + } + ] + }, + { + "name": "Recipes", + "item": [ + { + "name": "Create a Recipe", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"meal_id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"serving\": 2,\n \"cooking_time\": 120, // 2 hours. base unit is `seconds`\n \"ingredients\": [\n {\n \"measurement_id\": 1,\n \"ingredient_id\": 1,\n \"quantity\": 0.5\n }\n ],\n \"steps\": [\n {\n \"order\": 1,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n },\n {\n \"order\": 2,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/recipes", + "host": ["{{baseUrl}}"], + "path": ["recipes"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"meal_id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"serving\": 2,\n \"cooking_time\": 120, // 2 hours. base unit is `seconds`\n \"ingredients\": [\n {\n \"measurement_id\": 1,\n \"ingredient_id\": 1,\n \"quantity\": 0.5\n }\n ],\n \"steps\": [\n {\n \"order\": 1,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n },\n {\n \"order\": 2,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/recipes", + "host": ["{{baseUrl}}"], + "path": ["recipes"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Wed, 27 Mar 2024 21:39:42 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "251" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Recipe created\",\n \"data\": {\n \"serial\": 8,\n \"id\": \"1b7356e7-de71-4fa0-9ea1-13766dd2b49b\",\n \"serving\": 2,\n \"cooking_time\": 120,\n \"created_at\": \"2024-03-27T21:39:35.732Z\",\n \"updated_at\": \"2024-03-27T21:39:35.732Z\"\n }\n}" + } + ] + }, + { + "name": "All Recipes", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/recipes?limit=10&page=1", + "host": ["{{baseUrl}}"], + "path": ["recipes"], + "query": [ + { + "key": "limit", + "value": "10" + }, + { + "key": "page", + "value": "1" + } + ] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"meal_id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"serving\": 2,\n \"cooking_time\": 120, // 2 hours. base unit is `seconds`\n \"ingredients\": [\n {\n \"measurement_id\": 1,\n \"ingredient_id\": 1,\n \"quantity\": 0.5\n }\n ],\n \"steps\": [\n {\n \"order\": 1,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n },\n {\n \"order\": 2,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/recipes", + "host": ["{{baseUrl}}"], + "path": ["recipes"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Wed, 27 Mar 2024 21:39:42 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "251" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Recipe created\",\n \"data\": {\n \"serial\": 8,\n \"id\": \"1b7356e7-de71-4fa0-9ea1-13766dd2b49b\",\n \"serving\": 2,\n \"cooking_time\": 120,\n \"created_at\": \"2024-03-27T21:39:35.732Z\",\n \"updated_at\": \"2024-03-27T21:39:35.732Z\"\n }\n}" + }, + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/recipes?pageSize=2&pageNumber=2", + "host": ["{{baseUrl}}"], + "path": ["recipes"], + "query": [ + { + "key": "pageSize", + "value": "2" + }, + { + "key": "pageNumber", + "value": "2" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Thu, 28 Mar 2024 01:06:42 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "1750" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"serial\": 8,\n \"id\": \"1b7356e7-de71-4fa0-9ea1-13766dd2b49b\",\n \"serving\": 2,\n \"cooking_time\": 120,\n \"created_at\": \"2024-03-27T21:39:35.732Z\",\n \"updated_at\": \"2024-03-27T21:39:35.732Z\",\n \"meals\": [\n {\n \"serial\": 2,\n \"id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"name\": \"Ghana Jollof\",\n \"description\": \"Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.\",\n \"status\": \"DRAFT\",\n \"created_at\": \"2024-03-25T04:51:18.624Z\",\n \"updated_at\": \"2024-03-25T04:51:18.624Z\",\n \"video_url\": \"https://youtube.com/watch?q=ve9320a\"\n }\n ]\n },\n {\n \"serial\": 4,\n \"id\": \"2e6d3f86-01ed-46d0-aa03-f977658c1223\",\n \"serving\": 2,\n \"cooking_time\": 120,\n \"created_at\": \"2024-03-27T19:41:30.693Z\",\n \"updated_at\": \"2024-03-27T19:41:30.693Z\",\n \"meals\": [\n {\n \"serial\": 2,\n \"id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"name\": \"Ghana Jollof\",\n \"description\": \"Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.\",\n \"status\": \"DRAFT\",\n \"created_at\": \"2024-03-25T04:51:18.624Z\",\n \"updated_at\": \"2024-03-25T04:51:18.624Z\",\n \"video_url\": \"https://youtube.com/watch?q=ve9320a\"\n }\n ]\n }\n ],\n \"meta\": {\n \"current_page\": 2,\n \"per_page\": 2,\n \"total\": 7\n }\n}" + } + ] + }, + { + "name": "Get A Recipe", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"meal_id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"serving\": 2,\n \"cooking_time\": 120, // 2 hours. base unit is `seconds`\n \"ingredients\": [\n {\n \"measurement_id\": \"\",\n \"ingredient_id\": \"\",\n \"quantity\": \"1/2\"\n }\n ],\n \"steps\": [\n {\n \"order\": 1,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n },\n {\n \"order\": 2,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/recipes/{{recipeId}}", + "host": ["{{baseUrl}}"], + "path": ["recipes", "{{recipeId}}"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"meal_id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"serving\": 2,\n \"cooking_time\": 120, // 2 hours. base unit is `seconds`\n \"ingredients\": [\n {\n \"measurement_id\": \"\",\n \"ingredient_id\": \"\",\n \"quantity\": \"1/2\"\n }\n ],\n \"steps\": [\n {\n \"order\": 1,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n },\n {\n \"order\": 2,\n \"image\": \"{{$randomImageUrl}}\",\n \"caption\": \"Dance a little\",\n \"paragraph\": \"{{$randomLoremParagraph}}\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/recipes/{{recipeId}}", + "host": ["{{baseUrl}}"], + "path": ["recipes", "{{recipeId}}"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Wed, 27 Mar 2024 23:23:11 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "1849" + } + ], + "cookie": [], + "body": "{\n \"data\": {\n \"serial\": 1,\n \"id\": \"70b0543c-de4c-445e-a6c2-131f5c5c0aa7\",\n \"serving\": 2,\n \"cooking_time\": 120,\n \"created_at\": \"2024-03-27T19:33:59.883Z\",\n \"updated_at\": \"2024-03-27T19:33:59.883Z\",\n \"meals\": [\n {\n \"serial\": 2,\n \"id\": \"ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6\",\n \"name\": \"Ghana Jollof\",\n \"description\": \"Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.\",\n \"status\": \"DRAFT\",\n \"created_at\": \"2024-03-25T04:51:18.624Z\",\n \"updated_at\": \"2024-03-25T04:51:18.624Z\",\n \"video_url\": \"https://youtube.com/watch?q=ve9320a\"\n }\n ],\n \"recipes\": [\n {\n \"serial\": 1,\n \"id\": \"61f32a50-175d-4bcf-b3e4-2c4b49f65599\",\n \"image\": \"http://placeimg.com/640/480\",\n \"order\": 1,\n \"caption\": \"Dance a little\",\n \"paragraph\": \"Laudantium enim consequatur enim animi voluptatem quaerat nemo. Consequuntur illum quae exercitationem ducimus molestiae quae soluta hic aut. Natus sit doloremque et itaque et non. Perspiciatis voluptates placeat. Vel ullam est quam ut et.\",\n \"recipeId\": \"70b0543c-de4c-445e-a6c2-131f5c5c0aa7\"\n },\n {\n \"serial\": 2,\n \"id\": \"374965ef-0ad8-448f-9f40-a88bccef233f\",\n \"image\": \"http://placeimg.com/640/480\",\n \"order\": 2,\n \"caption\": \"Dance a little\",\n \"paragraph\": \"Repellat consectetur molestias eum velit ab reprehenderit aliquid dicta ipsam. Dicta et magni minus quaerat eius sed reiciendis. Qui dolores iure. Exercitationem iste velit. A velit accusamus qui quis qui placeat at molestiae.\",\n \"recipeId\": \"70b0543c-de4c-445e-a6c2-131f5c5c0aa7\"\n }\n ]\n }\n}" + } + ] + } + ] + }, + { + "name": "Onboarding", + "item": [ + { + "name": "Get Questions", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Sun, 21 Apr 2024 00:01:07 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "5528" + } + ], + "cookie": [], + "body": "{\n \"data\": [\n {\n \"id\": \"5eac4875-cd28-4b4a-b0ce-60e1a5be512f\",\n \"type\": \"single\",\n \"text\": \"Are you diabetic?\",\n \"status\": \"DRAFT\",\n \"options\": [\n {\n \"id\": \"79a9b597-935c-4ead-9b07-588a433d7a5c\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"5eac4875-cd28-4b4a-b0ce-60e1a5be512f\"\n },\n {\n \"id\": \"578856a8-9686-4006-be8f-f6a7e3929d55\",\n \"text\": \"No\",\n \"image\": null,\n \"questionId\": \"5eac4875-cd28-4b4a-b0ce-60e1a5be512f\"\n }\n ]\n },\n {\n \"id\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\",\n \"type\": \"multiple\",\n \"text\": \"Do you have any allergy?\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"55cb1a83-5564-4259-bf6f-565d46f24134\",\n \"text\": \"Sea food\",\n \"image\": null,\n \"questionId\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\"\n },\n {\n \"id\": \"0ca5884a-6e05-41d8-ac35-3e2f280f9247\",\n \"text\": \"Fish\",\n \"image\": null,\n \"questionId\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\"\n },\n {\n \"id\": \"f6e00c82-e532-4a75-a094-80aac1fb59ea\",\n \"text\": \"Egg\",\n \"image\": null,\n \"questionId\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\"\n },\n {\n \"id\": \"ccb3bc30-74cd-482a-8a21-7a14b701d37a\",\n \"text\": \"Peanut\",\n \"image\": null,\n \"questionId\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\"\n },\n {\n \"id\": \"0d593271-6c9e-4ba9-86a5-6e6c1107b2fc\",\n \"text\": \"Celery\",\n \"image\": null,\n \"questionId\": \"672d0022-898b-4d8d-9da0-3e2d6d734dd7\"\n }\n ]\n },\n {\n \"id\": \"45751715-c5eb-4dc2-ba66-ac07135d6ada\",\n \"type\": \"single\",\n \"text\": \"What your favourite window\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"1b81cf10-50e6-48d8-8e41-6d2049a3b3d5\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"45751715-c5eb-4dc2-ba66-ac07135d6ada\"\n }\n ]\n },\n {\n \"id\": \"a08dd432-6df8-470e-8812-c02d9d64004f\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite window\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"b6318183-ed2c-489c-a1d3-d9974d0ee2e8\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"a08dd432-6df8-470e-8812-c02d9d64004f\"\n },\n {\n \"id\": \"f42de010-d4bc-4215-95a2-2db19205ab00\",\n \"text\": \"No\",\n \"image\": null,\n \"questionId\": \"a08dd432-6df8-470e-8812-c02d9d64004f\"\n }\n ]\n },\n {\n \"id\": \"f02dddd0-cecc-41be-9d36-ad70707466ae\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite window\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"0298390a-b033-4373-b52a-8b745b627539\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"f02dddd0-cecc-41be-9d36-ad70707466ae\"\n },\n {\n \"id\": \"c584ee6c-13f3-4d50-b320-5cca87208798\",\n \"text\": \"No\",\n \"image\": null,\n \"questionId\": \"f02dddd0-cecc-41be-9d36-ad70707466ae\"\n }\n ]\n },\n {\n \"id\": \"e3b86580-438d-4602-bb14-705378b9e7ed\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite window\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"d013e070-6a3f-454d-aa1f-37ef9d2e4c0d\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"e3b86580-438d-4602-bb14-705378b9e7ed\"\n },\n {\n \"id\": \"6183dfe0-1983-4ce2-86e1-9e0f848b5e01\",\n \"text\": \"No\",\n \"image\": null,\n \"questionId\": \"e3b86580-438d-4602-bb14-705378b9e7ed\"\n }\n ]\n },\n {\n \"id\": \"81540fd5-d97c-4225-b314-fb9980d58ba2\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite fruit?\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"d0578505-1696-4d0a-8e3d-52435ed15599\",\n \"text\": \"Yes\",\n \"image\": null,\n \"questionId\": \"81540fd5-d97c-4225-b314-fb9980d58ba2\"\n },\n {\n \"id\": \"9dbd0ee6-772f-404a-9f6b-1d125caf225e\",\n \"text\": \"No\",\n \"image\": null,\n \"questionId\": \"81540fd5-d97c-4225-b314-fb9980d58ba2\"\n }\n ]\n },\n {\n \"id\": \"5155935f-52ec-4eaf-855c-ff2e4d39926a\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite fruit?\",\n \"status\": \"PUBLISHED\",\n \"options\": [\n {\n \"id\": \"7cb2f0f3-19af-4e78-9ffa-b14e31512b64\",\n \"text\": \"Apple\",\n \"image\": null,\n \"questionId\": \"5155935f-52ec-4eaf-855c-ff2e4d39926a\"\n },\n {\n \"id\": \"fbfb609c-f281-4425-89cd-bcbb758e7a18\",\n \"text\": \"Orange\",\n \"image\": null,\n \"questionId\": \"5155935f-52ec-4eaf-855c-ff2e4d39926a\"\n }\n ]\n },\n {\n \"id\": \"309f687d-0253-4dfa-863d-fd08b6a45487\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\",\n \"options\": [\n {\n \"id\": \"d82f60a2-9d6b-4aca-9b5d-97e7fb3f1627\",\n \"text\": \"Apple\",\n \"image\": null,\n \"questionId\": \"309f687d-0253-4dfa-863d-fd08b6a45487\"\n },\n {\n \"id\": \"fef2b37f-addf-4bb8-904f-c9bac1d672ae\",\n \"text\": \"Orange\",\n \"image\": null,\n \"questionId\": \"309f687d-0253-4dfa-863d-fd08b6a45487\"\n }\n ]\n }\n ]\n}" + } + ] + }, + { + "name": "Create Question", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "const questionId = response?.data?.id", + "", + "if (questionId)", + " pm.collectionVariables.set('questionId', questionId)" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\",\n \"type\": \"multiple\",\n \"options\": [\n {\n \"text\": \"Apple\"\n },\n {\n \"text\": \"Orange\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\",\n \"type\": \"multiple\",\n \"options\": [\n {\n \"text\": \"Apple\"\n },\n {\n \"text\": \"Orange\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Sun, 21 Apr 2024 00:00:40 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "196" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Question created\",\n \"data\": {\n \"id\": \"309f687d-0253-4dfa-863d-fd08b6a45487\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\"\n }\n}" + } + ] + }, + { + "name": "Update Question", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"Do you like beans?\",\n \"status\": \"UNPUBLISHED\",\n \"type\": \"single\",\n \"options\": [\n {\n \"id\": \"60a1204d-c985-4ed7-b731-adf8491a518e\",\n \"text\": \"Honey\"\n },\n {\n \"id\": \"98c17408-938a-420f-ab43-6611130ec9b1\",\n \"text\": \"Iron\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions/{{questionId}}", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions", "{{questionId}}"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\",\n \"type\": \"multiple\",\n \"options\": [\n {\n \"text\": \"Apple\"\n },\n {\n \"text\": \"Orange\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["org", "onboarding", "questions"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Sun, 21 Apr 2024 00:00:40 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "196" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Question created\",\n \"data\": {\n \"id\": \"309f687d-0253-4dfa-863d-fd08b6a45487\",\n \"type\": \"multiple\",\n \"text\": \"What your favourite fruit?\",\n \"status\": \"UNPUBLISHED\"\n }\n}" + }, + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"Do you like beans?\",\n \"status\": \"UNPUBLISHED\",\n \"type\": \"single\",\n \"options\": [\n {\n \"id\": \"60a1204d-c985-4ed7-b731-adf8491a518e\",\n \"text\": \"Honey\"\n },\n {\n \"id\": \"98c17408-938a-420f-ab43-6611130ec9b1\",\n \"text\": \"Iron\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/org/onboarding/questions/{{questionId}}", + "host": ["{{baseUrl}}"], + "path": [ + "org", + "onboarding", + "questions", + "{{questionId}}" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "access-control-allow-origin", + "value": "*" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Sun, 21 Apr 2024 00:40:12 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "569" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Update successful\",\n \"data\": {\n \"id\": \"5d827e40-e7f8-4750-8ffc-4f18f9a4c07f\",\n \"type\": \"single\",\n \"text\": \"Do you like beans?\",\n \"status\": \"UNPUBLISHED\",\n \"options\": [\n {\n \"id\": \"60a1204d-c985-4ed7-b731-adf8491a518e\",\n \"text\": \"Honey\",\n \"image\": null,\n \"questionId\": \"5d827e40-e7f8-4750-8ffc-4f18f9a4c07f\"\n },\n {\n \"id\": \"98c17408-938a-420f-ab43-6611130ec9b1\",\n \"text\": \"Iron\",\n \"image\": null,\n \"questionId\": \"5d827e40-e7f8-4750-8ffc-4f18f9a4c07f\"\n }\n ]\n }\n}" + } + ] + } + ] + }, + { + "name": "Get Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users?page=1&limit=12&range_created_at=[toDate,fromDate]", + "host": ["{{baseUrl}}"], + "path": ["users"], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "12" + }, + { + "key": "range_created_at", + "value": "[toDate,fromDate]" + } + ] + } + }, + "response": [] + }, + { + "name": "Payments", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/payments?user_id={{$randomUUID}}", + "host": ["{{baseUrl}}"], + "path": ["payments"], + "query": [ + { + "key": "user_id", + "value": "{{$randomUUID}}", + "description": "(optional)" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [""] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [""] + } + } + ] + }, + { + "name": "Customer", + "item": [ + { + "name": "Auth", + "item": [ + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json()", + "", + "if ( response?.data?.access_token)", + " pm.environment.set(\"accessToken\", response.data.access_token);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"owonwo@live.com\",\n \"password\": \"helloman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": ["{{baseUrl}}"], + "path": ["auth", "login"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"joseph.owonwo@gmail.com\",\n \"password\": \"helloman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": ["{{baseUrl}}"], + "path": ["auth", "login"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Mon, 08 Apr 2024 03:18:00 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "158" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Login successful\",\n \"data\": {\n \"access_token\": \"6wfgh44amvxok4twm08v7cimg5k3065ay1oklioh\",\n \"expires\": \"2024-05-08T03:18:00.035Z\"\n }\n}" + } + ] + }, + { + "name": "Logout", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/auth/logout", + "host": ["{{baseUrl}}"], + "path": ["auth", "logout"] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/auth/logout", + "host": ["{{baseUrl}}"], + "path": ["auth", "logout"] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Wed, 10 Apr 2024 01:38:00 GMT" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-length", + "value": "37" + } + ], + "cookie": [], + "body": "{\n \"message\": \"Session terminated\"\n}" + } + ] + } + ] + }, + { + "name": "Onboarding Questions", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/onboarding/questions", + "host": ["{{baseUrl}}"], + "path": ["onboarding", "questions"] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [""] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [""] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:3006/api/v1", + "type": "string" + }, + { + "key": "recipeId", + "value": "70b0543c-de4c-445e-a6c2-131f5c5c0aa7" + }, + { + "key": "accessToken", + "value": "", + "type": "string" + }, + { + "key": "questionId", + "value": "" + } + ] +} diff --git a/packages/apis/storage/digest-openapi.json b/packages/apis/storage/digest-openapi.json new file mode 100644 index 0000000000..248da59458 --- /dev/null +++ b/packages/apis/storage/digest-openapi.json @@ -0,0 +1,1450 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Digest", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://{{baseurl}}" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer" + } + } + }, + "tags": [ + { + "name": "Org" + }, + { + "name": "Org > Team" + }, + { + "name": "Org > Auth" + }, + { + "name": "Org > Metrics" + }, + { + "name": "Org > Categories" + }, + { + "name": "Org > Ingredients" + }, + { + "name": "Org > Dishes" + }, + { + "name": "Org > Recipes" + }, + { + "name": "Org > Onboarding" + }, + { + "name": "Customer" + }, + { + "name": "Customer > Auth" + } + ], + "paths": { + "/org/team": { + "get": { + "tags": ["Org > Team"], + "summary": "All Team members", + "operationId": "allTeamMembers", + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Thu, 11 Apr 2024 04:24:28 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "502" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "data": [ + { + "id": "81c7e4a4-19cf-48a7-bd02-f07fd239e8c4", + "first_name": "Joseph", + "password": "$argon2id$v=19$m=19456,t=2,p=1$4KmqjtSTFuUANlxHA+4Zxg$RXVfFWxe3Md4ZxtdMaz/lWRwsF9eM1eoWzgOerynlPM", + "last_name": "Owonvwon", + "email": "joseph.owonwo@gmail.com", + "role": "OWNER", + "created_at": "2024-04-11T01:52:52.330Z", + "updated_at": null, + "deleted_at": null + } + ], + "meta": { + "current_page": 1, + "per_page": 25, + "total": 1 + } + } + } + } + } + } + } + }, + "/org/team/c718b157-4fe4-4bb1-adc4-787e95b239a2": { + "delete": { + "tags": ["Org > Team"], + "summary": "Delete Team member", + "operationId": "deleteTeamMember", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/org/auth/login": { + "post": { + "tags": ["Org > Auth"], + "summary": "Login - Org", + "operationId": "loginOrg", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "email": "joseph.owonwo@gmail.com", + "password": "helloman" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Mon, 08 Apr 2024 03:18:00 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "158" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Login successful", + "data": { + "access_token": "6wfgh44amvxok4twm08v7cimg5k3065ay1oklioh", + "expires": "2024-05-08T03:18:00.035Z" + } + } + } + } + } + } + } + }, + "/org/auth/logout": { + "post": { + "tags": ["Org > Auth"], + "summary": "Logout - Org", + "operationId": "logoutOrg", + "requestBody": { + "content": {} + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Wed, 10 Apr 2024 01:38:00 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "37" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Session terminated" + } + } + } + } + } + } + }, + "/org/profile": { + "get": { + "tags": ["Org > Auth"], + "summary": "Profile", + "operationId": "profile", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/metrics": { + "get": { + "tags": ["Org > Metrics"], + "summary": "Get Metrics", + "operationId": "getMetrics", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/kpi/customers": { + "get": { + "tags": ["Org > Metrics"], + "summary": "Customer Growth Metrics", + "operationId": "customerGrowthMetrics", + "parameters": [ + { + "name": "range", + "in": "query", + "schema": { + "type": "string" + }, + "description": "1wk, 1mth, 1y, all. default: 1mth", + "example": "1mth" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/categories/meals": { + "post": { + "tags": ["Org > Categories"], + "summary": "Create a Meal Category", + "operationId": "createAMealCategory", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "name": "Pastries" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + }, + "get": { + "tags": ["Org > Categories"], + "summary": "Get all categories", + "operationId": "getAllCategories", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/categories/05fa9976-2366-479b-8854-21727fb78152": { + "delete": { + "tags": ["Org > Categories"], + "summary": "Delete Category", + "operationId": "deleteCategory", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/ingredients": { + "get": { + "tags": ["Org > Ingredients"], + "summary": "Get Ingredients", + "operationId": "getIngredients", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "1" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "12" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + }, + "post": { + "tags": ["Org > Ingredients"], + "summary": "Create Measurement", + "operationId": "createMeasurement", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "name": "Vanilla Extract" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/dishes": { + "post": { + "tags": ["Org > Dishes"], + "summary": "Create Meal", + "operationId": "createMeal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "name": "Peppered snail and crab shell", + "description": "{{$randomLoremParagraph}}", + "status": "DRAFT", + "category": [4], + "updated_at": "{{$isoTimestamp}}", + "created_at": "{{$isoTimestamp}}", + "video_url": "https://youtube.com/watch?q=ve9320a" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + }, + "get": { + "tags": ["Org > Dishes"], + "summary": "Get Meals", + "operationId": "getMeals", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/recipes": { + "post": { + "tags": ["Org > Recipes"], + "summary": "Create a Recipe", + "operationId": "createARecipe", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "meal_id": "ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6", + "serving": 2, + "cooking_time": 120, + "ingredients": [ + { + "measurement_id": 1, + "ingredient_id": 1, + "quantity": 0.5 + } + ], + "steps": [ + { + "order": 1, + "image": "{{$randomImageUrl}}", + "caption": "Dance a little", + "paragraph": "{{$randomLoremParagraph}}" + }, + { + "order": 2, + "image": "{{$randomImageUrl}}", + "caption": "Dance a little", + "paragraph": "{{$randomLoremParagraph}}" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Wed, 27 Mar 2024 21:39:42 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "251" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Recipe created", + "data": { + "serial": 8, + "id": "1b7356e7-de71-4fa0-9ea1-13766dd2b49b", + "serving": 2, + "cooking_time": 120, + "created_at": "2024-03-27T21:39:35.732Z", + "updated_at": "2024-03-27T21:39:35.732Z" + } + } + } + } + } + } + }, + "get": { + "tags": ["Org > Recipes"], + "summary": "All Recipes", + "operationId": "allRecipes", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "10" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "1" + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Wed, 27 Mar 2024 21:39:42 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "251" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "example-0": { + "summary": "Success", + "value": { + "message": "Recipe created", + "data": { + "serial": 8, + "id": "1b7356e7-de71-4fa0-9ea1-13766dd2b49b", + "serving": 2, + "cooking_time": 120, + "created_at": "2024-03-27T21:39:35.732Z", + "updated_at": "2024-03-27T21:39:35.732Z" + } + } + }, + "example-1": { + "summary": "Success", + "value": { + "data": [ + { + "serial": 8, + "id": "1b7356e7-de71-4fa0-9ea1-13766dd2b49b", + "serving": 2, + "cooking_time": 120, + "created_at": "2024-03-27T21:39:35.732Z", + "updated_at": "2024-03-27T21:39:35.732Z", + "meals": [ + { + "serial": 2, + "id": "ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6", + "name": "Ghana Jollof", + "description": "Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.", + "status": "DRAFT", + "created_at": "2024-03-25T04:51:18.624Z", + "updated_at": "2024-03-25T04:51:18.624Z", + "video_url": "https://youtube.com/watch?q=ve9320a" + } + ] + }, + { + "serial": 4, + "id": "2e6d3f86-01ed-46d0-aa03-f977658c1223", + "serving": 2, + "cooking_time": 120, + "created_at": "2024-03-27T19:41:30.693Z", + "updated_at": "2024-03-27T19:41:30.693Z", + "meals": [ + { + "serial": 2, + "id": "ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6", + "name": "Ghana Jollof", + "description": "Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.", + "status": "DRAFT", + "created_at": "2024-03-25T04:51:18.624Z", + "updated_at": "2024-03-25T04:51:18.624Z", + "video_url": "https://youtube.com/watch?q=ve9320a" + } + ] + } + ], + "meta": { + "current_page": 2, + "per_page": 2, + "total": 7 + } + } + } + } + } + } + } + } + } + }, + "/recipes/{recipeId}": { + "get": { + "tags": ["Org > Recipes"], + "summary": "Get A Recipe", + "operationId": "getARecipe", + "parameters": [ + { + "name": "recipeId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Wed, 27 Mar 2024 23:23:11 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "1849" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "data": { + "serial": 1, + "id": "70b0543c-de4c-445e-a6c2-131f5c5c0aa7", + "serving": 2, + "cooking_time": 120, + "created_at": "2024-03-27T19:33:59.883Z", + "updated_at": "2024-03-27T19:33:59.883Z", + "meals": [ + { + "serial": 2, + "id": "ee6b9c6d-6241-4feb-b70e-7bf6f7b798f6", + "name": "Ghana Jollof", + "description": "Dicta aliquid corporis nobis hic qui et officia quas quis. Quo omnis dolor modi maxime facere corrupti minima aliquid recusandae. Illo reiciendis officia officiis tenetur eos sit ratione sit. Amet earum amet iure at quia.", + "status": "DRAFT", + "created_at": "2024-03-25T04:51:18.624Z", + "updated_at": "2024-03-25T04:51:18.624Z", + "video_url": "https://youtube.com/watch?q=ve9320a" + } + ], + "recipes": [ + { + "serial": 1, + "id": "61f32a50-175d-4bcf-b3e4-2c4b49f65599", + "image": "http://placeimg.com/640/480", + "order": 1, + "caption": "Dance a little", + "paragraph": "Laudantium enim consequatur enim animi voluptatem quaerat nemo. Consequuntur illum quae exercitationem ducimus molestiae quae soluta hic aut. Natus sit doloremque et itaque et non. Perspiciatis voluptates placeat. Vel ullam est quam ut et.", + "recipeId": "70b0543c-de4c-445e-a6c2-131f5c5c0aa7" + }, + { + "serial": 2, + "id": "374965ef-0ad8-448f-9f40-a88bccef233f", + "image": "http://placeimg.com/640/480", + "order": 2, + "caption": "Dance a little", + "paragraph": "Repellat consectetur molestias eum velit ab reprehenderit aliquid dicta ipsam. Dicta et magni minus quaerat eius sed reiciendis. Qui dolores iure. Exercitationem iste velit. A velit accusamus qui quis qui placeat at molestiae.", + "recipeId": "70b0543c-de4c-445e-a6c2-131f5c5c0aa7" + } + ] + } + } + } + } + } + } + } + }, + "/org/onboarding/questions": { + "get": { + "tags": ["Org > Onboarding"], + "summary": "Get Questions", + "operationId": "getQuestions", + "responses": { + "200": { + "description": "OK", + "headers": { + "access-control-allow-origin": { + "schema": { + "type": "string", + "example": "*" + } + }, + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Sun, 21 Apr 2024 00:01:07 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "5528" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "data": [ + { + "id": "5eac4875-cd28-4b4a-b0ce-60e1a5be512f", + "type": "single", + "text": "Are you diabetic?", + "status": "DRAFT", + "options": [ + { + "id": "79a9b597-935c-4ead-9b07-588a433d7a5c", + "text": "Yes", + "image": null, + "questionId": "5eac4875-cd28-4b4a-b0ce-60e1a5be512f" + }, + { + "id": "578856a8-9686-4006-be8f-f6a7e3929d55", + "text": "No", + "image": null, + "questionId": "5eac4875-cd28-4b4a-b0ce-60e1a5be512f" + } + ] + }, + { + "id": "672d0022-898b-4d8d-9da0-3e2d6d734dd7", + "type": "multiple", + "text": "Do you have any allergy?", + "status": "PUBLISHED", + "options": [ + { + "id": "55cb1a83-5564-4259-bf6f-565d46f24134", + "text": "Sea food", + "image": null, + "questionId": "672d0022-898b-4d8d-9da0-3e2d6d734dd7" + }, + { + "id": "0ca5884a-6e05-41d8-ac35-3e2f280f9247", + "text": "Fish", + "image": null, + "questionId": "672d0022-898b-4d8d-9da0-3e2d6d734dd7" + }, + { + "id": "f6e00c82-e532-4a75-a094-80aac1fb59ea", + "text": "Egg", + "image": null, + "questionId": "672d0022-898b-4d8d-9da0-3e2d6d734dd7" + }, + { + "id": "ccb3bc30-74cd-482a-8a21-7a14b701d37a", + "text": "Peanut", + "image": null, + "questionId": "672d0022-898b-4d8d-9da0-3e2d6d734dd7" + }, + { + "id": "0d593271-6c9e-4ba9-86a5-6e6c1107b2fc", + "text": "Celery", + "image": null, + "questionId": "672d0022-898b-4d8d-9da0-3e2d6d734dd7" + } + ] + }, + { + "id": "45751715-c5eb-4dc2-ba66-ac07135d6ada", + "type": "single", + "text": "What your favourite window", + "status": "PUBLISHED", + "options": [ + { + "id": "1b81cf10-50e6-48d8-8e41-6d2049a3b3d5", + "text": "Yes", + "image": null, + "questionId": "45751715-c5eb-4dc2-ba66-ac07135d6ada" + } + ] + }, + { + "id": "a08dd432-6df8-470e-8812-c02d9d64004f", + "type": "multiple", + "text": "What your favourite window", + "status": "PUBLISHED", + "options": [ + { + "id": "b6318183-ed2c-489c-a1d3-d9974d0ee2e8", + "text": "Yes", + "image": null, + "questionId": "a08dd432-6df8-470e-8812-c02d9d64004f" + }, + { + "id": "f42de010-d4bc-4215-95a2-2db19205ab00", + "text": "No", + "image": null, + "questionId": "a08dd432-6df8-470e-8812-c02d9d64004f" + } + ] + }, + { + "id": "f02dddd0-cecc-41be-9d36-ad70707466ae", + "type": "multiple", + "text": "What your favourite window", + "status": "PUBLISHED", + "options": [ + { + "id": "0298390a-b033-4373-b52a-8b745b627539", + "text": "Yes", + "image": null, + "questionId": "f02dddd0-cecc-41be-9d36-ad70707466ae" + }, + { + "id": "c584ee6c-13f3-4d50-b320-5cca87208798", + "text": "No", + "image": null, + "questionId": "f02dddd0-cecc-41be-9d36-ad70707466ae" + } + ] + }, + { + "id": "e3b86580-438d-4602-bb14-705378b9e7ed", + "type": "multiple", + "text": "What your favourite window", + "status": "PUBLISHED", + "options": [ + { + "id": "d013e070-6a3f-454d-aa1f-37ef9d2e4c0d", + "text": "Yes", + "image": null, + "questionId": "e3b86580-438d-4602-bb14-705378b9e7ed" + }, + { + "id": "6183dfe0-1983-4ce2-86e1-9e0f848b5e01", + "text": "No", + "image": null, + "questionId": "e3b86580-438d-4602-bb14-705378b9e7ed" + } + ] + }, + { + "id": "81540fd5-d97c-4225-b314-fb9980d58ba2", + "type": "multiple", + "text": "What your favourite fruit?", + "status": "PUBLISHED", + "options": [ + { + "id": "d0578505-1696-4d0a-8e3d-52435ed15599", + "text": "Yes", + "image": null, + "questionId": "81540fd5-d97c-4225-b314-fb9980d58ba2" + }, + { + "id": "9dbd0ee6-772f-404a-9f6b-1d125caf225e", + "text": "No", + "image": null, + "questionId": "81540fd5-d97c-4225-b314-fb9980d58ba2" + } + ] + }, + { + "id": "5155935f-52ec-4eaf-855c-ff2e4d39926a", + "type": "multiple", + "text": "What your favourite fruit?", + "status": "PUBLISHED", + "options": [ + { + "id": "7cb2f0f3-19af-4e78-9ffa-b14e31512b64", + "text": "Apple", + "image": null, + "questionId": "5155935f-52ec-4eaf-855c-ff2e4d39926a" + }, + { + "id": "fbfb609c-f281-4425-89cd-bcbb758e7a18", + "text": "Orange", + "image": null, + "questionId": "5155935f-52ec-4eaf-855c-ff2e4d39926a" + } + ] + }, + { + "id": "309f687d-0253-4dfa-863d-fd08b6a45487", + "type": "multiple", + "text": "What your favourite fruit?", + "status": "UNPUBLISHED", + "options": [ + { + "id": "d82f60a2-9d6b-4aca-9b5d-97e7fb3f1627", + "text": "Apple", + "image": null, + "questionId": "309f687d-0253-4dfa-863d-fd08b6a45487" + }, + { + "id": "fef2b37f-addf-4bb8-904f-c9bac1d672ae", + "text": "Orange", + "image": null, + "questionId": "309f687d-0253-4dfa-863d-fd08b6a45487" + } + ] + } + ] + } + } + } + } + } + }, + "post": { + "tags": ["Org > Onboarding"], + "summary": "Create Question", + "operationId": "createQuestion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "question": "What your favourite fruit?", + "status": "UNPUBLISHED", + "type": "multiple", + "options": [ + { + "text": "Apple" + }, + { + "text": "Orange" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "access-control-allow-origin": { + "schema": { + "type": "string", + "example": "*" + } + }, + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Sun, 21 Apr 2024 00:00:40 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "196" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Question created", + "data": { + "id": "309f687d-0253-4dfa-863d-fd08b6a45487", + "type": "multiple", + "text": "What your favourite fruit?", + "status": "UNPUBLISHED" + } + } + } + } + } + } + } + }, + "/org/onboarding/questions/{questionId}": { + "post": { + "tags": ["Org > Onboarding"], + "summary": "Update Question", + "operationId": "updateQuestion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "question": "Do you like beans?", + "status": "UNPUBLISHED", + "type": "single", + "options": [ + { + "id": "60a1204d-c985-4ed7-b731-adf8491a518e", + "text": "Honey" + }, + { + "id": "98c17408-938a-420f-ab43-6611130ec9b1", + "text": "Iron" + } + ] + } + } + } + } + }, + "parameters": [ + { + "name": "questionId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "access-control-allow-origin": { + "schema": { + "type": "string", + "example": "*" + } + }, + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Sun, 21 Apr 2024 00:00:40 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "196" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "example-0": { + "summary": "Success", + "value": { + "message": "Question created", + "data": { + "id": "309f687d-0253-4dfa-863d-fd08b6a45487", + "type": "multiple", + "text": "What your favourite fruit?", + "status": "UNPUBLISHED" + } + } + }, + "example-1": { + "summary": "Success", + "value": { + "message": "Update successful", + "data": { + "id": "5d827e40-e7f8-4750-8ffc-4f18f9a4c07f", + "type": "single", + "text": "Do you like beans?", + "status": "UNPUBLISHED", + "options": [ + { + "id": "60a1204d-c985-4ed7-b731-adf8491a518e", + "text": "Honey", + "image": null, + "questionId": "5d827e40-e7f8-4750-8ffc-4f18f9a4c07f" + }, + { + "id": "98c17408-938a-420f-ab43-6611130ec9b1", + "text": "Iron", + "image": null, + "questionId": "5d827e40-e7f8-4750-8ffc-4f18f9a4c07f" + } + ] + } + } + } + } + } + } + } + } + } + }, + "/users": { + "get": { + "tags": ["Org"], + "summary": "Get Users", + "operationId": "getUsers", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "1" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer" + }, + "example": "12" + }, + { + "name": "range_created_at", + "in": "query", + "schema": { + "type": "string" + }, + "example": "[toDate,fromDate]" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/payments": { + "get": { + "tags": ["Org"], + "summary": "Payments", + "operationId": "payments", + "parameters": [ + { + "name": "user_id", + "in": "query", + "schema": { + "type": "string" + }, + "description": "(optional)", + "example": "{{$randomUUID}}" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": ["Customer > Auth"], + "summary": "Login", + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "email": "owonwo@live.com", + "password": "helloman" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Mon, 08 Apr 2024 03:18:00 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "158" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Login successful", + "data": { + "access_token": "6wfgh44amvxok4twm08v7cimg5k3065ay1oklioh", + "expires": "2024-05-08T03:18:00.035Z" + } + } + } + } + } + } + } + }, + "/auth/logout": { + "post": { + "tags": ["Customer > Auth"], + "summary": "Logout", + "operationId": "logout", + "requestBody": { + "content": {} + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "content-type": { + "schema": { + "type": "string", + "example": "application/json" + } + }, + "date": { + "schema": { + "type": "string", + "example": "Wed, 10 Apr 2024 01:38:00 GMT" + } + }, + "connection": { + "schema": { + "type": "string", + "example": "close" + } + }, + "content-length": { + "schema": { + "type": "integer", + "example": "37" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message": "Session terminated" + } + } + } + } + } + } + }, + "/onboarding/questions": { + "get": { + "tags": ["Customer"], + "summary": "Onboarding Questions", + "operationId": "onboardingQuestions", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + } + } + } + } + } +} diff --git a/packages/apis/tsconfig.json b/packages/apis/tsconfig.json new file mode 100644 index 0000000000..2688427422 --- /dev/null +++ b/packages/apis/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "types": ["vite/client", "@types/node"], + "target": "es5", + "outDir": "dist", + "allowJs": true, + "skipLibCheck": true, + "strictNullChecks": false, + "strict": true, + "module": "esnext", + "moduleResolution": "node", + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md new file mode 100644 index 0000000000..8b42d901b0 --- /dev/null +++ b/packages/eslint-config/README.md @@ -0,0 +1,3 @@ +# `@turbo/eslint-config` + +Collection of internal eslint configurations. diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js new file mode 100644 index 0000000000..c667cd1001 --- /dev/null +++ b/packages/eslint-config/library.js @@ -0,0 +1,34 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["eslint:recommended", "prettier", "eslint-config-turbo"], + plugins: ["only-warn"], + globals: { + React: true, + JSX: true, + }, + env: { + node: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + "node_modules/", + "dist/", + ], + overrides: [ + { + files: ["*.js?(x)", "*.ts?(x)"], + }, + ], +}; diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js new file mode 100644 index 0000000000..6000e54673 --- /dev/null +++ b/packages/eslint-config/next.js @@ -0,0 +1,35 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: [ + "eslint:recommended", + "prettier", + require.resolve("@vercel/style-guide/eslint/next"), + "eslint-config-turbo", + ], + globals: { + React: true, + JSX: true, + }, + env: { + node: true, + browser: true, + }, + plugins: ["only-warn"], + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + "node_modules/", + ], + overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }], +}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 0000000000..46ee25185f --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@repo/eslint-config", + "version": "0.0.0", + "private": true, + "files": [ + "library.js", + "next.js", + "react-internal.js" + ], + "devDependencies": { + "@vercel/style-guide": "^5.1.0", + "eslint-config-turbo": "^1.11.3", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-only-warn": "^1.1.0", + "@typescript-eslint/parser": "^6.17.0", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "typescript": "5.4.3" + } +} diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js new file mode 100644 index 0000000000..f56c9249e7 --- /dev/null +++ b/packages/eslint-config/react-internal.js @@ -0,0 +1,43 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +/* + * This is a custom ESLint configuration for use with + * internal (bundled by their consumer) libraries + * that utilize React. + * + * This config extends the Vercel Engineering Style Guide. + * For more information, see https://github.com/vercel/style-guide + * + */ + +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["eslint:recommended", "prettier", "eslint-config-turbo"], + plugins: ["only-warn"], + globals: { + React: true, + JSX: true, + }, + env: { + browser: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + "node_modules/", + "dist/", + ], + overrides: [ + // Force ESLint to detect .tsx files + { files: ["*.js?(x)", "*.ts?(x)"] }, + ], +}; diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000000..93a1db41a8 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,13 @@ +{ + "name": "@repo/shared", + "version": "0.0.0", + "private": true, + "scripts": {}, + "dependencies": { + "ramda": "^0.29.1" + }, + "devDependencies": { + "@types/ramda": "^0.29.10", + "typescript": "5.4.3" + } +} diff --git a/packages/shared/src/data.helpers.ts b/packages/shared/src/data.helpers.ts new file mode 100644 index 0000000000..8db93b6d80 --- /dev/null +++ b/packages/shared/src/data.helpers.ts @@ -0,0 +1,37 @@ +import { is } from "ramda"; + +const EmptyPrimitives = Object.freeze({ + Array: [], + Object: { + __proto_: { + type: "EmptyObject", + }, + }, +}); + +export const safeNum = (a: unknown, fallback = 0): number => { + const value = Number(a); + + return !Object.is(NaN, value) ? value : fallback; +}; + +export const safeArray = (a?: Array): Array => + Array.isArray(a) ? a : EmptyPrimitives.Array; + +export const safeStr = (a: unknown, fallback = ""): string => + typeof a === "string" ? a : fallback; + +const EmptyObject: Record = Object.freeze({}); + +export const safeObj = ( + obj: T, +): T extends Record ? T : typeof EmptyObject => { + // @ts-expect-error; + return is(Object, obj) ? obj : EmptyObject; +}; + +export function safeInt(num: unknown, fallback = 0): number { + const value = parseInt(num as string); + + return !Object.is(NaN, value) ? value : fallback; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000000..ca86687c4b --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@repo/typescript-config/react-library.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json new file mode 100644 index 0000000000..0f80cfd67c --- /dev/null +++ b/packages/typescript-config/base.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "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" + } +} diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json new file mode 100644 index 0000000000..44f4289918 --- /dev/null +++ b/packages/typescript-config/nextjs.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Next.js", + "extends": "./base.json", + "compilerOptions": { + "plugins": [{ "name": "next" }], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "jsx": "preserve", + "noEmit": true + } +} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000000..27c0e60436 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json new file mode 100644 index 0000000000..44924d9ed8 --- /dev/null +++ b/packages/typescript-config/react-library.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React Library", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js new file mode 100644 index 0000000000..d19366c38e --- /dev/null +++ b/packages/ui/.eslintrc.js @@ -0,0 +1,12 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + rules: { + "no-redeclare": "off", + }, + extends: ["@repo/eslint-config/react-internal.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "packages/ui/tsconfig.lint.json", + }, +}; diff --git a/packages/ui/api/general.ts b/packages/ui/api/general.ts new file mode 100644 index 0000000000..721cb819be --- /dev/null +++ b/packages/ui/api/general.ts @@ -0,0 +1,22 @@ +import { siteMetadata } from "../../../tools/bridge-frontend/src/lib/siteMetadata"; +import { DocumentInterface } from "../lib/types/common"; + +export const fetchDocument = async (id: string): Promise => { + const response = await fetch(`/docs/${id}.json`); + const data = await response.json(); + + const processedData = { + title: data.title, + subHeading: data.subHeading, + content: data.content.map((item: any) => { + return { + heading: item.heading, + content: item.content.map((paragraph: any) => { + return paragraph.replace(/siteMetadata.email/g, siteMetadata.email); + }), + }; + }), + }; + + return processedData; +}; diff --git a/packages/ui/api/index.ts b/packages/ui/api/index.ts new file mode 100644 index 0000000000..4f9cb5448e --- /dev/null +++ b/packages/ui/api/index.ts @@ -0,0 +1,90 @@ +import { apiHost } from "@/src/lib/constants"; +import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; + +type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; + +interface HttpOptions { + method?: HttpMethod; + url: string; + data?: Record; + params?: Record; + headers?: Record; + timeout?: number; + responseType?: + | "json" + | "arraybuffer" + | "blob" + | "document" + | "text" + | undefined; + download?: boolean; + searchParams?: Record; +} + +const baseConfig: AxiosRequestConfig = { + baseURL: apiHost, + timeout: 10000, +}; + +const https: AxiosInstance = axios.create(baseConfig); + +export const httpRequest = async ( + options: HttpOptions, + config: AxiosRequestConfig = {} +): Promise => { + const { + method = "get", + url, + data, + params, + headers, + timeout, + responseType, + searchParams, + } = options; + let query = ""; + if (searchParams) { + const filteredParams = Object.fromEntries( + Object.entries(searchParams).filter( + ([, value]) => value !== undefined && value !== null && value !== "" + ) + ); + if (Object.keys(filteredParams).length) { + query = new URLSearchParams(filteredParams).toString(); + } + } + + const httpConfig: AxiosRequestConfig = { + method, + url: query ? `${url}?${query}` : url, + data, + params, + headers: { ...(headers || {}) }, + timeout, + responseType: responseType, + ...config, + }; + try { + const response = await https(httpConfig); + return response.data as ResponseData; + } catch (error) { + handleHttpError(error); + throw error; + } +}; + +// Centralized error handling function +const handleHttpError = (error: any) => { + // if the error is a server error (status code 5xx) before handling + if (isAxiosError(error) && error.response && error.response.status >= 500) { + console.error("Server error:", error); + } else { + // other errors + console.error("An error occurred:", error); + } +}; + +// Type guard to check if the error is an AxiosError +const isAxiosError = (error: any): error is import("axios").AxiosError => { + return error.isAxiosError === true; +}; diff --git a/packages/ui/components/common/404.tsx b/packages/ui/components/common/404.tsx new file mode 100644 index 0000000000..599d7fd0bc --- /dev/null +++ b/packages/ui/components/common/404.tsx @@ -0,0 +1,38 @@ +import { ErrorType } from "../../lib/interfaces/ui"; +import CustomError from "./error"; + +export function Custom404Error({ + customPageTitle, + showRedirectText = true, + redirectText = "Home Page", + isFullWidth, + message, + showMessage = true, + redirectLink = "/", + children, +}: ErrorType) { + return ( + + {children} + + ); +} + +export default Custom404Error; diff --git a/packages/ui/components/common/500.tsx b/packages/ui/components/common/500.tsx new file mode 100644 index 0000000000..fa2afada31 --- /dev/null +++ b/packages/ui/components/common/500.tsx @@ -0,0 +1,32 @@ +import { ErrorType } from "../../lib/interfaces/ui"; +import CustomError from "./error"; + +function Custom500Error({ + customPageTitle, + message, + showRedirectText, + redirectText, + err, + redirectLink, + children, +}: ErrorType) { + return ( + + {children} + + ); +} + +export default Custom500Error; diff --git a/packages/ui/components/common/connect-wallet.tsx b/packages/ui/components/common/connect-wallet.tsx new file mode 100644 index 0000000000..54e017651e --- /dev/null +++ b/packages/ui/components/common/connect-wallet.tsx @@ -0,0 +1,94 @@ +import { + Link2Icon, + LinkBreak2Icon, + ExclamationTriangleIcon, +} from "@radix-ui/react-icons"; +import { cn, downloadMetaMask, ethereum } from "../../lib/utils"; +import useWalletStore from "../../stores/wallet-store"; +import TruncatedAddress from "./truncated-address"; +import { Button } from "../shared/button"; +import { ButtonVariants } from "../../lib/types/ui"; + +interface ConnectWalletButtonProps { + className?: string; + variant?: ButtonVariants; + text?: string; +} + +const ConnectWalletButton = ({ + className, + text = "Connect Wallet", + variant = "outline", +}: ConnectWalletButtonProps) => { + const { + walletConnected, + connectWallet, + disconnectWallet, + isWrongNetwork, + switchNetwork, + address, + } = useWalletStore(); + + const handleClick = () => { + if (!ethereum) { + downloadMetaMask(); + return; + } + + if (isWrongNetwork) { + switchNetwork(); + return; + } + + if (walletConnected) { + disconnectWallet(); + } else { + connectWallet(); + } + }; + + const renderButtonContent = () => { + if (!ethereum) { + return ( + <> + + Download MetaMask + + ); + } + + if (isWrongNetwork) { + return ( + <> + + Unsupported network + + ); + } + + return walletConnected ? ( + <> + + {} + + ) : ( + <> + + {text} + + ); + }; + + return ( + + ); +}; + +export default ConnectWalletButton; diff --git a/packages/ui/components/common/copy.tsx b/packages/ui/components/common/copy.tsx new file mode 100644 index 0000000000..438efdb193 --- /dev/null +++ b/packages/ui/components/common/copy.tsx @@ -0,0 +1,21 @@ +import { CopyIcon, CheckIcon } from "../shared/react-icons"; +import { useCopy } from "../../hooks/useCopy"; +import { Button } from "../shared/button"; + +const Copy = ({ value }: { value: string | number }) => { + const { copyToClipboard, copied } = useCopy(); + return ( + + ); +}; + +export default Copy; diff --git a/packages/ui/components/common/data-table/data-table-column-header.tsx b/packages/ui/components/common/data-table/data-table-column-header.tsx new file mode 100644 index 0000000000..013b98f131 --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-column-header.tsx @@ -0,0 +1,71 @@ +import { + ArrowDownIcon, + ArrowUpIcon, + CaretSortIcon, + EyeNoneIcon, +} from "../../shared/react-icons"; +import { Column } from "@tanstack/react-table"; + +import { cn } from "../../../lib/utils"; +import { Button } from "../../shared/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../shared/dropdown-menu"; + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column; + title: string; +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
; + } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + column.toggleVisibility(false)}> + + Hide + + + +
+ ); +} diff --git a/packages/ui/components/common/data-table/data-table-faceted-filter.tsx b/packages/ui/components/common/data-table/data-table-faceted-filter.tsx new file mode 100644 index 0000000000..96aff71c6e --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-faceted-filter.tsx @@ -0,0 +1,143 @@ +import * as React from "react"; +import { CheckIcon, PlusCircledIcon } from "../../shared/react-icons"; +import { Column } from "@tanstack/react-table"; + +import { cn } from "../../../lib/utils"; +import { Badge } from "../../shared/badge"; +import { Button } from "../../shared/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "../../shared/command"; +import { Popover, PopoverContent, PopoverTrigger } from "../../shared/popover"; +import { Separator } from "../../shared/separator"; + +interface DataTableFacetedFilterProps { + column?: Column; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function DataTableFacetedFilter({ + column, + title, + options, +}: DataTableFacetedFilterProps) { + const facets = column?.getFacetedUniqueValues(); + const selectedValues = new Set(column?.getFilterValue() as string[]); + + return ( + + + + + + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + { + if (isSelected) { + selectedValues.delete(option.value); + } else { + selectedValues.add(option.value); + } + const filterValues = Array.from(selectedValues); + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ); + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} +
+ ); + })} +
+ {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + + + + )} +
+
+
+
+ ); +} diff --git a/packages/ui/components/common/data-table/data-table-pagination.tsx b/packages/ui/components/common/data-table/data-table-pagination.tsx new file mode 100644 index 0000000000..148f06eb48 --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-pagination.tsx @@ -0,0 +1,144 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, +} from "@radix-ui/react-icons"; +import { PaginationState, Table } from "@tanstack/react-table"; +import { Button } from "../../shared/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../shared/select"; +import { Input } from "../../shared/input"; +import { useState } from "react"; + +interface DataTablePaginationProps { + table: Table; + refetch?: () => void; + setPagination: (pagination: PaginationState) => void; +} + +export function DataTablePagination({ + table, + refetch, + setPagination, +}: DataTablePaginationProps) { + const [page, setPage] = useState(table.getState().pagination.pageIndex); + + const handlePageChange = (e: React.ChangeEvent) => { + setPage(Number(e.target.value)); + }; + + const handleKey = (e: React.KeyboardEvent) => { + if ( + e.key === "Enter" && + page > 0 && + page !== table.getState().pagination.pageIndex + ) { + table.setPageIndex(page); + refetch?.(); + } + }; + + return ( +
+
+ Showing {table?.getFilteredRowModel()?.rows?.length} row(s) +
+
+
+

Rows per page

+ +
+
+ Page + e.target.select()} + onBlur={() => setPage(table.getState().pagination.pageIndex)} + /> + {/* uncomment the following line when total count feature is implemented */} + {/* of {formatNumber(table.getPageCount())} */} +
+
+ + + + {/* uncomment the following line when total count feature is implemented */} + {/* */} +
+
+
+ ); +} diff --git a/packages/ui/components/common/data-table/data-table-row-actions.tsx b/packages/ui/components/common/data-table/data-table-row-actions.tsx new file mode 100644 index 0000000000..e775e6c460 --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-row-actions.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { DotsHorizontalIcon } from "../../shared/react-icons"; +import { Row } from "@tanstack/react-table"; +import { Button } from "../../shared/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "../../shared/dropdown-menu"; + +interface DataTableRowActionsProps { + row: Row; + labels: { label: string; value: string }[] | null; +} + +export function DataTableRowActions({ + row, + labels, +}: DataTableRowActionsProps) { + return ( + + + + + + View + {labels === null ? null : ( + <> + + + Labels + + + {labels?.map((label) => ( + + {label.label} + + ))} + + + + + )} + + + + ); +} diff --git a/packages/ui/components/common/data-table/data-table-toolbar.tsx b/packages/ui/components/common/data-table/data-table-toolbar.tsx new file mode 100644 index 0000000000..d813ee817f --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-toolbar.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Cross2Icon, ReloadIcon } from "../../shared/react-icons"; +import { Table } from "@tanstack/react-table"; + +import { Button } from "../../shared/button"; +import { DataTableViewOptions } from "./data-table-view-options"; + +import { DataTableFacetedFilter } from "./data-table-faceted-filter"; + +interface DataTableToolbarProps { + table: Table; + refetch?: () => void; + toolbar?: { + column: string; + title: string; + options: { label: string; value: string }[]; + }[]; +} +export function DataTableToolbar({ + table, + toolbar, + refetch, +}: DataTableToolbarProps) { + const isFiltered = table.getState().columnFilters.length > 0; + + return ( +
+
+ {toolbar?.map( + (item, index) => + table.getColumn(item.column) && ( + + ) + )} + {isFiltered && ( + + )} +
+
+ {refetch && ( + + )} + +
+
+ ); +} diff --git a/packages/ui/components/common/data-table/data-table-view-options.tsx b/packages/ui/components/common/data-table/data-table-view-options.tsx new file mode 100644 index 0000000000..c46aec90e6 --- /dev/null +++ b/packages/ui/components/common/data-table/data-table-view-options.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { MixerHorizontalIcon } from "../../shared/react-icons"; +import { Table } from "@tanstack/react-table"; + +import { Button } from "../../shared/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "../../shared/dropdown-menu"; + +interface DataTableViewOptionsProps { + table: Table; +} + +export function DataTableViewOptions({ + table, +}: DataTableViewOptionsProps) { + return ( + + + + + + Toggle columns + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} + + + ); +} diff --git a/packages/ui/components/common/data-table/data-table.tsx b/packages/ui/components/common/data-table/data-table.tsx new file mode 100644 index 0000000000..80ccb1944e --- /dev/null +++ b/packages/ui/components/common/data-table/data-table.tsx @@ -0,0 +1,211 @@ +"use client"; + +import * as React from "react"; +import { useRouter } from "next/router"; +import { + ColumnDef, + ColumnFiltersState, + OnChangeFn, + PaginationState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../shared/table"; +import { DataTablePagination } from "./data-table-pagination"; +import { DataTableToolbar } from "./data-table-toolbar"; +import { Skeleton } from "../../shared/skeleton"; +import { Button } from "../../shared/button"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + toolbar?: { + column: string; + title: string; + options: { label: string; value: string }[]; + }[]; + updateQueryParams?: (query: any) => void; + refetch?: () => void; + total: number; + isLoading?: boolean; + noPagination?: boolean; + noResultsText?: string; + noResultsMessage?: string; +} + +export function DataTable({ + columns, + data, + toolbar, + refetch, + total, + isLoading, + noPagination, + noResultsText, + noResultsMessage, +}: DataTableProps) { + const { query, push, pathname } = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [sorting, setSorting] = React.useState([]); + + const pagination = React.useMemo(() => { + return { + pageIndex: Number(query.page) || 1, + pageSize: Number(query.size) || 20, + }; + }, [query.page, query.size]); + + const setPagination: OnChangeFn = (func) => { + const { pageIndex, pageSize } = + typeof func === "function" ? func(pagination) : func; + const newPageIndex = pagination.pageSize !== pageSize ? 1 : pageIndex; + const params = { + ...query, + page: newPageIndex > 0 ? newPageIndex : 1, + size: pageSize <= 100 ? pageSize : 100, + }; + push({ pathname, query: params }); + }; + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + onPaginationChange: setPagination, + manualPagination: true, + // pageCount: Math.ceil(total / pagination.pageSize), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + return ( +
+ {data && ( + + )} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {isLoading ? ( + <> + + + + + + + ) : data && table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {pagination.pageIndex > 1 ? ( +

+ No {noResultsText || "results"} found for the selected + filters. + +

+ ) : ( +

+ {noResultsMessage || + `No ${noResultsText || "results"} found.`} +

+ )} +
+
+ )} +
+
+
+ {data && !isLoading && !noPagination && ( + + )} +
+ ); +} diff --git a/packages/ui/components/common/data-table/skeleton-loader.tsx b/packages/ui/components/common/data-table/skeleton-loader.tsx new file mode 100644 index 0000000000..98180fdad2 --- /dev/null +++ b/packages/ui/components/common/data-table/skeleton-loader.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "../../shared/skeleton"; +import { TableHeader, TableRow, TableBody } from "../../shared/table"; +import { Table } from "lucide-react"; + +const DataTableSkeleton = ({ columns }: { columns: number }) => { + const renderSkeletonColumns = () => { + return Array.from({ length: columns }).map((_, index) => ( + + )); + }; + + return ( + + + {renderSkeletonColumns()} + + {renderSkeletonColumns()} +
+ ); +}; + +export default DataTableSkeleton; diff --git a/packages/ui/components/common/document.tsx b/packages/ui/components/common/document.tsx new file mode 100644 index 0000000000..74e96cbcd2 --- /dev/null +++ b/packages/ui/components/common/document.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import Custom404Error from "./404"; +import Spinner from "../shared/spinner"; +import { + DocumentContentInterface, + DocumentInterface, +} from "../../lib/types/common"; + +type DocumentProps = { + document: DocumentInterface; + isLoading: boolean; +}; + +const DocumentComponent = ({ document, isLoading }: DocumentProps) => { + if (isLoading) { + return ; + } + + if (!document?.title) { + return ; + } + + return ( + <> +
+

{document.title}

+

{document.subHeading}

+
+
+ {document.content && + document.content.map( + (section: DocumentContentInterface, index: number) => ( +
+

{section.heading}

+ {section.content && + section.content.map((paragraph: string, index: number) => ( +
+ ))} +
+ ) + )} +
+ + ); +}; + +export default DocumentComponent; diff --git a/packages/ui/components/common/empty-state.tsx b/packages/ui/components/common/empty-state.tsx new file mode 100644 index 0000000000..fd98671640 --- /dev/null +++ b/packages/ui/components/common/empty-state.tsx @@ -0,0 +1,54 @@ +import Image from "next/image"; +import React from "react"; +import { cn } from "../../lib/utils"; + +const EmptyState = ({ + title, + description, + icon, + imageSrc, + imageAlt, + action, + className, +}: { + title?: string; + description?: string; + icon?: React.ReactNode; + imageSrc?: string; + imageAlt?: string; + action?: React.ReactNode; + className?: string; +}) => { + return ( +
+
+ {icon &&
{icon}
} + {imageSrc && ( + {imageAlt + )} + {title && ( +

+ {title} +

+ )} + {description && ( +

{description}

+ )} + {action &&
{action}
} +
+
+ ); +}; + +export default EmptyState; diff --git a/packages/ui/components/common/error.tsx b/packages/ui/components/common/error.tsx new file mode 100644 index 0000000000..e881523922 --- /dev/null +++ b/packages/ui/components/common/error.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import NextErrorComponent from "next/error"; +import Link from "next/link"; +import { ErrorType } from "../../lib/interfaces/ui"; + +function ErrorMessage({ + statusText, + message, + showMessage, + showStatusText, +}: any) { + return ( +
+ {showStatusText &&

{statusText}

} + {message && showMessage && ( +

{message}

+ )} +
+ ); +} + +export function CustomError({ + showRedirectText = true, + heading = "Oops! Something went wrong.", + statusText = "500", + message = "We're experiencing technical difficulties. Please try again later.", + redirectText = "Home Page", + isFullWidth, + err, + showMessage = true, + showStatusText = true, + statusCode, + isModal, + redirectLink = "/", + children, + ...props +}: ErrorType) { + return ( +
+
+
+

{heading}

+
+ +
+ {showRedirectText && ( +
+

+ Go to{" "} + + {redirectText} + +

+
+ )} + {children &&
{children}
} +
+
+
+ ); +} + +// server-side error +CustomError.getInitialProps = async ({ res, err }: any) => { + const statusCode = res ? res.statusCode : err?.statusCode || 404; + const errorInitialProps = await NextErrorComponent.getInitialProps({ + res, + err, + } as any); + errorInitialProps.statusCode = statusCode; + + // custom server-side error + return statusCode < 500 + ? errorInitialProps + : { ...errorInitialProps, statusCode }; +}; + +export default CustomError; diff --git a/packages/ui/components/common/loading-list.tsx b/packages/ui/components/common/loading-list.tsx new file mode 100644 index 0000000000..8ea5f51b9a --- /dev/null +++ b/packages/ui/components/common/loading-list.tsx @@ -0,0 +1,19 @@ +import KeyValueItem, { KeyValueList } from "../shared/key-value"; +import { Skeleton } from "../shared/skeleton"; + +export function LoadingList({ numberOfItems = 4 }: { numberOfItems?: number }) { + return ( +
+ + {Array.from({ length: numberOfItems }).map((_, index) => ( + } + value={} + isLastItem={index === numberOfItems - 1} + /> + ))} + +
+ ); +} diff --git a/packages/ui/components/common/loading-state.tsx b/packages/ui/components/common/loading-state.tsx new file mode 100644 index 0000000000..1dbe19893e --- /dev/null +++ b/packages/ui/components/common/loading-state.tsx @@ -0,0 +1,35 @@ +import { Skeleton } from "../shared/skeleton"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "../shared/card"; +import { LoadingList } from "./loading-list"; + +export default function LoadingState({ + numberOfItems = 4, +}: { + numberOfItems?: number; +}) { + return ( + <> + + + + + + + + + + + ); +} + +export async function getServerSideProps(context: any) { + return { + props: {}, + }; +} diff --git a/packages/ui/components/common/network-status.tsx b/packages/ui/components/common/network-status.tsx new file mode 100644 index 0000000000..886d919f75 --- /dev/null +++ b/packages/ui/components/common/network-status.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { cn } from "../../lib/utils"; + +const MessageContent = ( +

+ You seem to be offline +
Please check your internet connection. +

+); + +export const NetworkStatus = ({ message = MessageContent }) => { + const [isOnline, setIsOnline] = React.useState(true); + + React.useEffect(() => { + const setOnlineStatus = () => { + setIsOnline(navigator.onLine); + }; + + window.addEventListener("online", setOnlineStatus); + window.addEventListener("offline", setOnlineStatus); + + return () => { + window.removeEventListener("online", setOnlineStatus); + window.removeEventListener("offline", setOnlineStatus); + }; + }, []); + + return ( +
+
+ {message} +
+
+ ); +}; diff --git a/packages/ui/components/common/truncated-address.tsx b/packages/ui/components/common/truncated-address.tsx new file mode 100644 index 0000000000..0f16fe5a51 --- /dev/null +++ b/packages/ui/components/common/truncated-address.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../shared/tooltip"; +import Copy from "./copy"; +import { useMediaQuery } from "../../hooks/useMediaQuery"; + +const TruncatedAddress = ({ + address, + prefixLength, + suffixLength, + showCopy = true, + link, + showFullLength = false, +}: { + address: string; + prefixLength?: number; + suffixLength?: number; + showCopy?: boolean; + link?: + | string + | { + pathname: string; + query: { [key: string]: string | number }; + }; + showFullLength?: boolean; +}) => { + if (!address) { + return -; + } + + const isDesktop = useMediaQuery("(min-width: 1024px)"); + + // should show full address only on desktop if showFullLength is true; otherwise show truncated version + const shouldShowFullAddress = isDesktop && showFullLength; + + const truncatedAddress = `${address?.substring( + 0, + prefixLength || 6 + )}...${address?.substring(address.length - (suffixLength || 4))}`; + + return ( + <> +
+ {link ? ( + + {shouldShowFullAddress ? address : truncatedAddress} + + ) : shouldShowFullAddress ? ( + {address} + ) : ( + + + {truncatedAddress} + +

{address}

+
+
+
+ )} + {showCopy && } +
+ + ); +}; + +export default TruncatedAddress; diff --git a/packages/ui/components/shared/alert.tsx b/packages/ui/components/shared/alert.tsx new file mode 100644 index 0000000000..ef4d6dbee7 --- /dev/null +++ b/packages/ui/components/shared/alert.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "../../lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + info: "border-info/50 text-info dark:border-info [&>svg]:text-info", + success: + "border-success/50 text-success dark:border-success [&>svg]:text-success", + warning: + "border-warning/50 text-warning dark:border-warning [&>svg]:text-warning", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/packages/ui/components/shared/avatar.tsx b/packages/ui/components/shared/avatar.tsx new file mode 100644 index 0000000000..d906d3fc71 --- /dev/null +++ b/packages/ui/components/shared/avatar.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "../../lib/utils"; + +type AvatarImageProps = React.ComponentPropsWithoutRef< + typeof AvatarPrimitive.Image +> & { + src?: string | React.ComponentType; +}; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + AvatarImageProps +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/ui/components/shared/badge.tsx b/packages/ui/components/shared/badge.tsx new file mode 100644 index 0000000000..d8a17f563b --- /dev/null +++ b/packages/ui/components/shared/badge.tsx @@ -0,0 +1,37 @@ +import * as 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 border 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: "border-transparent bg-primary hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + success: + "border-transparent bg-success text-success-foreground hover:bg-success/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/packages/ui/components/shared/button.tsx b/packages/ui/components/shared/button.tsx new file mode 100644 index 0000000000..10c65cc717 --- /dev/null +++ b/packages/ui/components/shared/button.tsx @@ -0,0 +1,79 @@ +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 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + clear: "bg-transparent text-primary-foreground outline-none", + }, + 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 p-1", + wrap: "h-full w-full", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +interface LinkButtonProps + extends React.AnchorHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const LinkButton = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "a"; + return ( + + ); + } +); + +LinkButton.displayName = "LinkButton"; + +export { Button, buttonVariants, LinkButton }; diff --git a/packages/ui/components/shared/calendar.tsx b/packages/ui/components/shared/calendar.tsx new file mode 100644 index 0000000000..7e2c82d897 --- /dev/null +++ b/packages/ui/components/shared/calendar.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker } from "react-day-picker"; + +import { cn } from "../../lib/utils"; +import { buttonVariants } from "./button"; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/packages/ui/components/shared/card.tsx b/packages/ui/components/shared/card.tsx new file mode 100644 index 0000000000..6abe706a68 --- /dev/null +++ b/packages/ui/components/shared/card.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/packages/ui/components/shared/checkbox.tsx b/packages/ui/components/shared/checkbox.tsx new file mode 100644 index 0000000000..efc02a97cb --- /dev/null +++ b/packages/ui/components/shared/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/packages/ui/components/shared/command.tsx b/packages/ui/components/shared/command.tsx new file mode 100644 index 0000000000..5ee7795618 --- /dev/null +++ b/packages/ui/components/shared/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react"; +import { DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "../../lib/utils"; +import { Dialog, DialogContent } from "./dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/packages/ui/components/shared/dialog.tsx b/packages/ui/components/shared/dialog.tsx new file mode 100644 index 0000000000..e7fc0c8651 --- /dev/null +++ b/packages/ui/components/shared/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/packages/ui/components/shared/dropdown-menu.tsx b/packages/ui/components/shared/dropdown-menu.tsx new file mode 100644 index 0000000000..a149167bf2 --- /dev/null +++ b/packages/ui/components/shared/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/packages/ui/components/shared/external-link.tsx b/packages/ui/components/shared/external-link.tsx new file mode 100644 index 0000000000..13366137ea --- /dev/null +++ b/packages/ui/components/shared/external-link.tsx @@ -0,0 +1,25 @@ +import { ExternalLinkIcon } from "@radix-ui/react-icons"; +import React from "react"; + +const ExternalLink = ({ + href, + children, + className, +}: { + href: string; + children: React.ReactNode; + className?: string; +}) => { + return ( + + {children} + + ); +}; + +export default ExternalLink; diff --git a/packages/ui/components/shared/input.tsx b/packages/ui/components/shared/input.tsx new file mode 100644 index 0000000000..0e5aa86c5b --- /dev/null +++ b/packages/ui/components/shared/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/packages/ui/components/shared/key-value.tsx b/packages/ui/components/shared/key-value.tsx new file mode 100644 index 0000000000..af2dce535d --- /dev/null +++ b/packages/ui/components/shared/key-value.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Separator } from "./separator"; + +export const KeyValueList = ({ children }: { children: React.ReactNode }) => ( +
    {children}
+); + +export const KeyValueItem = ({ + label, + value, + isLastItem, +}: { + label?: string | React.ReactNode; + value: string | number | React.ReactNode; + isLastItem?: boolean; +}) => ( +
  • +
    + {label && {label}} + {value} +
    + {!isLastItem && } +
  • +); + +export default KeyValueItem; diff --git a/packages/ui/components/shared/label.tsx b/packages/ui/components/shared/label.tsx new file mode 100644 index 0000000000..38ed561d01 --- /dev/null +++ b/packages/ui/components/shared/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "../../lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/packages/ui/components/shared/mode-toggle.tsx b/packages/ui/components/shared/mode-toggle.tsx new file mode 100644 index 0000000000..4537471aad --- /dev/null +++ b/packages/ui/components/shared/mode-toggle.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Moon, Sun } from "./react-icons"; +import { useTheme } from "next-themes"; + +import { Button } from "./button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/packages/ui/components/shared/navigation-menu.tsx b/packages/ui/components/shared/navigation-menu.tsx new file mode 100644 index 0000000000..40fe3561fc --- /dev/null +++ b/packages/ui/components/shared/navigation-menu.tsx @@ -0,0 +1,128 @@ +import * as React from "react"; +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; +import { cva } from "class-variance-authority"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)); +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; + +const NavigationMenuItem = NavigationMenuPrimitive.Item; + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +); + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)); +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; + +const NavigationMenuLink = NavigationMenuPrimitive.Link; + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
    + +
    +)); +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName; + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
    + +)); +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName; + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +}; diff --git a/packages/ui/components/shared/popover.tsx b/packages/ui/components/shared/popover.tsx new file mode 100644 index 0000000000..25256593d8 --- /dev/null +++ b/packages/ui/components/shared/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; + +import { cn } from "../../lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/packages/ui/components/shared/react-icons.tsx b/packages/ui/components/shared/react-icons.tsx new file mode 100644 index 0000000000..f46743156b --- /dev/null +++ b/packages/ui/components/shared/react-icons.tsx @@ -0,0 +1,5 @@ +export * from "@radix-ui/react-icons"; +// @ts-expect-error TODO: Resolve icon conflicts +export * from "lucide-react"; + + diff --git a/packages/ui/components/shared/select.tsx b/packages/ui/components/shared/select.tsx new file mode 100644 index 0000000000..5d4c184001 --- /dev/null +++ b/packages/ui/components/shared/select.tsx @@ -0,0 +1,119 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, +}; diff --git a/packages/ui/components/shared/separator.tsx b/packages/ui/components/shared/separator.tsx new file mode 100644 index 0000000000..e3eb575726 --- /dev/null +++ b/packages/ui/components/shared/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../../lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/packages/ui/components/shared/skeleton.tsx b/packages/ui/components/shared/skeleton.tsx new file mode 100644 index 0000000000..5f2f52224f --- /dev/null +++ b/packages/ui/components/shared/skeleton.tsx @@ -0,0 +1,26 @@ +import { cn } from "../../lib/utils"; + +function Skeleton({ + className, + width, + height, + ...props +}: { + className?: string; + width?: number | string; + height?: number | string; +}) { + return ( +
    + ); +} + +export { Skeleton }; diff --git a/packages/ui/components/shared/spinner.tsx b/packages/ui/components/shared/spinner.tsx new file mode 100644 index 0000000000..2948ef451b --- /dev/null +++ b/packages/ui/components/shared/spinner.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +const Spinner = () => { + return ( +
    +
    +
    + ); +}; + +export default Spinner; diff --git a/packages/ui/components/shared/table.tsx b/packages/ui/components/shared/table.tsx new file mode 100644 index 0000000000..b5f5948e64 --- /dev/null +++ b/packages/ui/components/shared/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/packages/ui/components/shared/tabs.tsx b/packages/ui/components/shared/tabs.tsx new file mode 100644 index 0000000000..ae87429071 --- /dev/null +++ b/packages/ui/components/shared/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "../../lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/packages/ui/components/shared/textarea.tsx b/packages/ui/components/shared/textarea.tsx new file mode 100644 index 0000000000..0ec32ac3fb --- /dev/null +++ b/packages/ui/components/shared/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +