diff --git a/apps/docs/.env.example b/apps/docs/.env.example new file mode 100644 index 0000000..846b3b0 --- /dev/null +++ b/apps/docs/.env.example @@ -0,0 +1,51 @@ +## --- Notes ----------------------------------------------------------------------------------- */ + +## -i- The `.env.example` file can be copied into `.env.local` using `npx turbo env:local` +## -i- For development, staging & production environments, check the next.js docs: +## -i- https://nextjs.org/docs/app/building-your-application/configuring/environment-variables + +## -i- Note that you should treat environment variables as if they could be inlined in your bundle during builds & deployments +## -i- This means dynamically retrieving environment variables from e.g. `process.env[someKey]` might not work +## -i- It also means that you should never prefix with `NEXT_PUBLIC_` for sensitive / private keys + +## -i- We suggest that for each environment variable you add here, you also add an entry in `appConfig.ts` +## -i- There, you can add logic like ```envValue: process.env.NEXT_PUBLIC_ENV_KEY || process.env.EXPO_PUBLIC_ENV_KEY``` +## -i- Where you would only define the NEXT_PUBLIC_ prefixed versions here in `.env.local` locally and using Next.js UI for deployed envs +## -i- For environment variables you only be available server-side, you can omit `NEXT_PUBLIC_` + +NEXT_PUBLIC_APP_ENV=next + +## --- General --------------------------------------------------------------------------------- */ +## -i- Env vars that should always be present & the same locally, independent of the simulated environment +## --------------------------------------------------------------------------------------------- */ + +APP_SECRET="your-secret-here" # used for signing header context, generate a random string + +## --- LOCAL ----------------------------------------------------------------------------------- */ +## -i- Defaults you might want to switch out for local development by commenting / uncommenting +## --------------------------------------------------------------------------------------------- */ + +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000/api +NEXT_PUBLIC_GRAPH_URL=http://localhost:3000/api/graphql + +# DB_URL= # TODO: Add DB layer connection for full local dev... + +## --- DEV ------------------------------------------------------------------------------------- */ +# -i- Uncomment while on development branch to simulate the dev environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the dev environment... + +## --- STAGE ----------------------------------------------------------------------------------- */ +# -i- Uncomment while on staging branch to simulate the stage environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the stage environment... + +## --- PROD ------------------------------------------------------------------------------------ */ +# -i- Uncomment while on main branch to simulate the production environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the production environment... diff --git a/apps/docs/babel.config.js b/apps/docs/babel.config.js new file mode 100644 index 0000000..91dfe76 --- /dev/null +++ b/apps/docs/babel.config.js @@ -0,0 +1,14 @@ +// babel.config.js +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + plugins: [ + ['@babel.plugin-transform-react-jsx', { + runtime: 'automatic', + importSource: 'nativewind', + jsxImportSource: 'nativewind', + }] + ] + } +} diff --git a/apps/docs/components/Button.docs.tsx b/apps/docs/components/Button.docs.tsx new file mode 100644 index 0000000..fc0d502 --- /dev/null +++ b/apps/docs/components/Button.docs.tsx @@ -0,0 +1,8 @@ +import * as AppButton from '@app/components/Button' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const Button = AppButton.Button +export const ButtonProps = AppButton.ButtonProps diff --git a/apps/docs/components/Checkbox.docs.tsx b/apps/docs/components/Checkbox.docs.tsx new file mode 100644 index 0000000..27c892d --- /dev/null +++ b/apps/docs/components/Checkbox.docs.tsx @@ -0,0 +1,8 @@ +import * as AppCheckbox from '@app/core/forms/Checkbox.styled' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const Checkbox = AppCheckbox.Checkbox +export const CheckboxProps = AppCheckbox.CheckboxProps diff --git a/apps/docs/components/Checklist.docs.tsx b/apps/docs/components/Checklist.docs.tsx new file mode 100644 index 0000000..8aa5c58 --- /dev/null +++ b/apps/docs/components/Checklist.docs.tsx @@ -0,0 +1,8 @@ +import * as AppCheckList from '@app/core/forms/CheckList.styled' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const CheckList = AppCheckList.CheckList +export const CheckListProps = AppCheckList.CheckListProps diff --git a/apps/docs/components/Hidden.tsx b/apps/docs/components/Hidden.tsx new file mode 100644 index 0000000..aa9f699 --- /dev/null +++ b/apps/docs/components/Hidden.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +/* --- ------------------------------------------------------------------------------- */ + +export const Hidden = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ) +} + +/* --- Aliases --------------------------------------------------------------------------------- */ + +export const LLMOptimized = Hidden +export const TitleWrapper = Hidden diff --git a/apps/docs/components/NumberStepper.docs.tsx b/apps/docs/components/NumberStepper.docs.tsx new file mode 100644 index 0000000..0fc07c7 --- /dev/null +++ b/apps/docs/components/NumberStepper.docs.tsx @@ -0,0 +1,12 @@ +import * as AppNumberStepper from '@app/core/forms/NumberStepper.styled' +import { styled } from '@app/primitives' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const NumberStepper = styled(AppNumberStepper.NumberStepper, '', { + textInputClassName: 'bg-transparent', +}) + +export const NumberStepperProps = AppNumberStepper.NumberStepperProps diff --git a/apps/docs/components/RadioGroup.docs.tsx b/apps/docs/components/RadioGroup.docs.tsx new file mode 100644 index 0000000..bb42006 --- /dev/null +++ b/apps/docs/components/RadioGroup.docs.tsx @@ -0,0 +1,14 @@ +import * as AppRadioGroup from '@app/core/forms/RadioGroup.styled' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const RadioGroup = AppRadioGroup.RadioGroup +export const RadioGroupProps = AppRadioGroup.RadioGroupProps + +export const RadioGroupContext = AppRadioGroup.RadioGroupContext +export const useRadioGroupContext = AppRadioGroup.useRadioGroupContext + +export const RadioButton = AppRadioGroup.RadioButton +export const RadioButtonProps = AppRadioGroup.RadioButtonProps diff --git a/apps/docs/components/Select.docs.tsx b/apps/docs/components/Select.docs.tsx new file mode 100644 index 0000000..d46842b --- /dev/null +++ b/apps/docs/components/Select.docs.tsx @@ -0,0 +1,173 @@ +import type { ReactNode, ElementRef, Dispatch, SetStateAction, LegacyRef } from 'react' +import { createContext, useContext, useState, useEffect, forwardRef, useRef } from 'react' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { Platform, StyleSheet, Dimensions } from 'react-native' +import * as SP from '@green-stack/forms/Select.primitives' +import * as AppSelect from '@app/core/forms/Select.styled' +import { cn, styled, View, Text, Pressable, getThemeColor } from '@app/primitives' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const SelectContext = AppSelect.SelectContext +export const useSelectContext = AppSelect.useSelectContext + +export const SelectTrigger = AppSelect.SelectTrigger +export const SelectTriggerProps = AppSelect.SelectTriggerProps + +export const SelectScrollButton = AppSelect.SelectScrollButton +export const SelectScrollButtonProps = AppSelect.SelectScrollButtonProps + +export const SelectContent = styled(AppSelect.SelectContent, 'bg-popover', { nativeID: 'docsTable' }) as typeof AppSelect.SelectContent +export const SelectContentProps = AppSelect.SelectContentProps + +export const SelectLabel = AppSelect.SelectLabel +export const SelectLabelProps = AppSelect.SelectLabelProps + +export const SelectItem = AppSelect.SelectItem +export const SelectItemProps = AppSelect.SelectItemProps + +export const SelectSeparator = AppSelect.SelectSeparator +export const SelectSeparatorProps = AppSelect.SelectSeparatorProps + +export const SelectProps = AppSelect.SelectProps + +/* --- Constants ------------------------------------------------------------------------------- */ + +const isWeb = Platform.OS === 'web' +const isMobile = ['ios', 'android'].includes(Platform.OS) + +/** --- createSelect() ------------------------------------------------------------------------- */ +/** -i- Create a Universal Select where you can pass a Generic type to narrow the string `value` & `onChange()` params */ +export const createSelectComponent = () => Object.assign(forwardRef< + ElementRef, + AppSelect.SelectProps +>((rawProps, ref) => { + // Props + const props = SelectProps.applyDefaults(rawProps) + const { placeholder, disabled, hasError, children, onChange, ...restProps } = props + + // State + const [value, setValue] = useState(props.value) + const [options, setOptions] = useState(props.options) + + // Hooks + const insets = useSafeAreaInsets() + const contentInsets = { + top: insets.top, + bottom: insets.bottom, + left: 12, + right: 12, + } + + // Vars + const optionsKey = Object.keys(options).join('-') + const hasPropOptions = Object.keys(props.options || {}).length > 0 + const selectValueKey = `${optionsKey}-${!!value}-${!!options?.[value]}` + + // -- Effects -- + + useEffect(() => { + const isValidOption = value && Object.keys(options || {})?.includes?.(value) + if (isValidOption) { + onChange(value as T) + } else if (!value && !restProps.required) { + onChange(undefined as unknown as T) + } + }, [value]) + + useEffect(() => { + if (props.value !== value) setValue(props.value) + }, [props.value]) + + // -- Render -- + + return ( + + setValue(option!.value!)} + disabled={disabled} + asChild + > + + + + + {isWeb && ( + + {options?.[value] || placeholder} + + )} + + + + + {hasPropOptions && ( + + + {!!placeholder && {placeholder}} + {Object.entries(props.options).map(([value, label]) => ( + + {label} + + ))} + + + )} + {children} + + + + + + + ) +}), { + displayName: 'Select', + Option: SelectItem, + Item: SelectItem, + Separator: SelectSeparator, + Group: SP.SelectGroup, + Label: SelectLabel, + Content: SelectContent, + /** -i- Create a Universal Select where you can pass a Generic type to narrow the string `value` & `onChange()` params */ + create: createSelectComponent, +}) + +/* --- Select ---------------------------------------------------------------------------------- */ + +export const Select = createSelectComponent() diff --git a/apps/docs/components/Switch.docs.tsx b/apps/docs/components/Switch.docs.tsx new file mode 100644 index 0000000..95619c6 --- /dev/null +++ b/apps/docs/components/Switch.docs.tsx @@ -0,0 +1,8 @@ +import * as AppSwitch from '@app/core/forms/Switch.styled' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const Switch = AppSwitch.Switch +export const SwitchProps = AppSwitch.SwitchProps diff --git a/apps/docs/components/TextArea.docs.tsx b/apps/docs/components/TextArea.docs.tsx new file mode 100644 index 0000000..8331400 --- /dev/null +++ b/apps/docs/components/TextArea.docs.tsx @@ -0,0 +1,9 @@ +import * as AppTextArea from '@app/core/forms/TextArea.styled' +import { styled } from '@app/primitives' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const TextArea = styled(AppTextArea.TextArea, 'bg-transparent') +export const TextAreaProps = AppTextArea.TextAreaProps diff --git a/apps/docs/components/TextInput.docs.tsx b/apps/docs/components/TextInput.docs.tsx new file mode 100644 index 0000000..c0c3d8f --- /dev/null +++ b/apps/docs/components/TextInput.docs.tsx @@ -0,0 +1,9 @@ +import * as AppTextInput from '@app/core/forms/TextInput.styled' +import { styled } from '@app/primitives' + +/* --- Documentation overrides? ---------------------------------------------------------------- */ + +// -i- Optionally wrap and edit these to restyle the component for the docs + +export const TextInput = styled(AppTextInput.TextInput, 'bg-transparent') +export const TextInputProps = AppTextInput.TextInputProps diff --git a/apps/docs/docs.theme.jsx b/apps/docs/docs.theme.jsx new file mode 100644 index 0000000..931ac53 --- /dev/null +++ b/apps/docs/docs.theme.jsx @@ -0,0 +1,202 @@ +import { useRouter } from 'next/router' + +/* --- Theme ----------------------------------------------------------------------------------- */ + +/** @type {import('nextra-theme-docs').DocsThemeConfig} */ +export default { + logo: ( +
+ +
+ FullProduct.dev ⚡️ Universal App Starter +
+ ), + logoLink: 'https://fullproduct.dev', + project: { + link: 'https://github.com/FullProduct-dev/green-stack-starter-demo?tab=readme-ov-file#:Rr9ab:', + }, + navigation: true, + sidebar: { + autoCollapse: true, + defaultMenuCollapseLevel: 2, + toggleButton: true, + }, + docsRepositoryBase: 'https://github.com/FullProduct-dev/green-stack-starter-demo', + editLink: { + component: null, + }, + darkMode: true, + footer: { + content: ( +
+
+
+ +
+ FullProduct.dev Starterkit Logo +
+
+
+
+ FullProduct.dev 🚀 +
+
+ Universal App Starterkit +
+
+
+
+ +
+
+
+ By +
+
+
+
+ Thorr / codinsonn's Profile Picture +
+
+
+
+ Thorr ⚡️ codinsonn.dev +
+
+
+
+
+
+ FullProduct.dev is a product of 'Aetherspace Digital' (registered as 0757.590.784 in Belgium) +
+
+
+ For support or inquiries, please email us at thorr@fullproduct.dev +
+
+
+
+ {/*
+
+ GREEN stack +
+
+ + GraphQL + +
+ + React + +
+ + Expo + +
+ + Next.js + +
+
+
*/} +
+
+ Product +
+
+ + Starterkit Docs + +
+ + Sign-Up + +
+ + Sign-In + +
+ + Demo + +
+
+
+
+ Legal +
+ + ), + }, + useNextSeoProps() { + const { asPath } = useRouter() + if (asPath === '/') { + return { + title: 'FullProduct.dev ⚡️ Universal App Starter', + } + } else if (asPath.includes('plugins')) { + return { + titleTemplate: 'FullProduct.dev ⚡️ %s Plugin - Universal App Starter Docs', + } + } + return { + titleTemplate: 'FullProduct.dev | %s - Universal App Starter Docs', + } + } +} diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts new file mode 100644 index 0000000..a4a7b3f --- /dev/null +++ b/apps/docs/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs new file mode 100644 index 0000000..9198b1a --- /dev/null +++ b/apps/docs/next.config.mjs @@ -0,0 +1,14 @@ +import { withExpo } from '@expo/next-adapter' +import nextra from 'nextra' + +const withNextra = nextra({ + theme: "nextra-theme-docs", + themeConfig: "./docs.theme.jsx", +}) + +import mainNextConfig from '@app/next/next.config.base.cjs' + +/** @type {import('next').NextConfig} */ +const nextConfig = withNextra(withExpo(mainNextConfig)) + +export default nextConfig diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..5dca3b7 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,21 @@ +{ + "name": "@app/docs", + "version": "1.0.0", + "private": true, + "dependencies": { + "@mdx-js/loader": "^3.1.0", + "@mdx-js/react": "^3.1.0", + "@next/mdx": "~15.0.4", + "@types/mdx": "^2.0.13", + "next": "~15.0.4", + "nextra": "^3.3.1", + "nextra-theme-docs": "^3.3.1" + }, + "scripts": { + "dev": "NEXT_PUBLIC_APP_ENV=docs next -p 4000", + "build": "node -v && NEXT_PUBLIC_APP_ENV=docs next build", + "start": "NEXT_PUBLIC_APP_ENV=docs next start -p 4000", + "env:local": "cp .env.example .env.local", + "regenerate:docs": "npm -w @green-stack/core run run:script ../../apps/docs/scripts/regenerate-docs.ts" + } +} diff --git a/apps/docs/pages/@app-core/components/Button.mdx b/apps/docs/pages/@app-core/components/Button.mdx new file mode 100644 index 0000000..a398b34 --- /dev/null +++ b/apps/docs/pages/@app-core/components/Button.mdx @@ -0,0 +1,199 @@ +import { Button, getDocumentationProps } from '@app/components/Button' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## Button + + +# Button + +```typescript copy +import { Button } from '@app/components/Button' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## Button Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const ButtonProps = z.object({ + type: z + .enum(["primary", "secondary", "outline", "link", "warn", "danger", "info", "success"]) + .default("primary"), + text: z + .string() + .optional() + .example("Press me"), + size: z + .enum(["sm", "md", "lg"]) + .default("md"), + href: z + .string() + .url() + .optional() + .example("https://fullproduct.dev"), + iconLeft: z + .enum(["AddFilled", "ArrowLeftFilled", "ArrowRightFilled", "CheckFilled", "ChevronDownFilled", "ChevronUpFilled", "RemoveFilled", "UndoFilled"]) + .optional() + .describe("Name of an icon registered in the icon registry"), + iconRight: z + .enum(["AddFilled", "ArrowLeftFilled", "ArrowRightFilled", "CheckFilled", "ChevronDownFilled", "ChevronUpFilled", "RemoveFilled", "UndoFilled"]) + .optional() + .example("ArrowRightFilled") + .describe("Name of an icon registered in the icon registry"), + disabled: z + .boolean() + .optional(), + fullWidth: z + .boolean() + .optional(), + className: z + .string() + .optional(), + textClassName: z + .string() + .optional(), + iconSize: z + .number() + .default(16), + hitSlop: z + .number() + .default(10), + target: z + .enum(["_blank", "_self", "_parent", "_top"]) + .default("_self") + .example("_blank"), + replace: z + .boolean() + .optional(), + push: z + .boolean() + .optional(), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + type?: "primary" | "secondary" | "outline" | "link" | "warn" | "danger" | "info" | "success"; + text?: string; + size?: "sm" | "md" | "lg"; + href?: string | undefined; + /** Name of an icon registered in the icon registry */ + iconLeft?: ("AddFilled" | "ArrowLeftFilled" | "ArrowRightFilled" | "CheckFilled" | "ChevronDownFilled" | "ChevronUpFilled" | "RemoveFilled" | "UndoFilled") | undefined; + /** Name of an icon registered in the icon registry */ + iconRight?: ("AddFilled" | "ArrowLeftFilled" | "ArrowRightFilled" | "CheckFilled" | "ChevronDownFilled" | "ChevronUpFilled" | "RemoveFilled" | "UndoFilled") | undefined; + disabled?: boolean; + fullWidth?: boolean; + className?: string | undefined; + textClassName?: string | undefined; + iconSize?: number; + hitSlop?: number; + target?: "_blank" | "_self" | "_parent" | "_top"; + replace?: boolean | undefined; + push?: boolean | undefined; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `Button` component in the following location: + + + + + + + + + + + +
+ +## Developer Notes + +### `children` vs `text` prop + +You can pass either `props.children` or `props.text` to the Button component. If you pass `props.children`, it will render the children as the button content. This supports JSX and other React Components. Whereas `props.text` only supports actual text strings. + +If you pass both, `props.children` will be used instead `props.text`. + +### `style` prop + +Instead of using just `className`, you can also pass a `style` prop. We will combine the styles from `className` and `style` props. This allows you to use inline styles for dynamic styling while still applying CSS classes. + +If both classNames and `style` prop influence the same style, the `style` prop will take precedence. + +### `onPress` event handlers + +Just like react-native's Pressable, our `Button` component supports the `onPress` prop, alongside other event handlers: + +- [`onPress()`](https://reactnative.dev/docs/pressable#onpress) - Called when a touch is released. +- [`onPressIn()`](https://reactnative.dev/docs/pressable#onpressin) - Called when a touch is initiated. +- [`onPressOut()`](https://reactnative.dev/docs/pressable#onpressout) - Called when a touch is released, after `onPressIn`. +- [`onHoverIn()`](https://reactnative.dev/docs/pressable#onhoverin) - Called when the pointer enters the button area. +- [`onHoverOut()`](https://reactnative.dev/docs/pressable#onhoverout) - Called when the pointer leaves the button area. +- [`onLongPress()`](https://reactnative.dev/docs/pressable#onlongpress) - Called when a long press gesture is detected. +- [`onBlur()`](https://reactnative.dev/docs/pressable#onblur) - Called when the button loses focus. +- [`onFocus()`](https://reactnative.dev/docs/pressable#onfocus) - Called when the button gains focus. + +Naturally, when `props.disabled` is `true`, all event handlers will be ignored. + +If you pass both `href` and `onPress` props, we will try to execute both. Giving preference to the `onPress` handler first. + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="Button.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = ButtonProps.documentationProps('Button') +``` diff --git a/apps/docs/pages/@app-core/components/_meta.ts b/apps/docs/pages/@app-core/components/_meta.ts new file mode 100644 index 0000000..d46ae39 --- /dev/null +++ b/apps/docs/pages/@app-core/components/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'Button': 'Button', +} diff --git a/apps/docs/pages/@app-core/forms/CheckList.mdx b/apps/docs/pages/@app-core/forms/CheckList.mdx new file mode 100644 index 0000000..7020a09 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/CheckList.mdx @@ -0,0 +1,134 @@ +import { CheckList, getDocumentationProps } from '@app/forms/CheckList.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## CheckList + + +# CheckList + +```typescript copy +import { CheckList } from '@app/forms/CheckList.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## CheckList Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const CheckListProps = z.object({ + disabled: z + .boolean() + .optional(), + hasError: z + .boolean() + .optional(), + className: z + .string() + .optional(), + checkboxClassName: z + .string() + .optional(), + indicatorClassName: z + .string() + .optional(), + labelClassName: z + .string() + .optional(), + hitSlop: z + .number() + .default(6), + options: z + .record(z.string(), z.string()), + value: z + .array(z.string()) + .default([]), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + disabled?: boolean; + hasError?: boolean; + className?: string | undefined; + checkboxClassName?: string | undefined; + indicatorClassName?: string | undefined; + labelClassName?: string | undefined; + hitSlop?: number; + options: { + [x: string]: string; + }; + value?: string[]; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `CheckList` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="CheckList.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = CheckListProps.documentationProps('CheckList') +``` diff --git a/apps/docs/pages/@app-core/forms/Checkbox.mdx b/apps/docs/pages/@app-core/forms/Checkbox.mdx new file mode 100644 index 0000000..d8b6615 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/Checkbox.mdx @@ -0,0 +1,134 @@ +import { Checkbox, getDocumentationProps } from '@app/forms/Checkbox.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## Checkbox + + +# Checkbox + +```typescript copy +import { Checkbox } from '@app/forms/Checkbox.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## Checkbox Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const CheckboxProps = z.object({ + checked: z + .boolean() + .optional(), + label: z + .string() + .optional() + .example("Label"), + disabled: z + .boolean() + .optional(), + hasError: z + .boolean() + .optional(), + className: z + .string() + .optional(), + checkboxClassName: z + .string() + .optional(), + indicatorClassName: z + .string() + .optional(), + labelClassName: z + .string() + .optional(), + hitSlop: z + .number() + .default(6), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + checked?: boolean; + label?: string | undefined; + disabled?: boolean; + hasError?: boolean; + className?: string | undefined; + checkboxClassName?: string | undefined; + indicatorClassName?: string | undefined; + labelClassName?: string | undefined; + hitSlop?: number; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `Checkbox` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="Checkbox.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = CheckboxProps.documentationProps('Checkbox') +``` diff --git a/apps/docs/pages/@app-core/forms/NumberStepper.mdx b/apps/docs/pages/@app-core/forms/NumberStepper.mdx new file mode 100644 index 0000000..d2254b7 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/NumberStepper.mdx @@ -0,0 +1,150 @@ +import { NumberStepper, getDocumentationProps } from '@app/forms/NumberStepper.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## NumberStepper + + +# NumberStepper + +```typescript copy +import { NumberStepper } from '@app/forms/NumberStepper.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## NumberStepper Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const NumberStepperProps = z.object({ + value: z + .number() + .optional(), + min: z + .number() + .optional(), + max: z + .number() + .optional(), + step: z + .number() + .default(1), + placeholder: z + .string() + .optional() + .example("Enter number..."), + disabled: z + .boolean() + .optional(), + readOnly: z + .boolean() + .optional(), + hasError: z + .boolean() + .optional(), + className: z + .string() + .optional(), + pressableClassName: z + .string() + .optional(), + textInputClassName: z + .string() + .optional(), + placeholderClassName: z + .string() + .optional(), + placeholderTextColor: z + .string() + .optional(), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + value?: number; + min?: number; + max?: number | undefined; + step?: number; + placeholder?: string | undefined; + disabled?: boolean; + readOnly?: boolean; + hasError?: boolean; + className?: string | undefined; + pressableClassName?: string | undefined; + textInputClassName?: string | undefined; + placeholderClassName?: string | undefined; + placeholderTextColor?: string | undefined; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `NumberStepper` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="NumberStepper.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = NumberStepperProps.documentationProps('NumberStepper') +``` diff --git a/apps/docs/pages/@app-core/forms/RadioGroup.mdx b/apps/docs/pages/@app-core/forms/RadioGroup.mdx new file mode 100644 index 0000000..1193d69 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/RadioGroup.mdx @@ -0,0 +1,134 @@ +import { RadioGroup, getDocumentationProps } from '@app/forms/RadioGroup.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## RadioGroup + + +# RadioGroup + +```typescript copy +import { RadioGroup } from '@app/forms/RadioGroup.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## RadioGroup Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const RadioGroupProps = z.object({ + value: z + .string() + .optional(), + disabled: z + .boolean() + .optional(), + hasError: z + .boolean() + .optional(), + className: z + .string() + .optional(), + radioButtonClassName: z + .string() + .optional(), + indicatorClassName: z + .string() + .optional(), + labelClassName: z + .string() + .optional(), + hitSlop: z + .number() + .default(6), + options: z + .record(z.string(), z.string()), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + value?: string; + disabled?: boolean; + hasError?: boolean; + className?: string | undefined; + radioButtonClassName?: string | undefined; + indicatorClassName?: string | undefined; + labelClassName?: string | undefined; + hitSlop?: number; + options: { + [x: string]: string; + }; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `RadioGroup` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="RadioGroup.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = RadioGroupProps.documentationProps('RadioGroup') +``` diff --git a/apps/docs/pages/@app-core/forms/Select.mdx b/apps/docs/pages/@app-core/forms/Select.mdx new file mode 100644 index 0000000..b87f4f4 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/Select.mdx @@ -0,0 +1,65 @@ +import { Select, getDocumentationProps } from '@app/forms/Select.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## Select + + +# Select + +```typescript copy +import { Select } from '@app/forms/Select.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## Select Props + + +
+ + +## Source Code + + +### File Location + +You can find the source of the `Select` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="Select.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = SelectProps.documentationProps('Select') +``` diff --git a/apps/docs/pages/@app-core/forms/Switch.mdx b/apps/docs/pages/@app-core/forms/Switch.mdx new file mode 100644 index 0000000..044a07d --- /dev/null +++ b/apps/docs/pages/@app-core/forms/Switch.mdx @@ -0,0 +1,65 @@ +import { Switch, getDocumentationProps } from '@app/forms/Switch.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## Switch + + +# Switch + +```typescript copy +import { Switch } from '@app/forms/Switch.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## Switch Props + + +
+ + +## Source Code + + +### File Location + +You can find the source of the `Switch` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="Switch.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = SwitchProps.documentationProps('Switch') +``` diff --git a/apps/docs/pages/@app-core/forms/TextArea.mdx b/apps/docs/pages/@app-core/forms/TextArea.mdx new file mode 100644 index 0000000..febc5ae --- /dev/null +++ b/apps/docs/pages/@app-core/forms/TextArea.mdx @@ -0,0 +1,152 @@ +import { TextArea, getDocumentationProps } from '@app/forms/TextArea.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## TextArea + + +# TextArea + +```typescript copy +import { TextArea } from '@app/forms/TextArea.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## TextArea Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const TextAreaProps = z.object({ + value: z + .string() + .optional(), + placeholder: z + .string() + .optional() + .example("Start typing..."), + className: z + .string() + .optional(), + placeholderClassName: z + .string() + .optional(), + placeholderTextColor: z + .string() + .default("hsl(var(--muted))"), + hasError: z + .boolean() + .optional(), + readOnly: z + .boolean() + .optional(), + disabled: z + .boolean() + .optional(), + multiline: z + .boolean() + .default(true) + .describe("See `numberOfLines`. Also disables some `textAlign` props."), + numberOfLines: z + .number() + .optional(), + textAlign: z + .enum(["left", "center", "right"]) + .default("left") + .describe("Might not work if multiline"), + textAlignVertical: z + .enum(["top", "center", "bottom"]) + .default("top") + .describe("Might not work if multiline"), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + value?: string | undefined; + placeholder?: string | undefined; + className?: string | undefined; + placeholderClassName?: string | undefined; + placeholderTextColor?: string; + hasError?: boolean; + readOnly?: boolean; + disabled?: boolean; + /** See `numberOfLines`. Also disables some `textAlign` props. */ + multiline?: boolean; + numberOfLines?: number | undefined; + /** Might not work if multiline */ + textAlign?: "left" | "center" | "right"; + /** Might not work if multiline */ + textAlignVertical?: "top" | "center" | "bottom"; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `TextArea` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="TextArea.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = TextAreaProps.documentationProps('TextArea') +``` diff --git a/apps/docs/pages/@app-core/forms/TextInput.mdx b/apps/docs/pages/@app-core/forms/TextInput.mdx new file mode 100644 index 0000000..92e4c85 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/TextInput.mdx @@ -0,0 +1,130 @@ +import { TextInput, getDocumentationProps } from '@app/forms/TextInput.styled' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## TextInput + + +# TextInput + +```typescript copy +import { TextInput } from '@app/forms/TextInput.styled' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## TextInput Props + + +
+ + + ### Props Schema + + +
+Show Props Schema + +```typescript copy +const TextInputProps = z.object({ + value: z + .string() + .optional(), + placeholder: z + .string() + .optional() + .example("Start typing..."), + className: z + .string() + .optional(), + placeholderClassName: z + .string() + .optional(), + placeholderTextColor: z + .string() + .default("hsl(var(--muted))"), + hasError: z + .boolean() + .optional(), + readOnly: z + .boolean() + .optional(), + disabled: z + .boolean() + .optional(), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +### Props Type + + +
+Show Props Types + +```typescript copy +{ + value?: string | undefined; + placeholder?: string | undefined; + className?: string | undefined; + placeholderClassName?: string | undefined; + placeholderTextColor?: string; + hasError?: boolean; + readOnly?: boolean; + disabled?: boolean; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ + +## Source Code + + +### File Location + +You can find the source of the `TextInput` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="TextInput.styled.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = TextInputProps.documentationProps('TextInput') +``` diff --git a/apps/docs/pages/@app-core/forms/_meta.ts b/apps/docs/pages/@app-core/forms/_meta.ts new file mode 100644 index 0000000..c001f70 --- /dev/null +++ b/apps/docs/pages/@app-core/forms/_meta.ts @@ -0,0 +1,11 @@ + +export default { + 'TextInput': 'TextInput', + 'TextArea': 'TextArea', + 'Switch': 'Switch', + 'Select': 'Select', + 'RadioGroup': 'RadioGroup', + 'NumberStepper': 'NumberStepper', + 'Checkbox': 'Checkbox', + 'CheckList': 'CheckList', +} diff --git a/apps/docs/pages/@app-core/resolvers/_meta.ts b/apps/docs/pages/@app-core/resolvers/_meta.ts new file mode 100644 index 0000000..6cd90c2 --- /dev/null +++ b/apps/docs/pages/@app-core/resolvers/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'healthCheck': 'healthCheck', +} diff --git a/apps/docs/pages/@app-core/resolvers/healthCheck.mdx b/apps/docs/pages/@app-core/resolvers/healthCheck.mdx new file mode 100644 index 0000000..3faa242 --- /dev/null +++ b/apps/docs/pages/@app-core/resolvers/healthCheck.mdx @@ -0,0 +1,434 @@ +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { View, Image } from '@app/primitives' + + + ## `healthCheck` - API + + +# healthCheck() - Resolver + + + + + + + + + + + + + +`healthCheck()` is a query resolver that allows you to get data from: + +- [Async Functions](#server-usage) during other resolver logic / GraphQL / API calls server-side +- [GraphQL](#graphql-query) - As a GraphQL query +- [API route](#nextjs-api-route) (GET) +- [Clientside Hooks](#client-usage) for calling the API with `react-query` from Web / Mobile + +
+ +## Resolver Config + +Input / output types, defaults, schemas and general config for the `healthCheck()` resolver are defined in its DataBridge file. Importable from: + +```typescript copy +import { healthCheckBridge } from '@app/core/resolvers/healthCheck.bridge' +``` + +
+ +### Input Shape + +You can find the schema used to validate the input arguments for the `healthCheck()` resolver in the bridge config: + +```typescript copy +const HealthCheckInput = healthCheckBridge.inputSchema +``` + +
+Show Input Schema + +```typescript copy +const HealthCheckInput = z.object({ + echo: z + .string() + .default("Hello World"), + verbose: z + .boolean() + .optional(), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ +If needed, you can extract the TypeScript type from the schema using `z.input()`, e.g.: + +```typescript copy +type HealthCheckInput = z.input +``` + + +
+Show Input Type + +```typescript copy +{ + echo?: string; + verbose?: boolean; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ +### Output Shape + +You can find the schema used to provide output defaults for the `healthCheck()` resolver in the bridge config too: + +```typescript copy +const HealthCheckOutput = healthCheckBridge.outputSchema +``` + +
+Show Output Schema + +```typescript copy +const HealthCheckOutput = z.object({ + echo: z + .string() + .optional(), + status: z + .literal("OK"), + alive: z + .boolean(), + kicking: z + .boolean(), + now: z + .string(), + aliveTime: z + .number(), + aliveSince: z + .date(), + serverTimezone: z + .string(), + requestHost: z + .string() + .optional(), + requestProtocol: z + .string() + .optional(), + requestURL: z + .string() + .optional(), + baseURL: z + .string() + .optional(), + backendURL: z + .string() + .optional(), + apiURL: z + .string() + .optional(), + graphURL: z + .string() + .optional(), + port: z + .number() + .int() + .nullable(), + debugPort: z + .number() + .int() + .nullable(), + nodeVersion: z + .string() + .optional(), + v8Version: z + .string() + .optional(), + systemArch: z + .string() + .optional(), + systemPlatform: z + .string() + .optional(), + systemRelease: z + .string() + .optional(), + systemFreeMemory: z + .number() + .optional(), + systemTotalMemory: z + .number() + .optional(), + systemLoadAverage: z + .array(z.number()) + .optional(), + context: z + .record(z.string(), z.unknown()) + .nullish(), +}) +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ +Here too, you can extract the TypeScript type from the schema using `z.output()`, e.g.: + +```typescript copy +type HealthCheckOutput = z.output +``` + +
+Show Output Type + +```typescript copy +{ + echo?: string | undefined; + status: "OK"; + alive: boolean; + kicking: boolean; + now: string; + aliveTime: number; + aliveSince: Date; + serverTimezone: string; + requestHost?: string | undefined; + requestProtocol?: string | undefined; + requestURL?: string | undefined; + baseURL?: string | undefined; + backendURL?: string | undefined; + apiURL?: string | undefined; + graphURL?: string | undefined; + port: number | null; + debugPort: number | null; + nodeVersion?: string | undefined; + v8Version?: string | undefined; + systemArch?: string | undefined; + systemPlatform?: string | undefined; + systemRelease?: string | undefined; + systemFreeMemory?: number | undefined; + systemTotalMemory?: number | undefined; + systemLoadAverage?: number[] | undefined; + context?: ({ + [x: string]: unknown; + } | null) | undefined; +} +``` +> 💡 Could be handy to copy-paste into an AI chat? +
+ +
+ +## Server Usage + +
+ +### `healthCheck()` function + +```typescript copy +import { healthCheck } from '@app/core/resolvers/healthCheck.resolver' +``` + +```typescript copy +// ... Later, in resolver or script logic ... +const output = await healthCheck({ ...inputArgs }) +// ?^ HealthCheckOutput +``` + +Note that using resolvers like `healthCheck()` as async functions is only available server-side, and might cause issues if imported into the client bundle. For client-side usage, use any of the other options below. + +
+ +## GraphQL Query + +
+ +### `healthCheckFetcher()` + +```typescript copy +import { healthCheckFetcher } from '@app/core/resolvers/healthCheck.query' +``` + +`healthCheckFetcher()` is a universal GraphQL query fetcher function. +It wil query the `healthCheck` resolver for you as a GraphQL query: + +```typescript copy +const response = await healthCheckFetcher({ healthCheckArgs: { ...inputArgs } }) +// ?^ { healthCheck: HealthCheckOutput } +``` + +Just make sure the `healthCheckArgs` input matches the [`HealthCheckInput`](#input-shape) schema. + +If you prefer, you can also use the following GraphQL snippet in your own GraphQL fetching logic: + +
+ +### GraphQL Query Snippet + +```graphql copy +query healthCheck($healthCheckArgs: HealthCheckInput!) { + healthCheck(args: $healthCheckArgs) { + echo + status + alive + kicking + now + aliveTime + aliveSince + serverTimezone + requestHost + requestProtocol + requestURL + baseURL + backendURL + apiURL + graphURL + port + debugPort + nodeVersion + v8Version + systemArch + systemPlatform + systemRelease + systemFreeMemory + systemTotalMemory + systemLoadAverage + context { + zodType + baseType + } + } +} +``` + +
+ +### Custom Query + +Using a custom query, you can omit certain fields you don't need and request only what's necessary. + +If you do, we suggest using `graphqlQuery()`, as it works seamlessly on the server, browser and mobile app: + +```typescript copy +import { graphql } from '@app/core/graphql/graphql' +import { graphqlQuery } from '@app/core/graphql/graphqlQuery' + +const query = graphql(` + query healthCheck($healthCheckArgs: HealthCheckInput!) { + healthCheck(args: $healthCheckArgs) { + // -i- ... type hints for the fields you need ... -i- + } + } +`) + +const response = await graphqlQuery(query, { healthCheckArgs: { ...inputArgs } }) +// ?^ { healthCheck: HealthCheckOutput } +``` + +Just make sure the `healthCheckArgs` input matches the [`HealthCheckInput`](#input-shape) schema. + +
+ +## Next.js API Route + +
+ +### `GET` requests + +```shell copy +GET /api/health?... +``` + +Provide query parameters as needed (e.g. `?someArg=123`). + +Make sure the params / query input match the [`HealthCheckInput`](#input-shape) schema. + +
+ +
+ +## Client Usage + +
+ +### Custom `react-query` hook + +> e.g. In the `healthCheck.query.ts` file: + +```typescript copy +import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query' +``` + +```typescript copy + +export const useHealthCheckQuery = ( + input: HealthCheckQueryInput, + options?: Omit, 'queryFn' | 'queryKey'> & { + queryKey?: QueryKey, + }, +) => { + return useQuery({ + queryKey: ['healthCheckFetcher', input], + queryFn: (context) => healthCheckFetcher(input), + ...options, + }) +} +``` + +Be sure to check the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options you might want to prefill or abstract. + +
+ +### Usage in React + +```typescript copy +import { useHealthCheckQuery } from '@app/core/resolvers/healthCheck.query' +``` + +```typescript copy +const { data, error, isLoading } = useHealthCheckQuery({ healthCheckArgs: /* ... */ }, { + // ... any additional options ... +}) +``` + +Be sure to check the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options. + +
+ +## Developer Notes + +### Why this resolver? + +We included this resolver in the starterkit as a reference implementation for a simple API resolver. + +While it can be used to verify that the application is running and responsive, its main goals for inclusion are to demonstrate: + +- How to create a basic API resolver. +- What our way of working of creating resolvers gets you. (typesafety, async function, REST API, GraphQL API, docs) +- Automatic docgen for API resolvers. (this entire docs page) + +### Adding your own resolvers + +If you want to quickly replicate this way of working for setting up APIs with all the side-effects listed above, you can use our resolver generator: + +```bash copy +npm run add:resolver +``` + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic API docs were auto-generated with `npm run regenerate-docs`. This happens from `.bridge.ts` files in any `/resolvers/` folder. + +You can opt-out of this by adding `export const optOut = true` somewhere in the file. + diff --git a/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx b/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx new file mode 100644 index 0000000..9636901 --- /dev/null +++ b/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx @@ -0,0 +1,156 @@ +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { View, Image } from '@app/primitives' + + + ## HealthCheckInput + + +# HealthCheckInput + +```typescript copy +import { HealthCheckInput } from '@app/core/schemas/HealthCheckInput' +``` + +### Location + + + + + + + + + + + +### Zod Schema + +What the schema would look like when defined with `z.object()` in Zod V3: + +```typescript copy +const HealthCheckInput = z.object({ + echo: z + .string() + .default("Hello World"), + verbose: z + .boolean() + .optional(), +}) +``` + +> (💡 Could be handy to copy-paste this schema info into an AI chat assistant) +
+ +### Type Definition + +You can extract the TypeScript type from the schema using `z.input()`, `z.output()` or `z.infer()` methods. e.g.: + +```typescript copy +type HealthCheckInput = z.input +``` + +What the resulting TypeScript type would look like: + +```typescript copy +{ + echo?: string; + verbose?: boolean; +} +``` + +> (💡 Could be handy to copy-paste this type info into an AI chat assistant) +
+ +### Usage - Validation + +To validate data against this schema, you have a few options: + +```typescript copy +// Throws if invalid +const healthCheckInput = HealthCheckInput.parse(data) + +// Returns { success: boolean, data?: T, error?: ZodError } +const healthCheckInput = HealthCheckInput.safeParse(data) + +``` + +> This might be useful for parsing API input data or validating form data before submission. + +> You can also directly integrate this schema with form state managers like our own: + +
+ +### Usage - Form State + +```typescript copy +import { useFormState } from '@green-stack/forms/useFormState' + +const formState = useFormState(HealthCheckInput, { + initialValues: { /* ... */ }, // Provide initial values? + validateOnMount: true, // Validate on component mount? +}) + +``` + +Learn more about using schemas for form state in our [Form Management Docs](/form-management). + +
+ +### Usage - Component Props / Docs + +Another potential use case for the 'HealthCheckInput' schema is to type component props, provide default values and generate documentation for that component: + +```typescript copy +export const HealthCheckInputComponentProps = HealthCheckInput.extend({ + // Add any additional props here +}) + +export type HealthCheckInputComponentProps = z.input + +/* --- --------------- */ + +export const HealthCheckInputComponent = (rawProps: HealthCheckInputComponentProps) => { + + // Extract the props and apply defaults + infer resulting type + const props = ComponentProps.applyDefaults(rawProps) + + // ... rest of the component logic ... + +} + +/* --- Documentation --------------- */ + +export const documentationProps = HealthCheckInputComponentProps.documentationProps('HealthCheckInputComponent') + +``` + +
+ +## Developer Notes + +### Why this schema? + +We included this schema in the starterkit as a reference guide for what schema docs look like. + +It's also reused in the [`healthCheck()`](/@app-core/resolvers/healthCheck) resolver, which has it's own auto-generated docs example. + +Learn more about how zod schemas work as the [Single Source of Truth](/single-sources-of-truth) for APIs, forms, GraphQL and other automations we consider the "right abstractions". + +### Adding your own schemas + +If you want to quickly replicate this way of working for setting up schemas with all the side-effects listed above, you can use our schema generator: + +```bash copy +npm run add:schema +``` + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic schema docs were auto-generated with `npm run regenerate-docs`. This happens automatically for schema files in any `\schemas\` folder. You can opt-out of this by adding `// export const optOut = true` somewhere in the file. + diff --git a/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx b/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx new file mode 100644 index 0000000..b40e90f --- /dev/null +++ b/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx @@ -0,0 +1,249 @@ +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { View, Image } from '@app/primitives' + + + ## HealthCheckOutput + + +# HealthCheckOutput + +```typescript copy +import { HealthCheckOutput } from '@app/core/schemas/HealthCheckOutput' +``` + +### Location + + + + + + + + + + + +### Zod Schema + +What the schema would look like when defined with `z.object()` in Zod V3: + +```typescript copy +const HealthCheckOutput = z.object({ + echo: z + .string() + .optional(), + status: z + .literal("OK"), + alive: z + .boolean(), + kicking: z + .boolean(), + now: z + .string(), + aliveTime: z + .number(), + aliveSince: z + .date(), + serverTimezone: z + .string(), + requestHost: z + .string() + .optional(), + requestProtocol: z + .string() + .optional(), + requestURL: z + .string() + .optional(), + baseURL: z + .string() + .optional(), + backendURL: z + .string() + .optional(), + apiURL: z + .string() + .optional(), + graphURL: z + .string() + .optional(), + port: z + .number() + .int() + .nullable(), + debugPort: z + .number() + .int() + .nullable(), + nodeVersion: z + .string() + .optional(), + v8Version: z + .string() + .optional(), + systemArch: z + .string() + .optional(), + systemPlatform: z + .string() + .optional(), + systemRelease: z + .string() + .optional(), + systemFreeMemory: z + .number() + .optional(), + systemTotalMemory: z + .number() + .optional(), + systemLoadAverage: z + .array(z.number()) + .optional(), + context: z + .record(z.string(), z.unknown()) + .nullish(), +}) +``` + +> (💡 Could be handy to copy-paste this schema info into an AI chat assistant) +
+ +### Type Definition + +You can extract the TypeScript type from the schema using `z.input()`, `z.output()` or `z.infer()` methods. e.g.: + +```typescript copy +type HealthCheckOutput = z.input +``` + +What the resulting TypeScript type would look like: + +```typescript copy +{ + echo?: string | undefined; + status: "OK"; + alive: boolean; + kicking: boolean; + now: string; + aliveTime: number; + aliveSince: Date; + serverTimezone: string; + requestHost?: string | undefined; + requestProtocol?: string | undefined; + requestURL?: string | undefined; + baseURL?: string | undefined; + backendURL?: string | undefined; + apiURL?: string | undefined; + graphURL?: string | undefined; + port: number | null; + debugPort: number | null; + nodeVersion?: string | undefined; + v8Version?: string | undefined; + systemArch?: string | undefined; + systemPlatform?: string | undefined; + systemRelease?: string | undefined; + systemFreeMemory?: number | undefined; + systemTotalMemory?: number | undefined; + systemLoadAverage?: number[] | undefined; + context?: ({ + [x: string]: unknown; + } | null) | undefined; +} +``` + +> (💡 Could be handy to copy-paste this type info into an AI chat assistant) +
+ +### Usage - Validation + +To validate data against this schema, you have a few options: + +```typescript copy +// Throws if invalid +const healthCheckOutput = HealthCheckOutput.parse(data) + +// Returns { success: boolean, data?: T, error?: ZodError } +const healthCheckOutput = HealthCheckOutput.safeParse(data) + +``` + +> This might be useful for parsing API input data or validating form data before submission. + +> You can also directly integrate this schema with form state managers like our own: + +
+ +### Usage - Form State + +```typescript copy +import { useFormState } from '@green-stack/forms/useFormState' + +const formState = useFormState(HealthCheckOutput, { + initialValues: { /* ... */ }, // Provide initial values? + validateOnMount: true, // Validate on component mount? +}) + +``` + +Learn more about using schemas for form state in our [Form Management Docs](/form-management). + +
+ +### Usage - Component Props / Docs + +Another potential use case for the 'HealthCheckOutput' schema is to type component props, provide default values and generate documentation for that component: + +```typescript copy +export const HealthCheckOutputComponentProps = HealthCheckOutput.extend({ + // Add any additional props here +}) + +export type HealthCheckOutputComponentProps = z.input + +/* --- --------------- */ + +export const HealthCheckOutputComponent = (rawProps: HealthCheckOutputComponentProps) => { + + // Extract the props and apply defaults + infer resulting type + const props = ComponentProps.applyDefaults(rawProps) + + // ... rest of the component logic ... + +} + +/* --- Documentation --------------- */ + +export const documentationProps = HealthCheckOutputComponentProps.documentationProps('HealthCheckOutputComponent') + +``` + +
+ +## Developer Notes + +### Why this schema? + +We included this schema in the starterkit as a reference guide for what schema docs look like. + +It's also reused in the [`healthCheck()`](/@app-core/resolvers/healthCheck) resolver, which has it's own auto-generated docs example. + +Learn more about how zod schemas work as the [Single Source of Truth](/single-sources-of-truth) for APIs, forms, GraphQL and other automations we consider the "right abstractions". + +### Adding your own schemas + +If you want to quickly replicate this way of working for setting up schemas with all the side-effects listed above, you can use our schema generator: + +```bash copy +npm run add:schema +``` + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic schema docs were auto-generated with `npm run regenerate-docs`. This happens automatically for schema files in any `\schemas\` folder. You can opt-out of this by adding `// export const optOut = true` somewhere in the file. + diff --git a/apps/docs/pages/@app-core/schemas/_meta.ts b/apps/docs/pages/@app-core/schemas/_meta.ts new file mode 100644 index 0000000..7437894 --- /dev/null +++ b/apps/docs/pages/@app-core/schemas/_meta.ts @@ -0,0 +1,5 @@ + +export default { + 'HealthCheckOutput': 'HealthCheckOutput', + 'HealthCheckInput': 'HealthCheckInput', +} diff --git a/apps/docs/pages/@app-core/screens/FormsScreen.mdx b/apps/docs/pages/@app-core/screens/FormsScreen.mdx new file mode 100644 index 0000000..8f26809 --- /dev/null +++ b/apps/docs/pages/@app-core/screens/FormsScreen.mdx @@ -0,0 +1,65 @@ +import { FormsScreen, getDocumentationProps } from '@app/screens/FormsScreen' +import { ComponentDocs } from '@app/core/mdx/ComponentDocs' +import { TitleWrapper } from '@app/docs/components/Hidden' +import { FileTree, Callout } from 'nextra/components' + + + ## FormsScreen + + +# FormsScreen + +```typescript copy +import { FormsScreen } from '@app/screens/FormsScreen' +``` + + + ### Interactive Preview + + + + ### Code Example + + + + ## FormsScreen Props + + +
+ + +## Source Code + + +### File Location + +You can find the source of the `FormsScreen` component in the following location: + + + + + + + + + + + +
+ +## Other + +### Disclaimer - Automatic Docgen + + +These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g: + + +```tsx /getDocumentationProps/ /documentationProps/ copy filename="FormsScreen.tsx" +/* --- Docs ---------------------- */ + +export const getDocumentationProps = FormsScreenProps.documentationProps('FormsScreen') +``` diff --git a/apps/docs/pages/@app-core/screens/_meta.ts b/apps/docs/pages/@app-core/screens/_meta.ts new file mode 100644 index 0000000..beb9c5b --- /dev/null +++ b/apps/docs/pages/@app-core/screens/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'FormsScreen': 'FormsScreen', +} diff --git a/apps/docs/pages/@green-stack-core/_meta.ts b/apps/docs/pages/@green-stack-core/_meta.ts new file mode 100644 index 0000000..f0f4b84 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'schemas': 'schemas', +} diff --git a/apps/docs/pages/@green-stack-core/components/Image.mdx b/apps/docs/pages/@green-stack-core/components/Image.mdx new file mode 100644 index 0000000..97e964b --- /dev/null +++ b/apps/docs/pages/@green-stack-core/components/Image.mdx @@ -0,0 +1,261 @@ +import { Image } from '@app/primitives' +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## Usage - `Image` + + + + +# Universal `Image` component + +```tsx copy +import { Image } from '@green-stack/components/Image' +``` + +### Platform Optimized Images + +Some primitives like the `Image` component have optimized versions for each environment: + +- `next/image` for web +- `expo-image` for mobile + +To automatically use the right one per render context, we've provided our own universal `Image` component: + +```tsx copy + +``` + +Which you might wish to wrap with Nativewind to provide class names to: + +```tsx {5} /styled/2 filename="styled.tsx" +import { Image as UniversalImage } from '@green-stack/components/Image' +// ☝️ Import the universal Image component +import { styled } from 'nativewind' + +// ⬇⬇⬇ + +export const Image = styled(UniversalImage, '') +// ☝️ Adds the ability to assign tailwind classes +``` + +
+ +### Image Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `src` | `string \| StaticImageData` | Required | Image source - can be URL or imported image | +| `alt` | `string` | `'Alt description missing in image'` | Alt text for accessibility and SEO | +| `width` | `number \| string` | Required* | Width in pixels or percentage | +| `height` | `number \| string` | Required* | Height in pixels or percentage | +| `className` | `string` | - | Tailwind classes for styling | +| `style` | `StyleProp` | `{}` | Additional styles | +| `priority` | `'high' \| 'normal'` | `'normal'` | Loading priority - 'high' for LCP elements | +| `onError` | `(error: any) => void` | - | Called on image loading error | +| `onLoadEnd` | `() => void` | - | Called when image load completes | +| `fill` | `boolean` | - | Fill parent container (requires relative positioning) | +| `contentFit` | `'cover' \| 'contain' \| 'fill' \| 'none' \| 'scale-down'` | `'cover'` | How image fits container | +| `cachePolicy` | `'none' \| 'disk' \| 'memory' \| 'memory-disk'` | `'disk'` | Image caching strategy | +| `blurRadius` | `number` | `0` | Blur effect radius in points | +| `quality` | `number` | `75` | Image quality (1-100) | +| `sizes` | `string` | - | Responsive image sizes hint | +| `unoptimized` | `boolean` | `false` | Skip image optimization | + +*Required unless using `fill` or static import + +
+TypeScript Definition + +You can find the TypeScript definition for our Universal `Image` component in `Image.types.ts`: + +```typescript filename="Image.types.ts" +type UniversalImageProps = { + + // -- Universal props -- + + /** + * Universal, will affect both Expo & Next.js - Must be one of the following: + * - A path string like `'/assets/logo.png'`. This can be either an absolute external URL, or an internal path depending on the loader prop. + * - A statically imported image file, like `import logo from './logo.png'` or `require('./logo.png')`. + * + * When using an external URL, you must add it to `remotePatterns` in `next.config.js`. + * @platform web, android, ios @framework expo, next.js */ + src: string | StaticImport + + width?: number | `${number}` | `${number}%` + height?: number | `${number}` | `${number}%` + + /** Universal, will affect both Expo & Next.js + * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */ + className?: string + + /** Universal, will affect both Expo & Next.js + * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */ + style?: StyleProp | ExpoImageProps['style'] + + alt?: string + priority?: "low" | "normal" | "high" | null + + onError?: ((event: ImageErrorEventData) => void) + onLoadEnd?: (() => void) + + // -- '@next/image' specific props -- + + /** Custom function used to resolve image URLs. A loader is a function returning a URL string for the image, given the following parameters: `src`, `width`, `quality` (`number` from 0 - 1) Alternatively, you can use the [loaderFile](https://nextjs.org/docs/pages/api-reference/components/image#loaderfile) configuration in next.config.js to configure every instance of next/image in your application, without passing a prop. */ + loader?: ImageLoader + + fill?: boolean + sizes?: string + quality?: number | `${number}` + nextPlaceholder?: PlaceholderValue | 'blur' | 'empty' | `data:image/${string}` + loading?: 'lazy' | 'eager' + blurDataURL?: string + unoptimized?: boolean + + // -- 'expo-image' specific props -- + + accessibilityLabel?: string + accessible?: boolean + allowDownscaling?: boolean + autoplay?: boolean + blurRadius?: number + cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk' + contentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' + contentPosition?: ImageContentPosition | 'top' | 'bottom' | 'left' | 'right' | 'center' | 'top left' | 'top right' | ... + enableLiveTextInteraction?: ExpoImageProps['enableLiveTextInteraction'] + focusable?: boolean + expoPlaceholder?: ExpoImageProps['expoPlaceholder'] | string | StaticImport + onLoadStart?: (() => void) + onProgress?: ((event: ImageProgressEventData) => void) + placeholderContentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' + recyclingKey?: string | null + responsivePolicy?: 'static' | 'initial' | 'live' +} +``` + +
+ +
+Zod Schema + +```typescript +import { z } from 'zod' + +const UniversalImageSchema = z.object({ + // Universal props + src: z.union([z.string(), z.any()]), // StaticImageData type + alt: z.string().optional(), + width: z.union([z.number(), z.string()]).optional(), + height: z.union([z.number(), z.string()]).optional(), + className: z.string().optional(), + style: z.any().optional(), + priority: z.enum(['high', 'normal']).optional(), + onError: z.function().optional(), + onLoadEnd: z.function().optional(), + + // Next.js specific + loader: z.function().optional(), + fill: z.boolean().optional(), + sizes: z.string().optional(), + quality: z.number().min(1).max(100).optional(), + nextPlaceholder: z.enum(['blur', 'empty']).optional(), + loading: z.enum(['lazy', 'eager']).optional(), + blurDataURL: z.string().optional(), + unoptimized: z.boolean().optional(), + + // Expo specific + accessibilityLabel: z.string().optional(), + accessible: z.boolean().optional(), + allowDownscaling: z.boolean().optional(), + autoplay: z.boolean().optional(), + blurRadius: z.number().optional(), + cachePolicy: z.enum(['none', 'disk', 'memory', 'memory-disk']).optional(), + contentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(), + contentPosition: z.string().optional(), + enableLiveTextInteraction: z.boolean().optional(), + focusable: z.boolean().optional(), + expoPlaceholder: z.any().optional(), + onLoadStart: z.function().optional(), + onProgress: z.function().optional(), + placeholderContentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(), + recyclingKey: z.string().optional(), + responsivePolicy: z.enum(['static', 'initial', 'live']).optional(), +}) +``` + +
+ +
+ +## React Portability Patterns + +Both Next.js and Expo have their own optimized `Image` components. This is why there are also versions specifically for each of those environments: + + + + + + + + + + + + + + +- `Image.next.tsx` uses `next/image` for web +- `Image.expo.tsx` uses `expo-image` for mobile +- `Image.types.ts` ensures there is a shared type for both implementations + +Finally, `Image.tsx` will retrieve whichever implementation was provided as `contextImage` to the `` component, which is further passed to ``: + +```tsx /.expo/ /useExpoImage/ filename="ExpoRootLayout.tsx" copy +import { Image as ExpoImage } from '@green-stack/components/Image.expo' + +// ... Later ... + + + ... + +``` + +```tsx /.next/ /useNextImage/ filename="NextRootLayout.tsx" copy +import { Image as NextImage } from '@green-stack/components/Image.next' + +// ... Later ... + + + ... + +``` + +### Why this pattern? + +The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of components across different flavours of writing React. + +On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way. + +But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `Image` component. + +### Supporting more environments + +Just add your own `Image..tsx` file that respects the shared types, and then pass it to the `` component as `contextImage`. diff --git a/apps/docs/pages/@green-stack-core/components/_meta.ts b/apps/docs/pages/@green-stack-core/components/_meta.ts new file mode 100644 index 0000000..c2ade70 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/components/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'Image': 'Image', +} diff --git a/apps/docs/pages/@green-stack-core/generators/_meta.ts b/apps/docs/pages/@green-stack-core/generators/_meta.ts new file mode 100644 index 0000000..58c79f9 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/_meta.ts @@ -0,0 +1,11 @@ + +export default { + 'add-workspace': 'add-workspace', + 'add-script': 'add-script', + 'add-schema': 'add-schema', + 'add-route': 'add-route', + 'add-resolver': 'add-resolver', + 'add-generator': 'add-generator', + 'add-form': 'add-form', + 'add-dependencies': 'add-dependencies', +} diff --git a/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx b/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx new file mode 100644 index 0000000..cbd0411 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx @@ -0,0 +1,56 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-dependencies` - Script + + + + +# `add-dependencies` + +```md copy +npm run add:dependencies -- --args +``` + +```md copy +npx turbo gen dependencies --args +``` + +Less of a generator and more of a utility script, this generator **installs Expo SDK compatible versions** of the specified dependencies in the selected workspace's `package.json`. + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|---------------|--------------|-----------------------------------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to install these dependencies?
=> e.g. `@app/core` or `some-package` | +| dependencies | text | Which dependencies should we install Expo SDK compatible versions for?
=> `string` (comma separated) | + +
+ +### Resulting File Changes + +```bash +# Installs dependencies in the selected workspace's package.json ❇️ +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-form.mdx b/apps/docs/pages/@green-stack-core/generators/add-form.mdx new file mode 100644 index 0000000..41af0e8 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-form.mdx @@ -0,0 +1,57 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-form` - Generator + + + + +# `add-form` + +```md copy +npm run add:form -- --args +``` + +```md copy +npx turbo gen add-form --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|---------------|--------------|-----------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this form?
=> e.g. `features/@app-core` | +| formSchema | autocomplete | Which zod / input schema should we use?
=> e.g. `HealthCheckInput` / `new` | +| formHookName | text | What should the form hook be named?
=> `string` (e.g. `useSomeSchemaState`) | + +
+ +### Resulting File Changes + +```bash +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── hooks/ + └── {formHookName}.ts ❇️ # <- Will integrate with chosen / new schema +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-generator.mdx b/apps/docs/pages/@green-stack-core/generators/add-generator.mdx new file mode 100644 index 0000000..b30256f --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-generator.mdx @@ -0,0 +1,58 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-generator` - Generator + + + + +# `add-generator` + +```md copy +npm run add:generator -- --args +``` + +```md copy +npx turbo gen generator --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|-----------------|--------------|--------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this generator?
=> e.g. `features/@app-core` | +| generatedEntity | text | What is being generated?
=> `string` (e.g. `component`) | + +
+ +### Resulting File Changes + +```bash +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── generators/ + └── add-{generatedEntity}.ts ❇️ + +package.json ➕ # <- adds a root package.json script as well +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx b/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx new file mode 100644 index 0000000..f9c5e77 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx @@ -0,0 +1,75 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-resolver` - Generator + + + + +# `add-resolver` + +```md copy +npm run add:resolver -- --args ... +``` + +```md copy +npx turbo gen resolver --args ... +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this resolver?
=> e.g. `features/@app-core` | +| resolverName | text | What is the resolver name?
=> `string` | +| resolverDescription | text | Optional description: What will this data resolver do?
=> `string` | +| resolverType | radio | Will this resolver query or mutate data?
=> `query` / `mutation` | +| generatables | checklist | What would you like to generate linked to this resolver?
=> `GRAPHQL` / `GET` / `POST` / `PUT` / `DELETE` / `formHook` | +| inputSchemaTarget | autocomplete | Which schema should we use for the resolver inputs?
=> e.g. `HealthCheckInput` | +| inputSchemaName | text | What will you call this new input schema?
=> `string` | +| outputSchemaTarget | autocomplete | Which schema should we use for the resolver output?
=> e.g. `HealthCheckOutput` | +| outputSchemaName | text | What will you call this new output schema?
=> `string` | +| apiPath | text | What API path would you like to use for REST?
=> e.g. `/api/some/endpoint/with/[params]/` | +| formHookName | text | What should the form hook be called?
=> e.g. `useSomeResolver` | + +
+ +### Resulting File Changes + +```bash +/apps/next/ + └── app/(generated)/{routePath}/route.ts ❇️ # <- e.g. '/api/some/endpoint/with/[params]/' (Next.js route handler) + +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── resolvers/ + └── {resolverName}.bridge.ts ❇️ # <- Bridge Metadata file for the resolver and API + └── {resolverName}.resolver.ts ❇️ # <- Reusable Fn with Business logic (server only) + └── {resolverName}.{resolverType}.ts ❇️ # <- e.g. `query` / `mutation` fetcher + └── routes/ + └── {apiPath}/ ❇️ # <- e.g. '/api/some/endpoint/with/[params]/' + └── route.ts ❇️ # <- If `GET` / `POST` / `PUT` / `DELETE` / `GRAPHQL` was selected + └── hooks/ + └── {formHookFileName}.ts ❇️ # <- If `formHook` selected, e.g. `useResolverFormState.ts` +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-route.mdx b/apps/docs/pages/@green-stack-core/generators/add-route.mdx new file mode 100644 index 0000000..029426e --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-route.mdx @@ -0,0 +1,66 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-route` - Generator + + + + +# `add-route` + +```md copy +npm run add:route -- --args +``` + +```md copy +npx turbo gen route --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|------------------|--------------|--------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this route?
=> e.g. `features/@app-core` | +| screenName | text | What should the screen component be called?
=> `string` | +| routePath | text | What url do you want this route on?
=> e.g. `/some/path` | +| initialDataTarget| autocomplete | Would you like to fetch initial data from a resolver?
=> `string` | + +
+ +### Resulting File Changes + +```bash +/apps/expo/ + └── app/(generated)/{routePath}/index.tsx ❇️ # <- e.g. '/posts/[slug]/' (expo-router) +/apps/next/ + └── app/(generated)/{routePath}/page.tsx ❇️ # <- e.g. '/posts/[slug]/' (Next.js app router) + +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── screens/ + └── {ScreenName}.tsx ❇️ + └── routes/ + └── {routePath}/ ❇️ # <- e.g. '/posts/[slug]/' + └── index.tsx ❇️ # -> re-exported to `@app/next` and `@app/expo` +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-schema.mdx b/apps/docs/pages/@green-stack-core/generators/add-schema.mdx new file mode 100644 index 0000000..22cf1aa --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-schema.mdx @@ -0,0 +1,57 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-schema` - Generator + + + + +# `add-schema` + +```md copy +npm run add:schema -- --args +``` + +```md copy +npx turbo gen schema --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|------------------|--------------|---------------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this schema?
=> e.g. `features/@app-core` | +| schemaName | text | What is the schema name?
=> `string` | +| schemaDescription| text | Optional description: What data structure does this schema describe?
=> `string` | + +
+ +### Resulting File Changes + +```bash +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── schemas/ + └── {schemaName}.schema.ts ❇️ +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-script.mdx b/apps/docs/pages/@green-stack-core/generators/add-script.mdx new file mode 100644 index 0000000..b8e54c7 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-script.mdx @@ -0,0 +1,61 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-script` - Generator + + + + +# `add-script` + +```md copy +npm run add:script -- --args +``` + +```md copy +npx turbo gen script --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|-----------------|--------------|-------------------------------------------------------------------------------| +| workspacePath | autocomplete | Where would you like to add this script?
=> e.g. `features/@app-core` | +| scriptName | text | What is the script name?
=> `string` | +| scriptFileName | text | What should we name the script file?
=> `string` | + +
+ +### Resulting File Changes + +```bash +/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/' + └── scripts/ + └── {scriptFileName}.ts ❇️ + └── package.json ➕ # <- adds the script to the package.json scripts section + +package.json ➕ # <- adds a root package.json script as well +turbo.json ➕ # <- adds a turbo.json monorepo script config entry +``` + +
diff --git a/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx b/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx new file mode 100644 index 0000000..1769a65 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx @@ -0,0 +1,61 @@ +import { FileTree } from 'nextra/components' +import { Image } from '@app/primitives' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## `add-workspace` - Generator + + + + +# `add-workspace` + +```md copy +npm run add:workspace -- --args +``` + +```md copy +npx turbo gen add-workspace --args +``` + + + + + + + + + + + +
+ +### Prompt Arguments + +| Argument | Type | Question / Description | +|--------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------| +| workspaceType | radio | What type of workspace would you like to generate?
=> `package` / `feature` | +| folderName | text | What foldername do you want to give this workspace?
=> `string` | +| packageName | text | What package name would you like to import from?
=> `string` | +| workspaceStructure | checklist | Optional: What will this workspace contain?
=> `schemas`, `resolvers`, `components`, `hooks`, `screens`, `routes`, ... | +| packageDescription | text | Optional: How would you shortly describe the package?
=> `string` | + +
+ +### Resulting Files + +```bash +/{workspaceType}/ # <- e.g. packages/ or features/ + └── {folderName}/ ❇️ + └── {workspaceStructure}/ ❇️ # <- e.g. schemas/, resolvers/, components/, ... + └── package.json ❇️ + └── tsconfig.json ❇️ +``` + +
diff --git a/apps/docs/pages/@green-stack-core/navigation/Link.mdx b/apps/docs/pages/@green-stack-core/navigation/Link.mdx new file mode 100644 index 0000000..3fdcbed --- /dev/null +++ b/apps/docs/pages/@green-stack-core/navigation/Link.mdx @@ -0,0 +1,202 @@ +import { Image } from '@app/primitives' +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper, LLMOptimized } from '@app/docs/components/Hidden' + + + ## Usage - `Link` + + + + +# Universal `Link` component + +If you import the `Link` component from `@green-stack/navigation`, it will automatically use the correct navigation system for the platform you are on. + +```tsx copy +import { Link } from '@green-stack/navigation' +``` + +However, you can also ***import it from `@app/primitives` to apply tailwind styles***: + +```tsx copy +import { Link } from '@app/primitives' +``` + +You can use the `href` prop to navigate to a new page: + +```tsx {3} + + See example + +``` + +
+ +### `UniversalLinkProps` + +| Property | Type | Description | +|------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| children | React.ReactNode | The content to be rendered inside the link. | +| href | string \| HREF | The path to route to on web or mobile. String only. Hints for internal routes provided through codegen. | +| style | | Style prop: https://reactnative.dev/docs/text#style | +| className | string | Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead. | +| replace | boolean | Should replace the current route without adding to the history - Default: false. | +| onPress | | Extra handler that fires when the link is pressed. | +| target | | Specifies where to display the linked URL. | +| asChild | boolean | **Mobile only** - Forward props to child component. Useful for custom buttons - Default: false. | +| push | boolean | **Mobile only** - Should push the current route, always adding to the history - Default: true. | +| testID | string \| undefined | **Mobile only** - Used to locate this view in end-to-end tests. | +| nativeID | string \| undefined | **Mobile only** - Used to reference react managed views from native code. @deprecated use `id` instead. | +| id | string \| undefined | **Mobile only** - Used to reference react managed views from native code. | +| allowFontScaling | | **Mobile only** - Specifies whether fonts should scale to respect Text Size accessibility settings. | +| numberOfLines | | **Mobile only** - Specifies the maximum number of lines to use for rendering text. | +| maxFontSizeMultiplier | | **Mobile only** - Specifies the maximum scale factor for text. | +| suppressHighlighting | | **Mobile only** - When true, no visual change is made when text is pressed down. | +| scroll | boolean | **Web only** - Whether to override the default scroll behavior - Default: false. | +| shallow | boolean | **Web only** - Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps - Default: false. | +| passHref | boolean | **Web only** - Forces `Link` to send the `href` property to its child - Default: false. | +| prefetch | boolean | **Web only** - Prefetch the page in the background. Any `` that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover. Pages using [Static Generation](https://docs.expo.dev/router/reference/static-rendering/) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. - Default: true | +| locale | string \| false | **Web only** - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. | +| as | Url \| undefined | **Web only** - Optional decorator for the path that will be shown in the browser URL bar. | + + + +Here's what that would look like in TypeScript: + +```tsx +type UniversalLinkProps = { + + children: React.ReactNode; + + /** Universal - The path to route to on web or mobile. String only. */ + href: HREF; + + /** Universal - Style prop: https://reactnative.dev/docs/text#style */ + style?: StyleProp; + + /** -!- Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead */ + className?: string; // never; + + /** Universal - Should replace the current route without adding to the history - Default: false. */ + replace?: boolean; + + /** Universal - Extra handler that fires when the link is pressed. */ + onPress?: ((e: MouseEvent | GestureResponderEvent) => void) | null | undefined; + + /** Universal - */ + target?: "_self" | "_blank" | "_parent" | "_top" | undefined; + + // - Expo - + + /** Mobile only - Forward props to child component. Useful for custom buttons - Default: false */ + asChild?: boolean; + + /** Mobile only - Should push the current route, always adding to the history - Default: true */ + push?: boolean; + + /** Mobile only - Used to locate this view in end-to-end tests. */ + testID?: string | undefined; + + /** Mobile only - Used to reference react managed views from native code. @deprecated use `id` instead. */ + nativeID?: string | undefined; + id?: string | undefined; + + allowFontScaling?: boolean | undefined; + numberOfLines?: number | undefined; + maxFontSizeMultiplier?: number | null | undefined; + suppressHighlighting?: boolean | undefined; + + // - Next - + + /** Web only - Whether to override the default scroll behavior - Default: false */ + scroll?: boolean; + + /** Web only - Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps - Default: false */ + shallow?: boolean; + + /** Web only - Forces `Link` to send the `href` property to its child - Default: false */ + passHref?: boolean; + + /** Web only - Prefetch the page in the background. Any `` that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover. Pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. - Defaultvalue: true */ + prefetch?: boolean; + + /** Web only - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. */ + locale?: string | false; + + /** Web only - Optional decorator for the path that will be shown in the browser URL bar. Before Next.js 9.5.3 this was used for dynamic routes, check our [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes) to see how it worked. Note: when this path differs from the one provided in `href` the previous `href`/`as` behavior is used as shown in the [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes). */ + as?: Url | undefined; + +} +``` + + + +
+ +## React Portability Patterns + +Each environment has it's own optimized Link component. This is why there are also versions specifically for each of those environments: + + + + + + + + + + + + + + +- `Link.next.tsx` is optimized for the Next.js app router. +- `Link.expo.tsx` is optimized for Expo Router. +- `Link.types.ts` ensures both implementations are compatible with the same interface, allowing you to use the same `Link` component across both Expo and Next.js environments. + +The main `Link.tsx` retrieves whichever implementation was provided as `contextLink` to the `` component, which is further passed to ``: + +```tsx /.expo/ /useExpoLink/ filename="ExpoRootLayout.tsx" copy +import { Link as ExpoLink } from '@green-stack/navigation/Link.expo' + +// ... Later ... + + + ... + +``` + +```tsx /.next/ /useNextLink/ filename="NextRootLayout.tsx" copy +import { Link as NextLink } from '@green-stack/navigation/Link.next' + +// ... Later ... + + + ... + +``` + +### Why this pattern? + +The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of components across different flavours of writing React. + +On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way. + +But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `Link` component. + +### Supporting more environments + +Just add your own `Link..tsx` file that respects the shared types, and then pass it to the `` component as `contextLink`. diff --git a/apps/docs/pages/@green-stack-core/navigation/_meta.ts b/apps/docs/pages/@green-stack-core/navigation/_meta.ts new file mode 100644 index 0000000..1733ccb --- /dev/null +++ b/apps/docs/pages/@green-stack-core/navigation/_meta.ts @@ -0,0 +1,6 @@ + +export default { + 'useRouter': 'useRouter', + 'useRouteParams': 'useRouteParams', + 'Link': 'Link', +} diff --git a/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx b/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx new file mode 100644 index 0000000..533b47a --- /dev/null +++ b/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx @@ -0,0 +1,88 @@ +import { Image } from '@app/primitives' +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## Usage - `useRouteParams()` + + + + +# useRouteParams() + +Tiny absctraction layer that retrieves the parameters of a route in both the Expo Router and Next.js app routers. Serverside, in the browser and on iOS or Android: + +```tsx copy +import { useRouteParams } from '@green-stack/navigation' +``` + +```tsx copy +const routeParams = useRouteParams(routeProps) +``` + +Typescript should complain if you don't, but make sure to include the route's screen props when using this hook, as it relies on them to access the route parameters in Next.js + +## React Portability Patterns + +Each environment has it's own ways of accessing route parameters. This is why there are also versions specifically for each of those environments: + + + + + + + + + + + + + + +Where `useRouteParams.next.tsx` covers the Next.js app router, and `useRouteParams.expo.tsx` covers Expo Router. The main `useRouteParams.tsx` retrieves whichever implementation was provided to the `` component and, which is further passed to ``: + +```tsx /.expo/ /useExpoRouteParams/ filename="ExpoRootLayout.tsx" copy +import { useRouteParams as useExpoRouteParams } from '@green-stack/navigation/useRouteParams.expo' + +// ... Later ... + + + ... + +``` + +```tsx /.next/ /useNextRouteParams/ filename="NextRootLayout.tsx" copy +import { useRouteParams as useNextRouteParams } from '@green-stack/navigation/useRouteParams.next' + +// ... Later ... + + + ... + +``` + +While the `useRouteParams.types.ts` file ensures both implementations are compatible with the same interface, allowing you to use the same `useRouteParams()` hook across both Expo and Next.js environments. + +### Why this pattern? + +The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of hooks across different flavours of writing React. + +On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way. + +But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `useRouteParams` hook. + +### Supporting more environments + +Just add your own `useRouteParams..ts` file that respects the shared types, and then pass it to the `` component as `useContextRouteParams`. + +
diff --git a/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx b/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx new file mode 100644 index 0000000..2c69e16 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx @@ -0,0 +1,104 @@ +import { Image } from '@app/primitives' +import { FileTree, Callout } from 'nextra/components' +import { TitleWrapper } from '@app/docs/components/Hidden' + + + ## Usage - `useRouter()` + + + + +# Universal `useRouter()` hook + +```tsx +import { useRouter } from '@green-stack/navigation/useRouter' +``` + +You can use `router.push()` to navigate to a new page: + +```tsx +const router = useRouter() + +router.push('/examples/[slug]', '/examples/123') +``` + +> `.push()` will use a push operation on mobile if possible. + +There are also other methods available on the `router` object: + +- `router.navigate()` - Navigate to the provided href +- `router.replace()` - Navigate without appending to the history +- `router.back()` - Go back in the history +- `router.canGoBack()` - Check if there's history that supports invoking the `back()` function +- `router.setParams()` - Update the current route query params without navigating + +
+ +## React Portability Patterns + +Each environment has it's own optimized router. This is why there are also versions specifically for each of those environments: + + + + + + + + + + + + + + +Where `useRouter.next.ts` covers the Next.js app router, and `useRouter.expo.ts` covers Expo Router. The main `useRouter.ts` retrieves whichever implementation was provided to the `` component, which is further passed to ``: + +```tsx /.expo/ /useExpoRouter/ filename="ExpoRootLayout.tsx" copy +import { useRouter as useExpoRouter } from '@green-stack/navigation/useRouter.expo' + +// ... Later ... + +const expoContextRouter = useExpoRouter() + + + ... + +``` + +```tsx /.next/ /useNextRouter/ filename="NextRootLayout.tsx" copy +import { useRouter as useNextRouter } from '@green-stack/navigation/useRouter.next' + +// ... Later ... + +const nextContextRouter = useNextRouter() + + + ... + +``` + +While the `useRouter.types.ts` file ensures both implementations are compatible with the same interface, allowing you to use the same `useRouter()` hook across both Expo and Next.js environments. + +### Why this pattern? + +The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of hooks across different flavours of writing React. + +On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way. + +But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `useRouter` hook. + +### Supporting more environments + +Just add your own `useRouter..ts` file that respects the shared types, and then pass it to the `` component as `contextRouter`. + +
diff --git a/apps/docs/pages/@green-stack-core/schemas.mdx b/apps/docs/pages/@green-stack-core/schemas.mdx new file mode 100644 index 0000000..308cfc6 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas.mdx @@ -0,0 +1,629 @@ +import { Image } from '@app/primitives' +import { FileTree, Callout } from 'nextra/components' +import { Hidden, TitleWrapper, LLMOptimized } from '@app/docs/components/Hidden' + + + ## Intro - `schema()` + + + + +# Schemas API reference + +```typescript copy +import { z, schema } from '@green-stack/schemas' +``` + + + + + + + + + + + +## Building Schemas + +Because `schema` is a tiny wrapper around Zod (V3), it's usage is similar to using `z.object()` + +```typescript {4, 8} /Requires a name/1 /'User'/ /to port to other formats later/ filename="User.ts" +export const User = schema('User', { + // Requires a name value (☝️) to port to other formats later, best to keep the same + + // Zod can help you go even narrower than typescript + name: z.string().min(2), // <- e.g. Needs to be a string with at least 2 letters + age: z.number().min(18), // <- e.g. Age must be a number of at least 18 + + // Just like TS, it can help you indicate fields as optional + isAdmin: z.boolean().default(false), // <- Marked optional, defaults to false + birthdate: z.Date().nullish(), // = same as calling .nullable().optional() +}) +``` + +> The main difference is that you have to specify a name for each schema as the first argument. This helps us port your schemas to other formats later: + +- [Auto-generated MDX UI component docs](/single-sources-of-truth#automatic-mdx-docgen) +- [Resolver Inputs, Outputs, Docs, and GraphQL definitions](/data-resolvers) +- [Data Fetching Functions and Hooks](/data-fetching) +- [Custom Implementations using the introspection metadata API](/single-sources-of-truth#transforming-to-other-formats) + +### Defining Primitives + +```typescript +const someString = z.string() // -> string +const someNumber = z.number() // -> number +const someBoolean = z.boolean() // -> boolean +const someDate = z.date() // -> Date +``` + +### Defaults and optionality + +You can mark fields as optional or provide default values by using either: +- `.optional()` - to allow `undefined` +- `.nullable()` - to allow `null` +- `.nullish()` - to allow both + +You can also use `.default()` to provide a default value when the field isn't passed in: + +```ts {8} /.optional()/ /.nullable()/ /.nullish()/ /.default/2 filename="User.ts" +// Define a schema with optional and nullable fields +const User = schema('...', { + + name: z.string().optional(), // <- Allow undefined + age: z.number().nullable(), // <- Allow null + birthData: z.date().nullish(), // <- Allow both + + // Use .default() to make optional in args, + // but provide a default value when it IS undefined + isAdmin: z.boolean().default(false), // <- false +}) +``` + +> When using `.default()`, you might need to be more specifc when inferring types. You can use `z.input()` or `z.output()` to get the correct type. Based on which you choose, defaulted fields will be either optional or required. + +### Enums with `inputOptions()` + +```typescript copy +import { inputOptions } from '@green-stack/schemas' +``` + +To define more flexible enums you can easily reuse in forms, we also export an `inputOptions()` function: + +```typescript +const MY_ENUM = inputOptions({ + // Define the enum values, alongside their display names + value1: 'Value 1', + value2: 'Value 2', + value3: 'Value 3', +}) + +const schemaWithEnum = schema('MySchema', { + myEnumField: MY_ENUM, // <- Use the enum in a schema +}) +``` + +This helps you create an enum definition extending Zod's `z.enum()` with some additions: + +You can use `.entries` object to get the original object with key-value pairs + +```typescript /.entries/ +MY_ENUM.entries +// { +// value1: 'Value 1', +// value2: 'Value 2', +// value3: 'Value 3', +// } +``` + +You can get auto-completion for the enum values / option keys: + +```typescript +MyEnum.value1 // => 'value1' +MyEnum.enum.value1 // => 'value1' (alternatively, the way Zod Enums work) +``` + +You can retrieve an array of the enum values with `.options`: + +```typescript +MY_ENUM.options // => ['value1', 'value2', 'value3'] +``` + +All of this on top of what's already available in [Zod Enums](https://v3.zod.dev/?id=zod-enums). + +### `.extendSchema()` - add fields + +It can happen that you need to differentiate between two similar data shapes, for example, needing to expand on an existing shape. + + + + + + + + +You can add new fields by calling `.extendSchema()` on the original schema: + +```ts {6} /.extendSchema/ filename="AdminUser.ts" +// Extend the User schema +const AdminUser = User.extendSchema('AdminUser', { + isAdmin: z.boolean().default(true), +}) + +type AdminUser = z.infer + +// { +// name: string, +// age: number, +// birthDate?: Date | null, +// +// isAdmin?: boolean, // <- New field added +// } +``` + +> You will **need to provide a new name** for the extended schema. This ensures there is no conflict with the original one when we port it to other formats. + +### `.pickSchema()` - select fields + +Similar to extending, **you can create a new schema by picking specific fields from another**: + +```ts {6} /.pickSchema/ filename="PublicUser.ts" +const PublicUser = User.pickSchema('PublicUser', { + name: true, + age: true, // <- Only these fields will be included +}) + +type PublicUser = z.infer + +// { +// name: string, +// age: number, +// } +``` + +### `.omitSchema()` - remove fields + +The reverse is also possible by removing certain fields from another. The new schema will have all fields from the original, *except the ones you specify*: + +```ts {5} /.omitSchema/ filename="PublicUser.ts" +const PublicUser = User.omitSchema('PublicUser', { + birthDate: true, // <- Will be missing in the new schema +}) + +type PublicUser = z.infer + +// { +// name: string, +// age: number, +// } +``` + +### Nesting and Collections + +You can nest schemas within each other. +This is useful when you need to represent a more complex data shape + + + + + + + + + + +For example, sometimes you need to represent a collection of specific data: + +```ts {6, 17} /z.array(User)/ /User[]/ filename="Team.ts" +const Team = schema('Team', { + members: z.array(User), // <- Pass the 'User' schema to z.array() + teamName: z.string(), +}) + +type Team = z.infer + +// { +// teamName: string, +// members: { +// name: string, +// age: number, +// birthDate?: Date | null, +// }[] +// } + +// ⬇⬇⬇ Which is the same as: + +// { +// teamName: string, +// members: User[] +// } +``` + +## Extracting Types + +The main thing to use schemas for is to hard-link validation with types. + +You can extract the type from the schema using `z.infer()`, `z.input()` or `z.output()`: + +```tsx filename="User.ts" +// Extract type from the schema and export it as a type alias +export type User = z.infer + +// If you have defaults, you can use z.input() or z.output() instead +export type UserOutput = z.output +export type UserInput = z.input +``` + +`⬇⬇⬇` + +```tsx /?/ +// { +// name: string, +// age: number, +// isAdmin?: boolean, +// birthDate?: Date | null, +// } +``` + +> In this case where we check the resulting type of `z.input()`, the 'isAdmin' field will be marked as optional, as it's supposedly not defaulted to `false` yet. If we'd inspect `z.output()`, it would be marked as required since it's either provided or presumed defaulted. + +### Advanced Types + +*Anything you can define the shape of in Typescript, you can define in Zod:* + +```ts {3, 6, 9} +const Task = schema('Task', { + + // Enums + status: z.enum(['draft', 'published', 'archived']), + + // Arrays + tags: z.array(z.string()), + + // Tuples + someTuple: z.tuple([z.string(), z.number()]), + +}) +``` + +When extracting the type with `type Task = z.infer`: + +```ts +// { +// status: 'draft' | 'published' | 'archived', +// tags: string[], +// someTuple: [string, number], +// } +``` + +> Check [zod.dev](https://zod.dev) for the full list of what you can define with zod. + +## Validating inputs + +You can use the `.parse()` method to validate inputs against the schema: + +```tsx {1, 4} /.shape.age/ +// Call .parse() on the whole User schema... +const newUser = User.parse(someInput) // <- Auto infers 'User' type if valid + +// ...or validate idividual fields by using '.shape' 👇 +User.shape.age.parse("Invalid - Not a number") +// Throws => ZodError: "Expected a number, recieved a string." +// Luckily, TS will already catch this in your editor ( instant feedback 🙌 ) +``` + +If a field's value does not match the schema, it will throw a `ZodError`: + +```ts {8, 14} /.issues/ +try { + + // 🚧 Will fail validation + const someNumber = z.number().parse("Not a number") + +} catch (error) { // ⬇⬇⬇ + + /* Throws 'ZodError' with a .issues array: + [{ + code: 'invalid_type', + expected: 'number', + received: 'string', + path: [], + message: 'Expected number, received string', + }] + */ + +} +``` + +### Custom Errors + +You can provide custom error messages by passing the `message` prop: + +```ts +const NumberValue = z.number({ message: 'Please provide a number' }) +// Throws => ZodError: [{ message: "Please provide a number", ... }) +``` + +You can provide **custom error messages** for specific validations: + +```ts +const MinimumValue = z.number().min(10, { message: 'Value must be at least 10' }) +// Throws => ZodError: [{ message: "Value must be at least 10", ... }) + +const MaximumValue = z.number().max(100, { message: 'Value must be at most 100' }) +// Throws => ZodError: [{ message: "Value must be at most 100", ... }) +``` + +## Security Extensions + +We added some additional features to Zod to help you with security and data handling. + +### Mark fields as `.sensitive()` + +```typescript + password: z.string().sensitive() +``` + +In your schemas, you can mark fields as sensitive using `.sensitive()`. This will: + +- Exclude the field from appearing in the GraphQL schema, introspection or queries +- Mark the field as strippable in API resolvers / responses (*) +- Mark the field with `isSensitive: true` in schema introspection + +> * 'Strippable' means when using either `withDefaults()` OR `applyDefaults()` / `formatOutput()` with the `{ stripSensitive: true }` option as second argument. If none of these are used, the field will still be present in API route handler responses, but not GraphQL. + +## Metadata APIs + +The reason we're able to use schemas as a [single source of truth](/single-sources-of-truth) to build the right abstractions around, is due to its strong metadata API: + +### `.introspect()` + +```typescript +const metadata = User.introspect() +``` + +This will return a metadata object with the following type: + +```typescript +type Metadata = { + + // Essentials + name?: string, // <- The name you passed to schema(), e.g. 'User' + zodType: ZOD_TYPE, // e.g. 'ZodString' | 'ZodNumber' | 'ZodBoolean' | 'ZodDate' | ... + baseType: BASE_TYPE, // e.g. 'String' | 'Number' | 'Boolean' | 'Date' | ... + + // Optionality and defaults + isOptional?: boolean, + isNullable?: boolean, + defaultValue?: T, // The resulting Zod type + + // Documentation + exampleValue?: T, // The resulting Zod type + description?: string, + minLength?: number, + maxLength?: number, + exactLength?: number, + minValue?: number, + maxValue?: number, + + // Flags + isInt?: boolean, + isBase64?: boolean, + isEmail?: boolean, + isURL?: boolean, + isUUID?: boolean, + isDate?: boolean, + isDatetime?: boolean, + isTime?: boolean, + isIP?: boolean, + + // Literals, e.g. z.literal() + literalValue?: T, // The resulting Zod type + literalType?: 'string' | 'boolean' | 'number', + literalBase?: BASE_TYPE, + + // e.g. Nested schema field(s) to represent: + // - object properties (like meta for 'age' / 'isAdmin' / ...) + // - array elements + // - tuple elements + schema?: S, + + // The actual Zod object, only included with .introspect(true) + zodStruct?: z.ZodType & { ... }, // <- Outer zod schema (e.g. ZodDefault) + innerStruct?: z.ZodType & { ... }, // <- Inner zod schema (not wrapped) + + // Mark as serverside only, strippable in API responses + isSensitive?: boolean, + + // Compatibility with other systems like databases & drivers + isID?: boolean, + isIndex?: boolean, + isUnique?: boolean, + isSparse?: boolean, +} +``` + +When inspecting an object, like the `User` schema we've referenced in this doc, the metadata for all the fields defined on the schema will be defined as `Record` on the `schema` property. + +If you need to access the zod struct that was used to create the schema or field definition, you can pass `true` as the first argument to `.introspect()`: + +```typescript +const metadataWithZodStructs = User.introspect(true) +``` + +### `.documentationProps()` + +You can use schemas to generate interactive UI docs for your components by describing their props with it. + +You can define specific example props for the component's docs by chaining `.example()` on prop definitions, alternatively, `.default()` will be used. + +All that's needed afterwards, is to call `.documentationProps()` on the schema from within the component file: + +```typescript {2, 12} /.example/ /.documentationProps/ filename="Button.tsx" +export const ComponentProps = schema('ComponentProps', { + // Define the props shape with zod, e.g.: + someProp; z.string().example('Some example value'), +}) + +/* --- --------------- */ + +export const ComponentName = (rawProps: ComponentProps) => <>... + +/* --- Documentation ------------------ */ + +export const documentationProps = ComponentProps.documentationProps('ComponentName') +``` + +Doing this will indicate to the `npm run regenerate:docs` command that this component should have docs generated for it. + +For an example of what generated docs look like, check out the [Button component docs](/@app-core/components/Button). + +To further customize the docs, you can pass options that follow this type as the second argument: + +```typescript +type DocumentationPropsOptions = { + /** -i- Pass to display specific props as starting examples for preview + props table */ + previewProps: Record, + exampleProps?: Partial, + /** -i- If a form component, you can use this prefill the current value from the url */ + valueProp?: keyof T | HintedKeys, + /** -i- If a form component, you can use this save the current value to the url */ + onChangeProp?: keyof T | HintedKeys, +} +``` + +For example, for a `` component, you can use: + +```typescript +export const getDocumentationProps = SwitchProps.documentationProps('Switch', { + exampleProps: { checked: true }, // <- Start in the 'checked' state + valueProp: 'checked', + onChangeProp: 'onCheckedChange', +}) +``` + +> For an example of what this looks like, try visiting: +> [/@app-core/forms/Switch?checked=true&label=Hello+from+the+URL](/@app-core/forms/Switch?checked=true&label=Hello+from+the+URL). + +## Automations + +### Schema generator + +Like all elements of our recommended way of working, there is a turborepo generator to help create a schema in a specific workspace: + +```shell copy +npm run add:schema +``` + +`⬇⬇⬇` + +will prompt you for a target workspace and name: + +```shell +>>> Modify "your-project" using custom generators + +? Where would you like to add this schema? +❯ @app/core # -- from features/app-core + some-feature # -- from features/some-feature + some-package # -- from packages/some-package +``` + +`⬇⬇⬇` + +```shell +>>> Modify "your-project" using custom generators + +? Where would you like to add this schema? # @app/core +? What is the schema name? # SomeData +? Optional description? # ... + +>>> Changes made: + • /features/@app-core/schemas/SomeData.ts # (add) + • Opened 1 file in VSCode # (open-in-vscode) + +>>> Success! +``` + +`⬇⬇⬇` + +```shell +@app-core + + └── /schemas/... + └── SomeData.schema.ts +``` + + + +Though if you chose to also generate an integration, it might look like this instead: + +```shell +@app-core + + └── /schemas/... + └── SomeData.schema.ts + + └── /hooks/... + └── useSomeData.ts # <- Form state hook using `useFormState()` + + └── /models/... + └── SomeData.ts # <- `@db/driver` model using `createSchemaModel()` +``` + + + +## Disclaimers + +### About `z.union` and `z.tuple` + +Since GraphQL and other systems might not natively support unions or tuples, it could be best to avoid them. We allow you to define them, but tranformations of these to other formats is considered experimental and potentially not entirely type-safe. + +You can always use `z.array` and `z.object` to represent the same data shapes. + +e.g. instead of: + +```ts +const someUnionField = z.union([z.string(), z.number()]) +// TS: string | number + +const someTupleField = z.tuple([z.string(), z.number()]) +// TS: [string, number] +``` + +you might try this instead: + +```ts +const SomeSchema = schema('SomeSchema', { + + // Unions + someUnionFieldStr: z.string(), // TS: string + someUnionFieldNum: z.number(), // TS: number + + // Tuples + someTupleField: schema('SomeTupleField', { + stringValue: z.string(), // TS: string + numberValue: z.number(), // TS: number + }) +}) +``` + +... which will work in GraphQL and other formats as well. + +We've done our best to hack in experimental tuple and union support where possible. You can take it as-is, edit it further to your liking or avoid tuples and unions entirely. *The choice is yours.* + +## Further reading + +From our own docs: +- [Single Sources of Truth](/single-sources-of-truth) - Core Starterkit Concept +- [Data Bridges for fetching](/data-resolvers#start-from-a-databridge) - Starterkit Docs + +Relevant external resources: +- [Zod's official docs](https://zod.dev) - zod.dev +- [The Joy of Single Sources of Truth](https://dev.to/codinsonn/the-joy-of-single-sources-of-truth-277o) - Blogpost by [Thorr ⚡️ @codinsonn.dev](https://codinsonn.dev) diff --git a/apps/docs/pages/@green-stack-core/schemas/_meta.ts b/apps/docs/pages/@green-stack-core/schemas/_meta.ts new file mode 100644 index 0000000..3bed75a --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/_meta.ts @@ -0,0 +1,8 @@ + +export default { + 'createResolver': 'createResolver', + 'createNextRouteHandler': 'createNextRouteHandler', + 'createGraphResolver': 'createGraphResolver', + 'createDataBridge': 'createDataBridge', + 'bridgedFetcher': 'bridgedFetcher', +} diff --git a/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx b/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx new file mode 100644 index 0000000..4d46715 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx @@ -0,0 +1,85 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## `bridgedFetcher()` + + + + +# `bridgedFetcher()` + +```typescript copy +import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher' +``` + + + + + + + + + + + +A util to turn a [Data Bridge](/@green-stack-core/schemas/createDataBridge) into a universal fetcher function that can be used in your front-end code with, for example, `react-query`. + + + ### Creating a bridged fetcher + + +```ts {3} /bridgedFetcher/3 filename="updatePost.query.ts" +import { updatePostBridge } from './updatePost.bridge' +// ☝️ Reuse your data bridge +import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher' +// ☝️ Universal graphql fetcher that can be used in any JS environment + +/* --- updatePostFetcher() --------- */ + +export const updatePostFetcher = bridgedFetcher(updatePostBridge) +``` + +
+ +### Fetching with `react-query` + +```typescript copy +import { useQuery } from '@tanstack/react-query' +``` + +For simply fetching data with `react-query`, you can use the `useQuery` hook with the `bridgedFetcher`: + +```typescript copy +useQuery({ + queryKey: ['updatePost', postId], + queryFn: someBridgedFetcher, + // provide the fetcher (👆) as the queryFn + ...options, +}) +``` + +Be sure to check the [`useQuery()`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options you might want to prefill or abstract. + +You can also do [mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) with `useMutation()` for updating data in any way. + +
+ +## Related Automations + +### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator + +You don't need to manually create a new fetcher with `bridgedFetcher()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its fetching function in one go: + +```bash copy +npx turbo gen resolver +``` + +
diff --git a/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx b/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx new file mode 100644 index 0000000..65676eb --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx @@ -0,0 +1,347 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## `createDataBridge()` + + + + +# `createDataBridge()` + +```typescript copy +import { createDataBridge } from '@green-stack/schemas/createDataBridge' +``` + + + + + + + + + + + +
+ +To create a `DataBridge`, simply provide call `createDataBridge()` with with the input and output schemas and some additional metadata: + +```typescript copy +const healthCheckBridge = createDataBridge({ + + // Input and Output + inputSchema: HealthCheckInput, + outputSchema: HealthCheckOutput, + + // Basic Metadata + resolverName: 'healthCheck', + resolverType: 'query', // 'query' | 'mutation' + + // REST Metadata + apiPath: '/api/health', + allowedMethods: ['GET'], // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'GRAPHQL' + + // Optional Metadata + resolverArgsName?: 'healthCheckArgs', // Custom Args name for GraphQL schema and queries + +}) +``` + +
+ +## Why "Data Bridges"? + +[Schemas](/@green-stack-core/schemas) serve as the [single source of truth](/single-sources-of-truth) for your data shape. But what about the shape of your APIs? + +By combining input and output schemas into a `bridge` file, and adding some API metadata, bridges serve as the source of truth for your API resolver: + +### Reusable Client-Server Contract + +Think of a **"Databridge"** as a literal *bridge between the front and back-end*. + +It's a metadata object you can use from either side to provide / transform / extract: + +- ✅ **Route handler args** from request params / body +- ✅ Input and output **types + validation + defaults** +- ✅ GraphQL **schema definitions** for `schema.graphql` +- ✅ The query string to call our GraphQL API with + +### Recommended file conventions + + + + + + + + + + + + +*There's two reasons we suggest you define this "DataBridge" in a separate file*: + +- 1. **Reusability**: If kept separate from business logic, you can reuse it in both front and back-end code. +- 2. **Consistency**: Predicatable patterns make it easier to build automations and generators around them. + +> For this reason, we suggest you add `.bridge.ts` to your bridge filenames. + +
+ +## Using a `DataBridge` + +You can use the resulting `DataBridge` in various ways, depending on your needs: + +### Flexible Resolvers + +To use the data bridge we just created to bundle together the input and output types with our business logic, create a new resolver file and passing the bridge as the final arg to [`createResolver()`](/@green-stack-core/schemas/createResolver) at the end. + +The first argument is your resolver function will contain a function with your business logic: + +```ts {7, 17, 22} /updatePostBridge/ /createResolver/1,3 filename="updatePost.resolver.ts" +import { createResolver } from '@green-stack/schemas/createResolver' +import { updatePostBridge } from './updatePost.bridge' +import { Posts } from '@db/models' + +/** --- updatePost() ---- */ +/** -i- Update a specific post. */ +export const updatePost = createResolver(async ({ + args, // <- Auto inferred types (from 'inputSchema') + context, // <- Request context (from middleware) + parseArgs, // <- Input validator (from 'inputSchema') + withDefaults, // <- Response helper (from 'outputSchema') +}) => { + + // Validate input and apply defaults, infers input types as well + const { slug, ...postUpdates } = parseArgs(args) + + // -- Context / Auth Guards / Security -- + + // e.g. use the request 'context' to log out current user + const { user } = context // example, requires auth middleware + + // -- Business Logic -- + + // e.g. update the post in the database + const updatedPost = await Posts.updateOne({ slug }, postUpdates) + + // -- Respond -- + + // Typecheck response and apply defaults from bridge's outputSchema + return withDefaults({ + slug, + title, + content, + }) + +}, updatePostBridge) +``` + +> You pass the bridge (☝️) as the second argument to `createResolver()` to: +- 1️⃣ infer the input / arg types from the bridge's `inputSchema` +- 2️⃣ enable `parseArgs()` and `withDefaults()` helpers for validation, hints + defaults + +> **The resulting function can be used as just another async function** anywhere in your back-end. + +> The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs: + +
+ +### API Route Handlers + + + + + + + + + + + + + + + + + + +You can create a new API route by exporting a `GET` / `POST` / `UPDATE` / `DELETE` handler assigned to a `createNextRouteHandler()` that wraps your "bridged resolver": + +```ts /createNextRouteHandler/1,3 /updatePost/4 filename="@some-feature / routes / api / posts / [slug] / update / route.ts" +import { updatePost } from '@app/resolvers/updatePost.resolver' +import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler' + +/* --- Routes ------------ */ + +export const UPDATE = createNextRouteHandler(updatePost) +// Automatically extracts (☝️) args from url / search params +// based on the zod 'inputSchema' + +// If you want to support e.g. POST (👇), same deal (checks request body too) +export const POST = createNextRouteHandler(updatePost) +``` + +What `createNextRouteHandler()` does under the hood: +- 1. extract the input from the request context +- 2. validate it +- 3. call the resolver function with the args (and e.g. token / session / request context) +- 4. return the output from your resolver with defaults applied + +> Check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to understand supported exports (like `GET` or `POST`) and their options. + +> Restart your dev server or run `npm run link:routes` to make sure your new API route is available. + +
+ +### GraphQL Resolvers + +We made it quite easy to enable GraphQL for your resolvers. The flow is quite similar. + +*In the same file*, add the following: + +```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts" +import { updatePost } from '@app/resolvers/updatePost.resolver' +import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler' +import { createGraphResolver } from '@green-stack/schemas/createGraphResolver' + +/* --- Routes ------------ */ + +// exports of `GET` / `POST` / `PUT` / ... + +/* --- GraphQL ----------- */ + +export const graphResolver = createGraphResolver(updatePost) +// Automatically extracts input (☝️) from graphql request context +``` + +After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually. + +This will: +- 1. pick up the `graphResolver` export +- 2. put it in our list of graphql compatible resolvers at `resolvers.generated.ts` in `@app/registries` +- 3. recreate `schema.graphql` from input & output schemas from registered resolvers + +You can now check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql) + +![Apollo Server GraphQL Playground Preview](https://github.com/FullProduct-dev/universal-app-router/assets/5967956/3b6b1d98-1228-44a4-aaa0-968664a027c3) + +
+ +### Universal Data Fetching + +The easiest way to create a fetcher is to use the `bridgedFetcher()` helper: + +```ts {3} /bridgedFetcher/3 filename="updatePost.query.ts" +import { updatePostBridge } from './updatePost.bridge' +// ☝️ Reuse your data bridge +import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher' +// ☝️ Universal graphql fetcher that can be used in any JS environment + +/* --- updatePostFetcher() --------- */ + +export const updatePostFetcher = bridgedFetcher(updatePostBridge) +``` + +This will automatically build the query string with all relevant fields from the bridge. + +To write a custom query with only certain fields, you can use our `graphql()` helper *with* `bridgedFetcher()`: + +```ts {3, 11, 21, 31, 41} filename="updatePost.query.ts" +import { ResultOf, VariablesOf } from 'gql.tada' +// ☝️ Type helpers that interface with the GraphQL schema +import { graphql } from '../graphql/graphql' +// ☝️ Custom gql.tada query builder that integrates with our types +import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher' +// ☝️ Universal graphql fetcher that can be used in any JS environment + +/* --- Query ----------------------- */ + +// VSCode and gql.tada will help suggest or autocomplete thanks to our schema definitions +export const updatePostQuery = graphql(` + query updatePost ($updatePostArgs: UpdatePostInput) { + updatePost(args: $updatePostArgs) { + slug + title + body + } + } +`) + +// ⬇⬇⬇ automatically typed as ⬇⬇⬇ + +// TadaDocumentNode<{ +// updatePost(args: Partial): { +// slug: string | null; +// title: boolean | null; +// body: boolean | null; +// }; +// }> + +// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇ + +/* --- Types ----------------------- */ + +export type UpdatePostQueryInput = VariablesOf + +export type UpdatePostQueryOutput = ResultOf + +/* --- updatePostFetcher() --------- */ + +export const updatePostFetcher = bridgedFetcher({ + ...updatePostBridge, // <- Reuse your data bridge ... + graphqlQuery: updatePostQuery, // <- ... BUT, use our custom query +}) +``` + +> Whether you use a custom query or not, you now have a fetcher that: + +- ✅ Uses the executable graphql schema serverside +- ✅ Can be used in the browser or mobile using fetch + +Want to know even more? Check the [Universal Data Fetching Docs](/universal-data-fetching). + +
+ +### Resolver Form State + + + + + + + + + + + + + + + + +To further keep your API's input and form state in sync, you can link the input schema to a form state hook using `useFormState()`: + +```tsx copy +import { useFormState } from '@green-stack/forms/useFormState' +``` + +```tsx {10} filename="useUpdatePostFormState.ts" +// Create a set of form state utils to use in your components +const formState = useFormState(updatePostBridge.inputSchema, { + initialValues: { ... }, + // ... other options +}) +``` + +More about this in the [Form Management Docs](/form-management). + +
diff --git a/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx b/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx new file mode 100644 index 0000000..60a0885 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx @@ -0,0 +1,118 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## `createGraphResolver()` + + + + +# `createGraphResolver()` + +```typescript copy +import { createGraphResolver } from '@green-stack/schemas/createGraphResolver' +``` + + + + + + + + + + + +We made it quite easy to enable GraphQL for your resolvers. The flow is quite similar to [adding API routes](/@green-stack-core/schemas/createNextRouteHandler). + +*From a related route file*, add the following: + + + ### Registering a GraphQL resolver + + +```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts" +import { updatePost } from '@app/resolvers/updatePost.resolver' +import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler' +import { createGraphResolver } from '@green-stack/schemas/createGraphResolver' + +/* --- Routes ------------ */ + +// exports of `GET` / `POST` / `PUT` / ... + +/* --- GraphQL ----------- */ + +export const graphResolver = createGraphResolver(updatePost) +// Automatically extracts input (☝️) from graphql request context +``` + +After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually. + +This will: +- 1. pick up the `graphResolver` export +- 2. put it in our list of graphql compatible resolvers at `resolvers.generated.ts` in `@app/registries` +- 3. recreate `schema.graphql` from input & output schemas from registered resolvers + +You can now check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql) + +![Apollo Server GraphQL Playground Preview](https://github.com/FullProduct-dev/universal-app-router/assets/5967956/3b6b1d98-1228-44a4-aaa0-968664a027c3) + +
+ +## Related Automations + +### `collect-resolvers` - script + +```bash copy +npm run collect:resolvers +``` + +This script will: + +- 1. Scan the `/routes/` folder in any of your non-app workspaces +- 2. Find all `route.ts` files that export a `createGraphResolver()` function +- 3. Re-export them in the `@app/registries/resolvers.generated.ts` file + +The goal of this script is so you can define which resolvers should be registered for GraphQL on a feature by feature basis, instead of manually linking them somewhere centrally. + +Overall, this will keep your features more modular and portable between GREEN stack / FullProduct.dev projects. + +The [`collect-routes`](/@green-stack-core/scripts/collect-routes) script runs automatically on `npm run dev` and any production build commands. + +
+ +### `build-schema` - script + +```bash copy +npm run build:schema +``` + +This script will: + +- 1. Import all the resolvers from your `@app/registries/resolvers.generated.ts` file +- 2. Read the input and output schemas from each resolver (the [Data Bridge](/@green-stack-core/schemas/createDataBridge) for the resolver logic) +- 3. Generate a new `schema.graphql` file in `@app/core/graphql/` folder +- 4. Bind the schema to the resolvers to create an executable schema and effortless GraphQL API + +Meaning you won't need to manually write your own GraphQL schema, it will be generated automatically based on your resolvers. + +The [`build-schema`](/@green-stack-core/scripts/build-schema) script runs automatically on `npm run dev` and any production build commands. + +
+ +### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator + +You don't need to manually create a new API route with `createGraphResolver()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its API route at once: + +```bash copy +npx turbo gen resolver +``` + +
diff --git a/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx b/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx new file mode 100644 index 0000000..0123977 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx @@ -0,0 +1,97 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## `createNextRouteHandler()` + + + + +# `createNextRouteHandler()` + +```typescript copy +import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler' +``` + + + + + + + + + + + +You can create a new API route by exporting a `GET` / `POST` / `UPDATE` / `DELETE` handler assigned to a `createNextRouteHandler()` that wraps your "bridged resolver": + + + ### Creating API routes + + +```ts /createNextRouteHandler/1,3 /updatePost/4 filename="@some-feature / routes / api / posts / [slug] / update / route.ts" +import { updatePost } from '@app/resolvers/updatePost.resolver' +import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler' + +/* --- Routes ------------ */ + +export const UPDATE = createNextRouteHandler(updatePost) +// Automatically extracts (☝️) args from url / search params +// based on the zod 'inputSchema' + +// If you want to support e.g. POST (👇), same deal (checks request body too) +export const POST = createNextRouteHandler(updatePost) +``` + +### How it works + +What `createNextRouteHandler()` does under the hood: +- 1. extract the input from the request context +- 2. validate it +- 3. call the resolver function with the args (and e.g. token / session / request context) +- 4. return the output from your resolver with defaults applied + +> Check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to understand supported exports (like `GET` or `POST`) and their options. + +> Restart your dev server or run `npm run link:routes` to make sure your new API route is available. + +
+ +## Related Automations + +### `link-routes` - script + +```bash copy +npm run link:routes +``` + +This script will: + +- 1. Scan the `/routes/` folder in any of your non-app workspaces +- 2. Find all `route.ts` files that export a `createNextRouteHandler()` function +- 3. Re-export your `GET` / `POST` / `PUT` / `DELETE` route handlers to the `@app/next` workspace + +The goal of this script is so you can define your API routes colocated by feature, instead of grouping by the "API route" type. + +Overall, this will keep your features more modular and portable between GREEN stack / FullProduct.dev projects. + +The [`link-routes`](/@green-stack-core/scripts/link-routes) script runs automatically on `npm run dev` and any production build commands. + +
+ +### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator + +You don't need to manually create a new API route with `createNextRouteHandler()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its API route at once: + +```bash copy +npx turbo gen resolver +``` + +
diff --git a/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx b/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx new file mode 100644 index 0000000..4d01f93 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx @@ -0,0 +1,101 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## `createResolver()` + + + + +# `createResolver()` + +```typescript copy +import { createResolver } from '@green-stack/schemas/createResolver' +``` + + + + + + + + + + + +The `createResolver()` function is a utility that simplifies the creation of data resolvers from [Data Bridges](/@green-stack-core/schemas/createDataBridge): + + + ### Creating a resolver + + +```typescript copy +/* --- someResolver() --- */ +/* -i- ... */ +export const someResolver = createResolver(async ({ + args, // <- Auto inferred types (from bridge 'inputSchema') + context, // <- Request context (from next.js middleware) + parseArgs, // <- Input validator (from bridge 'inputSchema') + withDefaults, // <- Response helper (from bridge 'outputSchema') +}) => { + + // Validate input and apply defaults + const validatedArgs = parseArgs(args) + + // -- Business Logic -- + + // ... Your back-end business logic goes here ... + + // -- Respond -- + + // Typecheck response and apply defaults from bridge's outputSchema + return withDefaults({ + ..., + }) + +}, someDataBridge) +``` + +> Pass the bridge (☝️) as the second argument to `createResolver()` to: +- 1️⃣ infer the input / arg types from the bridge's `inputSchema` +- 2️⃣ enable `parseArgs()` and `withDefaults()` helpers for validation, hints + defaults + +> **The resulting function can be used as just another async function** anywhere in your back-end. + +> The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs + +
+ +## Why Resolvers? + +Our definition of a resolver is a function that: + +- **Takes arguments** (input data) +- **Returns a value** (output data) +- **Can be used as an API** (e.g. in a Next.js route handler or GraphQL API) +- **Has its metadata like the input and output schema bundled wiht it** ([Data Bridges](/@green-stack-core/schemas/createDataBridge)) + +That last part is important, because when you bundle the business logic with it's input and output metadata, you can easilyt transform it into APIs, while still keeping it as reusable function returning a promise. + +More about this in the [Data Bridges](/@green-stack-core/schemas/createDataBridge) docs. + +
+ +## Related Automations + +### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator + +You don't need to manually create a new resolver with `createResolver()`. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and all related files (like the [DataBridge](/@green-stack-core/schemas/createDataBridge) and [API route](/@green-stack-core/schemas/createNextRouteHandler)) in one go: + +```bash copy +npx turbo gen resolver +``` + +
diff --git a/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts b/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts new file mode 100644 index 0000000..4b08fb5 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts @@ -0,0 +1,4 @@ + +export default { + 'scriptUtils': 'scriptUtils', +} diff --git a/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx b/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx new file mode 100644 index 0000000..c6af135 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx @@ -0,0 +1,471 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Script Utils + + + + +# Script Utils + +```typescript copy +import * as scriptUtils from '@green-stack/scripts/helpers/scriptUtils' +``` + + + + + + + + + + + + + +A collection of utility functions for working with scripts, file paths, and workspace management in the Green Stack ecosystem. + +## File System Utilities + +### `excludeDirs()` + +Filter function to exclude folders and directories. Returns true if the path contains a file (has an extension). + +```typescript +excludeDirs('path/to/file.ts') // => true +excludeDirs('path/to/folder') // => false + +// Usage as a filter function: + +const paths = [ + '/components/Button.tsx', + '/components/Input.tsx', + '/components/', + '/schemas' +] + +const filteredPaths = paths.filter(excludeDirs) +// => ['/components/Button.tsx', '/components/Input.tsx'] +``` + +### `excludeModules()` + +Filter function to exclude node_modules folders. Returns true if the path does not include 'node_modules'. + +```typescript +excludeModules('src/components/Button.tsx') // => true +excludeModules('node_modules/react/index.js') // => false + +// Usage as a filter function: + +const paths = [ + 'src/components/Button.tsx', + 'src/components/Input.tsx', + 'node_modules/react/index.js', + 'node_modules/@app-core/utils.js' +] + +const filteredPaths = paths.filter(excludeModules) +// => ['src/components/Button.tsx', 'src/components/Input.tsx'] +``` + +### `globRel()` + +Gets the relative glob path of a glob-style selector using [`globSync`](https://github.com/isaacs/node-glob?tab=readme-ov-file#globsyncpattern-string--string-options-globoptions--string--path). The path must contain one of the workspace folder locations ('features', 'packages', or 'apps'). + +```typescript +globRel('packages/**/*.ts') +// => ['packages/@green-stack-core/utils.ts', 'packages/@app-core/components.ts'] + +globRel('features/**/components/*.tsx') +// => ['features/@app-core/components/Button.tsx', 'features/@app-core/components/Input.tsx'] +``` + +### `hasOptOutPatterns()` + +Checks if file content has opt-out patterns to determine if the file should be ignored. + +```typescript +const content = ` +export const optOut = true +// Rest of the file... +` +hasOptOutPatterns(content) // => true + +const content2 = ` +// @automation optOut = true +// Rest of the file... +` +hasOptOutPatterns(content2) // => true +``` + +
+ +## String Utilities + +### `normalizeName()` + +Makes sure only lowercase and uppercase letters are left in a given string by removing all other characters. + +```typescript +normalizeName('user-profile') // => 'userprofile' +normalizeName('User_Profile_123') // => 'UserProfile' +``` + +### `matchMethods()` + +Checks that a certain method (like 'GET', 'POST', etc.) is included in a list of method names. + +```typescript +const methods = ['GET', 'POST', 'PUT'] +matchMethods(methods, 'GET') // => true +matchMethods(methods, 'DELETE') // => false +``` + +### `createDivider()` + +Creates a code divider that's always 100 chars wide. Can be used for documentation or code comments. + +```typescript +createDivider('Section Title') +// => '/* --- Section Title ----------------------------------------------------------------------------------- */' + +createDivider('Section Title', true) +// => '/** --- Section Title ----------------------------------------------------------------------------------- */' +``` + +### `validateNonEmptyNoSpaces()` + +Validates that a string is not empty and does not contain spaces. + +```typescript +validateNonEmptyNoSpaces('validName') // => true +validateNonEmptyNoSpaces('') // => 'Please enter a non-empty value' +validateNonEmptyNoSpaces('invalid name') // => 'Please enter a value without spaces' +``` + +> Handy for plop prompt input validation. + +
+ +## Workspace Management + +### `parseWorkspaces()` + +Figure out all info about all workspaces in the monorepo and return mapped linking info for use in scripts and generators. + +```typescript +const workspaceInfo = parseWorkspaces( + folderLevel = '../../', + includeApps = false, +) +// => { +// +// /** -i- Map of { [path]: package.json config, ... } */ +// workspaceConfigs: { ... }, +// +// /** -i- Map of { [path]: pkgName, ... } */ +// workspaceImports: { ... }, +// +// /** -i- Map of { [pkgName]: path, ... } */ +// workspacePathsMap: { ... }, +// +// /** -i- Array of all workspace packages, e.g. ['features/@app-core', 'packages/@green-stack-core', ...] */ +// workspacePaths: [ ... ], +// +// /** -i- Array of all workspace packages, e.g. ['@app/core', '@green-stack/core', ...] */ +// workspacePackages: [ ... ] +// +// // -- Aliases and Constants -- +// +// /** -i- Map of { [path]: package.json config, ... } */ +// PATH_CONFIGS: { ... }, +// +// /** -i- Map of { [pkgName]: package.json config, ... } */ +// PKG_CONFIGS: { ... }, +// +// /** -i- Map of { [path]: pkgName, ... } */ +// PATH_PKGS: { ... }, +// +// /** -i- Map of { [pkgName]: path, ... } */ +// PKG_PATHS: { ... }, +// +// } +``` + +### `getWorkspaceOptions()` + +List all the available workspaces for generators to use. + +```typescript +const options = getWorkspaceOptions(folderLevel = '../../', { + filter: ['@app'], // string[] + exclude = [], // string[] -i- (List of packages to exclude) + excludePackages = false, // boolean -i- (Exclude workspaces from /packages/?) + includeApps = false, // boolean -i- (Include workspaces from /apps/?) + includeGreenStack = false, // boolean -i- (Include /packages/@green-stack-core/?) + skipFormatting = false, // boolean -i- (Skip formatting the output) + prefer = [], // string[] -i- (Puts these options at the top of the list) +}) +// => { +// '@app/core -- from features/@app-core': 'features/@app-core', +// '@app/ui -- from features/@app-ui': 'features/@app-ui' +// } +``` + +### `getAvailableSchemas()` + +List all the available schemas in the codebase that generators can use. + +```typescript +const schemas = getAvailableSchemas(folderLevel = '../../', { + schemaKey = 'schemaName', // 'schemaName' | 'schemaPath' | 'schemaOption' + includeOptOut = false, // boolean +}) +// => { +// 'UserSchema': { +// schemaPath: 'features/@app-core/schemas/User.ts', +// schemaName: 'UserSchema', +// schemaFileName: 'User.ts', +// schemaOption: '@app/core - User (♻ - Schema)', +// workspacePath: 'features/@app-core', +// workspaceName: '@app/core', +// isNamedExport: true, +// isDefaultExport: false, +// schema?: ZodSchema | undefined +// }, +// ... (more schemas) +// } +``` + +The default arguments for `folderLevel` and `schemaKey` are `'../../'` and `'schemaName'`, respectively. You can adjust these based on your project structure. If you want the resulting lookup object to be keyed by either the path or option, you can pass `schemaPath` or `schemaOption` as the `schemaKey`. + +### `getAvailableDataBridges()` + +List all the available data bridges for generators to use. + +```typescript +const bridges = getAvailableDataBridges('../../', { + filterType?: 'query', // 'query' | 'mutation' + bridgeKey?: 'bridgeName', // 'bridgeName' | 'bridgePath' | 'bridgeOption' | 'bridgeInputOption' + allowNonGraphql?: false, // boolean + includeOptOut?: false, // boolean +}) +// => { +// 'UserBridge': { +// bridgePath: 'features/@app-core/resolvers/healthCheck.bridge.ts', +// bridgeName: 'healthCheckBridge', +// bridgeOption: '@app/core >>> healthCheck() (♻ - Resolver)', +// bridgeInputOption: '@app/core >>> healthCheck() (♻ - Input)', +// workspacePath: 'features/@app-core', +// workspaceName: '@app/core', +// resolverName: 'healthCheck', +// resolverType: 'query' | 'mutation', +// operationType: 'search' | 'add' | 'update' | 'delete' | 'get' | 'list', +// fetcherName: 'healthCheckQuery', +// inputSchemaName: 'HealthCheckInput', +// outputSchemaName: 'HealthCheckOutput', +// allowedMethods: ['GET', 'POST'], +// isNamedExport: true, +// isDefaultExport: false, +// isMutation: false, +// isQuery: true, +// bridge?: DataBridgeType | undefined, +// }, +// ... (more bridges) +// } +``` + +
+ +## Import Management + +### `readImportAliases()` + +Retrieve the import aliases from the main tsconfig.json in `@app/core`. + +```typescript +const aliases = readImportAliases('../../') +// => { +// '@app/core/appConfig': '@app/config', +// '@app/core/schemas': '@app/schemas', +// '@app/core/utils': '@app/utils', +// '@app/core/hooks': '@app/hooks', +// '@app/core/components/styled': '@app/primitives', +// '@app/core/components': '@app/components', +// '@app/core/forms': '@app/forms', +// '@app/core/screens': '@app/screens', +// '@app/core/assets': '@app/assets', +// '@app/core/resolvers': '@app/resolvers', +// '@app/core/middleware': '@app/middleware', +// '@green-stack/core/schemas': '@green-stack/schemas', +// '@green-stack/core/navigation/index': '@green-stack/navigation', +// '@green-stack/core/navigation': '@green-stack/navigation', +// '@green-stack/core/utils': '@green-stack/utils', +// '@green-stack/core/hooks': '@green-stack/hooks', +// '@green-stack/core/components': '@green-stack/components', +// '@green-stack/core/styles/index': '@green-stack/styles', +// '@green-stack/core/styles': '@green-stack/styles', +// '@green-stack/core/forms': '@green-stack/forms', +// '@green-stack/core/context': '@green-stack/context', +// '@green-stack/core/scripts': '@green-stack/scripts', +// '@green-stack/core/svg/svg.primitives': '@green-stack/svg', +// } +``` + +### `swapImportAlias()` + +Swap an import path with an alias if a match occurs. + +```typescript +swapImportAlias('@app/core/appConfig') +// => '@app/config' + +swapImportAlias('@app/config') +// => '@app/config' +``` + +
+ +## CLI Utilities + +### `opt()` + +Format a CLI option with proper styling and hierarchy. + +Will grey out the anything after the `--` when displayed in the terminal. + +```typescript +opt('Some Option') +// => 'Some Option' (nothing greyed out using ansi colors) + +opt('Some Option -- This extra info is greyed out') +// => 'Some option -- This extra info is greyed out' +``` + +
+ +## Helper Functions + +### `includesOption()` + +Higher-order function to prefill a list of options that are checked against in the actual filter method. + +```typescript +const includesGetOrPost = includesOption(['GET', 'POST']) +const methods = ['GET', 'POST', 'PUT'] +methods.filter(includesGetOrPost) // => ['GET', 'POST'] +``` + +### `maybeImport()` + +Attempts to `require()` a file, but returns an empty object if it fails or the file doesn't exist. + +```typescript +// File exists +maybeImport('./config.js') +// => { port: 3000, host: 'localhost' } + +// File doesn't exist +maybeImport('./nonexistent.js') +// => {} + +// With error logging +maybeImport('./config.js', 'logErrors') +// => Logs error if file doesn't exist +``` + +
+ +## Types + +### `SchemaFileMeta` + +Metadata about a schema file: + +```typescript +type SchemaFileMeta = { + schema?: ZodSchema | {} + schemaPath: string + schemaName: string + schemaFileName: string + schemaOption: string + workspacePath: string + workspaceName: string + isNamedExport: boolean + isDefaultExport: boolean +} +``` + +### `FetcherFileMeta` + +Metadata about a fetcher file: + +```typescript +type FetcherFileMeta = { + resolverName: string + fetcherName: string + fetcherType: 'query' | 'mutation' +} +``` + +### `BridgeFileMeta` + +Metadata about a data bridge file: + +```typescript +type BridgeFileMeta = { + bridge?: DataBridgeType | {} + bridgePath: string + bridgeName: string + bridgeOption: string + bridgeInputOption: string + workspacePath: string + workspaceName: string + resolverName: string + resolverType: 'query' | 'mutation' + operationType: 'add' | 'update' | 'delete' | 'get' | 'list' | 'search' + fetcherName: string + inputSchemaName: string + outputSchemaName: string + allowedMethods: ALLOWED_METHODS[] + isNamedExport: boolean + isDefaultExport: boolean + isMutation: boolean + isQuery: boolean +} +``` + +## Re-exports + +### Utils from `stringUtils` + +Re-exports all the utility functions from `@green-stack/core/utils/stringUtils`: + +- [`snakeToCamel()`](/@green-stack-core/utils/stringUtils#snaketocamel) +- [`snakeToDash()`](/@green-stack-core/utils/stringUtils#snaketodash) +- [`dashToCamel()`](/@green-stack-core/utils/stringUtils#dashtocamel) +- [`dashToSnake()`](/@green-stack-core/utils/stringUtils#dashtosnake) +- [`camelToSnake()`](/@green-stack-core/utils/stringUtils#cameltosnake) + +- [`uppercaseFirstChar()`](/@green-stack-core/utils/stringUtils#uppercasefirstchar) +- [`lowercaseFirstChar()`](/@green-stack-core/utils/stringUtils#lowercasefirstchar) + +- [`slugify()`](/@green-stack-core/utils/stringUtils#slugify) +- [`replaceStringVars()`](/@green-stack-core/utils/stringUtils#replacestringvars) +- [`replaceMany()`](/@green-stack-core/utils/stringUtils#replacemany) + +- [`includesAny()`](/@green-stack-core/utils/stringUtils#includesany) +- [`extractPathParams()`](/@green-stack-core/utils/stringUtils#extractpathparams) + +- [`ansi` - constants](/@green-stack-core/utils/stringUtils#ansi---constants) +- [`a` - terminal formatters](/@green-stack-core/utils/stringUtils#a---terminal-formatters) diff --git a/apps/docs/pages/@green-stack-core/utils/_meta.ts b/apps/docs/pages/@green-stack-core/utils/_meta.ts new file mode 100644 index 0000000..811e48f --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/_meta.ts @@ -0,0 +1,11 @@ + +export default { + 'styleUtils': 'styleUtils', + 'stringUtils': 'stringUtils', + 'objectUtils': 'objectUtils', + 'numberUtils': 'numberUtils', + 'functionUtils': 'functionUtils', + 'commonUtils': 'commonUtils', + 'arrayUtils': 'arrayUtils', + 'apiUtils': 'apiUtils', +} diff --git a/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx b/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx new file mode 100644 index 0000000..71ccfaf --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx @@ -0,0 +1,226 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## API Utils + + + + +# API Utils + +```typescript copy +import * as apiUtils from '@green-stack/utils/apiUtils' +``` + +Collection of utility functions for handling API requests, parameters, and security: + + + + + + + + + + + +
+ +## Disclaimer - Server Only + +> These utilities are designed for server-side use only. They are not intended for client-side execution and may not work correctly in a browser environment. + +
+ +## JSON Utils + +### `validateJSON()` + +Checks whether a string is valid JSON. + +```typescript +validateJSON('{"name": "John"}') // => true +validateJSON('{"name": John}') // => false (missing quotes) +validateJSON('not json') // => false +``` + +### `parseIfJSON()` + +Attempts to parse a string if it's valid JSON, otherwise returns the original value. + +```typescript +parseIfJSON('{"name": "John"}') // => { name: 'John' } +parseIfJSON('not json') // => 'not json' +parseIfJSON({ already: 'object' }) // => { already: 'object' } +``` + +
+ +## API Parameter Utils + +### `getApiParam()` + +Gets a specific property by key from supplied API sources (query, params, body, args, context). + +```typescript +const apiSources = { + query: { id: '123' }, + body: { name: 'John' }, + params: { type: 'user' } +} + +getApiParam('id', apiSources) // => '123' +getApiParam('name', apiSources) // => 'John' +getApiParam('type', apiSources) // => 'user' +getApiParam('nonexistent', apiSources) // => undefined +``` + +### `getApiParams()` + +Get multiple API parameters from supplied API sources. + +```typescript +const apiSources = { + query: { id: '123' }, + body: { name: 'John' }, + params: { type: 'user' } +} + +getApiParams(['id', 'name'], apiSources) +// => { id: '123', name: 'John' } + +getApiParams('id name type', apiSources) +// => { id: '123', name: 'John', type: 'user' } +``` + +### `getUrlParams()` + +Extracts and parses query parameters from a URL. + +```typescript +getUrlParams('https://api.example.com/users?id=123&name=John') +// => { id: 123, name: 'John' } + +getUrlParams('https://api.example.com/users') +// => {} +``` + +
+ +## Security Utils + +### `createHmac()` + +Creates a cryptographic signature based on the data and chosen algorithm. + +```typescript +createHmac('sensitive-data', 'sha256') +// => 'hashed-signature' + +createHmac('sensitive-data', 'md5') +// => 'hashed-signature' +``` + +### `encrypt()` + +Encrypts data using AES encryption with a secret key. + +```typescript +const encrypted = encrypt('sensitive-data', 'my-secret-key') +// => 'encrypted-string' + +// Using APP_SECRET environment variable +const encrypted = encrypt('sensitive-data') +// => 'encrypted-string' +``` + +### `decrypt()` + +Decrypts data that was encrypted using the `encrypt()` function. + +```typescript +const decrypted = decrypt(encrypted, 'my-secret-key') +// => 'sensitive-data' + +// Using APP_SECRET environment variable +const decrypted = decrypt(encrypted) +// => 'sensitive-data' +``` + +
+ +## Header Context Utils + +### `createMiddlewareContext()` + +Adds serializable context data to request headers (signed based on APP_SECRET). + +```typescript +const headers = await createMiddlewareContext( + request, + { userId: '123', role: 'admin' }, + { 'X-Custom-Header': 'value' } +) +// => Headers object with context and custom headers +``` + +### `getHeaderContext()` + +Extracts and validates header context from various request types. + +```typescript +// From NextApiRequest +const context = getHeaderContext(nextApiRequest) +// => { userId: '123', role: 'admin' } + +// From standard Request +const context = getHeaderContext(request) +// => { userId: '123', role: 'admin' } + +// From GetServerSidePropsContext +const context = getHeaderContext(context.req) +// => { userId: '123', role: 'admin' } +``` + +Note: Both header context utilities require the `APP_SECRET` environment variable to be set for signing and validation. + +
+ +## Fire and Forget Utils + +### `fireGetAndForget()` + +Fires a GET request and ignores whether it succeeds or not (useful for webhooks). + +```typescript +fireGetAndForget('https://api.example.com/webhook') +// => true (immediately, doesn't wait for response) + +// With custom config +fireGetAndForget('https://api.example.com/webhook', { + headers: { 'X-API-Key': 'secret' } +}) +``` + +### `firePostAndForget()` + +Fires a POST request and ignores whether it succeeds or not (useful for webhooks). + +```typescript +firePostAndForget('https://api.example.com/webhook', { event: 'user.created' }) +// => true (immediately, doesn't wait for response) + +// With custom config +firePostAndForget('https://api.example.com/webhook', + { event: 'user.created' }, + { headers: { 'X-API-Key': 'secret' } } +) +``` diff --git a/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx b/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx new file mode 100644 index 0000000..dabf1f3 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx @@ -0,0 +1,133 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Array Utils + + + + +# Array Utils + +```typescript copy +import * as arrayUtils from '@green-stack/utils/arrayUtils' +``` + +Collection of utility functions for array manipulation and set-like operations: + + + + + + + + + + + +
+ +## Set-like Operations + +### `arrFromSet()` + +Deduplicates items in an array using Set. + +```typescript +const numbers = [1, 2, 2, 3, 3, 3] +arrFromSet(numbers) // => [1, 2, 3] + +const strings = ['a', 'b', 'a', 'c'] +arrFromSet(strings) // => ['a', 'b', 'c'] + +// Works with objects too +const objects = [{ id: 1 }, { id: 1 }, { id: 2 }] +arrFromSet(objects) // => [{ id: 1 }, { id: 1 }, { id: 2 }] +``` + +### `addSetItem()` + +Adds an item to array if it doesn't exist already. Uses JSON.stringify for deep comparison. + +```typescript +const arr = [{ id: 1 }, { id: 2 }] + +// Adding a new item +addSetItem(arr, { id: 3 }) +// => [{ id: 1 }, { id: 2 }, { id: 3 }] + +// Adding a duplicate (won't add) +addSetItem(arr, { id: 1 }) +// => [{ id: 1 }, { id: 2 }, { id: 3 }] +``` + +### `removeSetItem()` + +Removes an item from an array. + +```typescript +const arr = [1, 2, 3, 4] + +removeSetItem(arr, 3) // => [1, 2, 4] +removeSetItem(arr, 5) // => [1, 2, 3, 4] (no change if item not found) +``` + +### `toggleArrayItem()` + +Adds or removes an item from an array (toggles its presence). + +```typescript +const arr = [1, 2, 3] + +// Adding an item +toggleArrayItem(arr, 4) // => [1, 2, 3, 4] + +// Removing an item +toggleArrayItem(arr, 2) // => [1, 3] +``` + +
+ +## Array Transformation + +### `createLookup()` + +Creates a lookup object from an array of objects, indexed by a specified property key. + +```typescript +const users = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Bob' } +] + +// Create a lookup by id +const userLookup = createLookup(users, 'id') +// => { +// 1: { id: 1, name: 'John' }, +// 2: { id: 2, name: 'Jane' }, +// 3: { id: 3, name: 'Bob' } +// } + +// Access user by id +userLookup[1] // => { id: 1, name: 'John' } + +// Items without the key are skipped +const mixedData = [ + { id: 1, name: 'John' }, + { name: 'Jane' }, // no id, will be skipped + { id: 3, name: 'Bob' } +] +const lookup = createLookup(mixedData, 'id') +// => { +// 1: { id: 1, name: 'John' }, +// 3: { id: 3, name: 'Bob' } +// } +``` \ No newline at end of file diff --git a/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx b/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx new file mode 100644 index 0000000..5861139 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx @@ -0,0 +1,126 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Common Utils + + + + +# Common Utils + +```typescript copy +import * as commonUtils from '@green-stack/utils/commonUtils' +``` + +Collection of common utility functions for general use: + + + + + + + + + + + +
+ +## Validation Utils + +### `isEmpty()` + +Checks for null, undefined, empty strings, empty objects or empty arrays. + +```typescript +// Empty values +isEmpty(null) // => true +isEmpty(undefined) // => true +isEmpty('') // => true +isEmpty([]) // => true +isEmpty({}) // => true + +// Non-empty values +isEmpty('hello') // => false +isEmpty([1, 2, 3]) // => false +isEmpty({ key: 'value' }) // => false + +// Custom behavior for empty strings +isEmpty('', false) // => false (when failOnEmptyStrings is false) +``` + +### `isKvRecord()` + +Checks whether an object is a simple key-value record (all values are primitives). + +```typescript +// Valid key-value records +isKvRecord({ name: 'John', age: 30 }) // => true +isKvRecord({ active: true, count: 0 }) // => true +isKvRecord({ title: undefined, id: null }) // => true + +// Invalid key-value records +isKvRecord({ data: [] }) // => false (array value) +isKvRecord({ handler: () => {} }) // => false (function value) +isKvRecord({ user: { name: 'John' } }) // => false (nested object) +isKvRecord([]) // => false (array) +isKvRecord(null) // => false (null) +``` + +
+ +## Console Utils + +These utilities help with logging messages only once, which is useful for one-off messages, warnings, or errors. + +### `consoleOnce()` + +Base function that logs to the console only once, skip on subsequent logs. + +```typescript +consoleOnce('This will only show once', console.log, 'Additional data') +consoleOnce('This will only show once', console.log, 'Additional data') // Won't show +``` + +### `logOnce()` + +Logs a message to the console only once. + +```typescript +logOnce('This will only show once') +logOnce('This will only show once') // Won't show +``` + +### `warnOnce()` + +Shows a warning message only once. + +```typescript +warnOnce('This warning will only show once') +warnOnce('This warning will only show once') // Won't show +``` + +### `errorOnce()` + +Shows an error message only once. + +```typescript +errorOnce('This error will only show once') +errorOnce('This error will only show once') // Won't show +``` + +All console utilities support additional arguments that will be passed to the console method: + +```typescript +logOnce('User logged in:', { userId: 123, timestamp: new Date() }) +warnOnce('Deprecated feature used:', { feature: 'oldMethod', version: '1.0.0' }) +errorOnce('API Error:', { status: 500, message: 'Internal Server Error' }) +``` diff --git a/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx b/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx new file mode 100644 index 0000000..611f79a --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx @@ -0,0 +1,95 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Function Utils + + + + +# Function Utils + +```typescript copy +import * as functionUtils from '@green-stack/utils/functionUtils' +``` + +Collection of utility functions for handling asynchronous operations and error handling: + + + + + + + + + + + +
+ +## Error Handling + +### `tryCatch()` + +Attempts to execute a promise, wraps it with try/catch, and returns either the data or error in a structured way. + +```typescript +// Successful promise +const successPromise = Promise.resolve({ data: 'success' }) +const result = await tryCatch(successPromise) +// => { data: { data: 'success' } } + +// Failed promise with Error +const errorPromise = Promise.reject(new Error('Something went wrong')) +const errorResult = await tryCatch(errorPromise) +// => { error: Error('Something went wrong') } + +// Failed promise with non-Error +const nonErrorPromise = Promise.reject('String error') +const nonErrorResult = await tryCatch(nonErrorPromise) +// => { error: Error('String error') } +``` + +Common use cases: + +```typescript +// API call with error handling +const fetchData = async () => { + const { data, error } = await tryCatch(fetch('/api/data')) + + if (error) { + console.error('Failed to fetch data:', error) + return null + } + + return data +} + +// Database operation +const createUser = async (userData) => { + const { data, error } = await tryCatch(db.users.create(userData)) + + if (error) { + // Handle specific error cases + if (error.message.includes('duplicate')) { + throw new Error('User already exists') + } + throw error + } + + return data +} +``` + +This is particularly useful when you want to: +- Handle errors in a consistent way +- Avoid try/catch blocks in your business logic +- Ensure all errors are properly converted to Error objects +- Keep error handling separate from the main logic flow \ No newline at end of file diff --git a/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx b/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx new file mode 100644 index 0000000..2d0927f --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx @@ -0,0 +1,138 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Number Utils + + + + +# Number Utils + +```typescript copy +import * as numberUtils from '@green-stack/utils/numberUtils' +``` + +Collection of utility functions for number manipulation and validation: + + + + + + + + + + + +
+ +## Random Number Utils + +### `randomInt()` + +Generates a random integer between a max & min range. + +```typescript +// Generate a random number between 0 and 10 +const result = randomInt(10) // => 7 + +// Generate a random number between 5 and 10 +const result2 = randomInt(10, 5) // => 8 +``` + +
+ +## Rounding Utils + +### `roundTo()` + +Uses scientific string notation to round numbers to specific decimals. Can pass a specific math rounding function as third arg. + +```typescript +// Round to 2 decimal places using default rounding +roundTo(3.14159, 2) // => 3.14 + +// Round to 2 decimal places using ceiling +roundTo(3.14159, 2, Math.ceil) // => 3.15 + +// Round to 2 decimal places using floor +roundTo(3.14159, 2, Math.floor) // => 3.14 +``` + +### `roundUpTo()` + +Convenience function to round up to specific decimals. + +```typescript +roundUpTo(3.14159, 2) // => 3.15 +``` + +### `roundDownTo()` + +Convenience function to round down to specific decimals. + +```typescript +roundDownTo(3.14159, 2) // => 3.14 +``` + +
+ +## Number Validation Utils + +### `hasLeadingZeroes()` + +Detects leading zeroes in "numbers". + +```typescript +hasLeadingZeroes('0568') // => true +hasLeadingZeroes('568') // => false +hasLeadingZeroes('0') // => false +``` + +### `isValidNumber()` + +Checks if a string is a valid number, excluding leading zeroes. + +```typescript +// Valid numbers +isValidNumber('0') // => true +isValidNumber('1') // => true +isValidNumber('250') // => true +isValidNumber('0.123') // => true +isValidNumber(42) // => true + +// Invalid numbers +isValidNumber('0568') // => false (leading zero) +isValidNumber('abc') // => false (not a number) +isValidNumber('') // => false (empty string) +isValidNumber(null) // => false (null) +isValidNumber(undefined) // => false (undefined) +isValidNumber(NaN) // => false (NaN) +``` + +### `parseIfValidNumber()` + +Checks if a variable can be parsed as a valid number, and then parses that number. + +```typescript +// Valid numbers +parseIfValidNumber('42') // => 42 +parseIfValidNumber('0.123') // => 0.123 +parseIfValidNumber(42) // => 42 + +// Invalid numbers +parseIfValidNumber('0568') // => undefined +parseIfValidNumber('abc') // => undefined +parseIfValidNumber('') // => undefined +parseIfValidNumber(null) // => undefined +parseIfValidNumber(undefined) // => undefined +parseIfValidNumber(NaN) // => undefined +``` \ No newline at end of file diff --git a/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx b/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx new file mode 100644 index 0000000..3223fe9 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx @@ -0,0 +1,206 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Object Utils + + + + +# Object Utils + +```typescript copy +import * as objectUtils from '@green-stack/utils/objectUtils' +``` + +Collection of utility functions for object manipulation and URL parameter handling: + + + + + + + + + + + +
+ +## URL Parameter Utils + +### `parseUrlParamsObject()` + +Parses object property values, converting string representations to their proper types. Handles nested objects and arrays. + +```typescript +// Basic type conversion +parseUrlParamsObject({ + id: '123', // => 123 (number) + active: 'true', // => true (boolean) + name: 'John' // => 'John' (string) +}) + +// Nested objects +parseUrlParamsObject({ + 'user.name': 'John', + 'user.age': '25' +}) +// => { user: { name: 'John', age: 25 } } + +// Arrays +parseUrlParamsObject({ + 'items[0]': '1', + 'items[1]': '2' +}) +// => { items: [1, 2] } + +// Ignoring specific keys +parseUrlParamsObject({ + id: '123', + raw: 'true' +}, ['raw']) +// => { id: 123, raw: 'true' } +``` + +### `buildUrlParamsObject()` + +Builds an object with all array and object keys flattened. Essentially the opposite of `parseUrlParamsObject()`. + +```typescript +// Arrays +buildUrlParamsObject({ arr: [0, 2] }) +// => { 'arr[0]': 0, 'arr[1]': 2 } + +// Nested objects +buildUrlParamsObject({ obj: { prop: true } }) +// => { 'obj.prop': true } + +// Empty values are removed +buildUrlParamsObject({ + name: 'John', + empty: null, + nested: { value: undefined } +}) +// => { name: 'John' } +``` + +
+ +## Object Transformation + +### `swapEntries()` + +Swaps the object's keys and values while keeping the types intact. Particularly useful for object enums. + +```typescript +const Status = { + ACTIVE: 'active', + INACTIVE: 'inactive' +} as const + +const StatusReverse = swapEntries(Status) +// => { +// active: 'ACTIVE', +// inactive: 'INACTIVE' +// } + +// Type-safe usage +const status: keyof typeof Status = 'ACTIVE' +const value: typeof Status[typeof status] = 'active' +``` + +### `createKey()` + +Turns an object into a string by deeply rendering both keys & values to string and joining them. + +```typescript +// Simple object +createKey({ id: 1, name: 'John' }) +// => 'id-1-name-John' + +// Nested object +createKey({ + user: { id: 1, name: 'John' }, + active: true +}) +// => 'active-true-user-id-1-name-John' + +// Array values +createKey({ + ids: [1, 2, 3], + tags: ['a', 'b'] +}) +// => 'ids-1-2-3-tags-a-b' + +// Custom separator +createKey({ id: 1, name: 'John' }, '_') +// => 'id_1_name_John' +``` + +
+ +## Dot Prop Utils + +The module re-exports several utilities from the `dot-prop` package for working with nested object properties: + +### `getProperty()` + +Gets a property from a nested object using dot notation. + +```typescript +const obj = { user: { name: 'John' } } +getProperty(obj, 'user.name') // => 'John' +``` + +### `setProperty()` + +Sets a property in a nested object using dot notation. + +```typescript +const obj = {} +setProperty(obj, 'user.name', 'John') +// => { user: { name: 'John' } } +``` + +### `deleteProperty()` + +Deletes a property from a nested object using dot notation. + +```typescript +const obj = { user: { name: 'John' } } +deleteProperty(obj, 'user.name') +// => { user: {} } +``` + +### `hasProperty()` + +Checks if a property exists in a nested object using dot notation. + +```typescript +const obj = { user: { name: 'John' } } +hasProperty(obj, 'user.name') // => true +hasProperty(obj, 'user.age') // => false +``` + +### `deepKeys()` + +Gets all keys from a nested object, including nested properties. + +```typescript +const obj = { + user: { + name: 'John', + address: { city: 'New York' } + } +} +deepKeys(obj) +// => ['user.name', 'user.address.city'] +``` \ No newline at end of file diff --git a/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx b/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx new file mode 100644 index 0000000..c067016 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx @@ -0,0 +1,231 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## String Utils + + + + +# String Utils + +```typescript copy +import * as stringUtils from '@green-stack/utils/stringUtils' +``` + +Collection of utility functions for string manipulation and formatting: + + + + + + + + + + + +
+ +## Case Conversion Utils + +### `snakeToCamel()` + +Converts a snake_case string to camelCase. + +```typescript +const result = snakeToCamel('hello_world') // => 'helloWorld' +``` + +### `snakeToDash()` + +Converts a snake_case string to kebab-case. + +```typescript +const result = snakeToDash('hello_world') // => 'hello-world' +``` + +### `dashToCamel()` + +Converts a kebab-case string to camelCase. + +```typescript +const result = dashToCamel('hello-world') // => 'helloWorld' +``` + +### `dashToSnake()` + +Converts a kebab-case string to snake_case. + +```typescript +const result = dashToSnake('hello-world') // => 'hello_world' +``` + +### `camelToSnake()` + +Converts a camelCase string to snake_case. + +```typescript +const result = camelToSnake('helloWorld') // => 'hello_world' +``` + +### `camelToDash()` + +Converts a camelCase string to kebab-case. + +```typescript +const result = camelToDash('helloWorld') // => 'hello-world' +``` + +
+ +## Character Case Utils + +### `uppercaseFirstChar()` + +Uppercases the first character of a string. + +```typescript +const result = uppercaseFirstChar('hello') // => 'Hello' +``` + +### `lowercaseFirstChar()` + +Lowercases the first character of a string. + +```typescript +const result = lowercaseFirstChar('Hello') // => 'hello' +``` + +
+ +## String Transformation Utils + +### `slugify()` + +Converts a string to a URL-friendly slug. + +```typescript +const result = slugify('Hello World!') // => 'hello-world' +``` + +### `replaceStringVars()` + +Replaces placeholders like `{somevar}` or `[somevar]` with values from injectables. + +```typescript +const result = replaceStringVars( + 'Hello {name}, welcome to [place]!', + { name: 'John', place: 'Paris' } +) // => 'Hello John, welcome to Paris!' +``` + +### `replaceMany()` + +Replaces every string you pass as the targets with the replacement string. + +```typescript +// Removes 'Update' or 'Add' from the string +replaceMany('useUpdateDataForm()', ['Update', 'Add'], '') +// => 'useDataForm()' + +replaceMany('useAddUserForm()', ['Update', 'Add'], '') +// => 'useUserForm()' +``` + +## String Analysis Utils + +### `findTargetString()` + +Finds a `$target$` string inside another string. + +```typescript +const folderWithIcons = findTargetString( + 'some/path/to/specific-folder/icons/', + 'some/path/to/$target$/icons/' +) // => 'specific-folder' +``` + +### `includesAny()` + +Checks whether a given string includes any of the provided words (case-insensitive). + +```typescript +const result = includesAny('Hello World', ['world', 'universe']) // => true +``` + +### `extractPathParams()` + +Extracts an array of potential params from a URL path. + +```typescript +const params = extractPathParams('/api/user/[slug]/posts/[id]') +// => ['slug', 'id'] +``` + +
+ +## ANSI Terminal Formatting + +We provide two main objects for terminal text formatting: + +### `ansi` - constants + +Constants for ANSI escape codes, including: + +- Text styles: `reset`, `bold`, `dim`, `underscore`, etc. +- Colors: `black`, `red`, `green`, `yellow`, etc. +- Backgrounds: `bgBlack`, `bgRed`, `bgGreen`, etc. + +You should finish each ANSI code with `reset` to clear formatting for what follows after it. + +To automate this, you can use the `a` helper functions below. + +### `a` - terminal formatters + +Ansi helper functions for applying ANSI formatting to strings: + +```typescript +// Utility +a.bold('...') // Makes text bold +a.underscore('...') // Underlines text +a.reset('...') // Resets all formatting for this string + after +a.italic('...') // Italicizes text + +// Colors +a.muted('...') // Makes text muted +a.black('...') // Black text +a.red('...') // Red text +a.green('...') // Green text +a.yellow('...') // Yellow text +a.blue('...') // Blue text +a.magenta('...') // Magenta text +a.cyan('...') // Cyan text +a.white('...') // White text + +// Backgrounds +a.bgBlack('...') // Black background +a.bgRed('...') // Red background +a.bgGreen('...') // Green background +a.bgYellow('...') // Yellow background +a.bgBlue('...') // Blue background +a.bgMagenta('...') // Magenta background +a.bgCyan('...') // Cyan background +a.bgWhite('...') // White background +``` + +Each formatting function accepts: +- `msg`: The message to format +- `clear`: Optional boolean to reset formatting before applying new style, default is `true`. + +Example: +```typescript +console.log(a.bold(a.red('Error:')) + ' Something went wrong') +``` diff --git a/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx b/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx new file mode 100644 index 0000000..1bfc4e5 --- /dev/null +++ b/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx @@ -0,0 +1,154 @@ +import { Image } from '@app/primitives' +import { FileTree } from 'nextra/components' +import { TitleWrapper, Hidden } from '@app/docs/components/Hidden' + + + ## Style Utils + + + + +# Style Utils + +```typescript copy +import * as styleUtils from '@green-stack/utils/styleUtils' +``` + +Collection of utility functions for handling CSS classes, theme colors, and CSS variable manipulation: + + + + + + + + + + + +
+ +## Class Name Utils + +### `cn()` + +Combines an array of classNames but filters out falsy array elements. Uses `clsx` and `tailwind-merge` under the hood. + +```typescript +// Basic usage +cn('text-red-500', 'bg-blue-500') +// => 'text-red-500 bg-blue-500' + +// With conditional classes +cn( + 'base-class', + isActive && 'active-class', + isError ? 'error-class' : 'success-class' +) +// => 'base-class active-class error-class' (when isActive is true and isError is true) + +// Merging Tailwind classes +cn('px-2 py-1 bg-red-500', 'px-4 bg-blue-500') +// => 'py-1 px-4 bg-blue-500' (later classes override earlier ones) +``` + +
+ +## CSS Variable Utils + +### `extractCssVar()` + +Extracts the CSS variable name from any string if present. + +```typescript +extractCssVar('var(--primary-color)') // => '--primary-color' +extractCssVar('color: var(--text-color)') // => '--text-color' +extractCssVar('regular-text') // => '' (no CSS variable found) +``` + +
+ +## Theme Color Utils + +### `getThemeColor()` + +Retrieves the nativewind theme color for the global.css variable provided. + +```typescript +// Get color for current theme +getThemeColor('primary-color') +// => '#007AFF' (or whatever the color is in your theme) + +// Get color for specific theme +getThemeColor('primary-color', 'dark') +// => '#0A84FF' (dark theme color) +``` + +### `useThemeColor()` + +React hook that retrieves the nativewind theme color for the global.css variable provided. + +```typescript +function MyComponent() { + const primaryColor = useThemeColor('primary-color') + // => '#007AFF' in light mode, '#0A84FF' in dark mode + + return ( +
+ Themed Text +
+ ) +} +``` + +
+ +## CSS Parsing Utils + +### `parseGlobalCSS()` + +Parses the contents of the global.css file to extract light & dark mode colors if present. Only detects colors defined within `:root` and `.dark:root`. + +```typescript +const globalCSS = ` +:root { + --primary-color: #007AFF; + --secondary-color: #5856D6; + --text-color: #000000; +} + +.dark:root { + --primary-color: #0A84FF; + --secondary-color: #5E5CE6; + --text-color: #FFFFFF; +} +` + +const themeColors = parseGlobalCSS(globalCSS) +// => { +// light: { +// '--primary-color': '#007AFF', +// '--secondary-color': '#5856D6', +// '--text-color': '#000000' +// }, +// dark: { +// '--primary-color': '#0A84FF', +// '--secondary-color': '#5E5CE6', +// '--text-color': '#FFFFFF' +// } +// } +``` + +The parser supports various color formats: +- Hex colors (`#RRGGBB` or `#RGB`) +- RGB/RGBA colors (`rgb(r, g, b)` or `rgba(r, g, b, a)`) +- HSL colors (`hsl(h, s%, l%)`) +- CSS variables (`var(--variable-name)`) + +Note: The parser will only detect colors if they are defined within `:root` and `.dark:root` selectors in your global CSS file. \ No newline at end of file diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx new file mode 100644 index 0000000..2f96f9e --- /dev/null +++ b/apps/docs/pages/_app.tsx @@ -0,0 +1,83 @@ +import type { AppProps } from 'next/app' +import React, { useEffect } from 'react' +import { useTheme } from 'nextra-theme-docs' +import { useColorScheme } from 'nativewind' +import UniversalAppProviders from '@app/screens/UniversalAppProviders' +import ServerStylesProvider from '@app/next/app/ServerStylesProvider' +import { SafeAreaProvider } from 'react-native-safe-area-context' +import { ComponentDocsContextManager } from '@app/core/mdx/ComponentDocs' +import { Image as NextContextImage } from '@green-stack/components/Image.next' +import { Link as NextContextLink } from '@green-stack/navigation/Link.next' +import { useRouter as useNextRouter } from 'next/router' +import { useRouter as useNextContextRouter } from '@green-stack/navigation/useRouter.next' +import { useRouteParams as useNextRouteParams } from '@green-stack/navigation/useRouteParams.next' +import '@app/next/global.css' + +/* --- ---------------------------------------------------------------------------------- */ + +export default function App({ Component, pageProps }: AppProps) { + // Navigation + const nextContextRouter = useNextContextRouter() + const nextRouter = useNextRouter() + + // Styles + const theme = useTheme() + const scheme = useColorScheme() + const resolvedTheme = theme.resolvedTheme || theme.systemTheme + + // -- Theme Effects -- + + useEffect(() => { + // Figure out the theme and apply it + const resolveTheme = () => { + const storedTheme = localStorage.getItem('theme') + let currentTheme = (resolvedTheme || storedTheme) as 'light' | 'dark' | 'system' + if (currentTheme === 'system' || !currentTheme) { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const prefersDark = mediaQuery.media === '(prefers-color-scheme: dark)' && mediaQuery.matches + currentTheme = prefersDark ? 'dark' : 'light' + } + theme.setTheme(currentTheme) + scheme.setColorScheme(currentTheme) + } + // Hacky, but it works 🤷‍♂️ (listening for theme or scheme changes is unreliable, sadly) + const queuedThemeCheck = () => { + window.scrollTo(0, 0) + new Array(20).fill(null).forEach((_, idx) => setTimeout(resolveTheme, idx * 200)) + } + // Attach listeners + const $themeButtons = document.querySelectorAll('button[title="Change theme"]') + $themeButtons.forEach($button => $button.addEventListener('click', queuedThemeCheck)) + // Trigger on mount or when theme changes + resolveTheme() + // Remove listeners + return () => $themeButtons.forEach($button => { + $button.removeEventListener('click', queuedThemeCheck) + }) + }, [resolvedTheme]) + + // -- Render -- + + return ( + + + + + + + + + + ) +} diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx new file mode 100644 index 0000000..a0e3129 --- /dev/null +++ b/apps/docs/pages/_document.tsx @@ -0,0 +1,52 @@ +import Document, { Html, Head, Main, NextScript } from 'next/document' + +/* --- Constants ------------------------------------------------------------------------------- */ + +const isProd = process.env.NODE_ENV === 'production' + +/* --- -------------------------------------------------------------------------- */ + +class AppDocument extends Document { + render() { + return ( + + + {isProd && } +