diff --git a/README.md b/README.md index 4689c7f..b44bc67 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,13 @@ ![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png) -bknd simplifies app development by providing fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services! +

+ +⭐ Live Demo + +

+ +bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services! **For documentation and examples, please visit https://docs.bknd.io.** @@ -17,7 +23,7 @@ bknd simplifies app development by providing fully functional backend for databa ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/elements/index.js?compression=gzip&label=bknd/elements) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/index.js?compression=gzip&label=bknd/ui) -The unpacked size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. +The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets. ## Motivation Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them. @@ -43,8 +49,8 @@ The package is mainly split into 4 parts, each serving a specific purpose: | Import | Purpose | |-----------------------------|------------------------------------------------------| -| `bknd`
`bknd/adapter/*` | Backend including all APIs | -| `bknd/ui` | Admin UI components for react frameworks at | +| `bknd`
`bknd/adapter/*` | Backend including APIs and adapters | +| `bknd/ui` | Admin UI components for react frameworks | | `bknd/client` | TypeScript SDK and React hooks for the API endpoints | | `bknd/elements` | React components for authentication and media | @@ -110,6 +116,7 @@ export default function App() { You don't have to figure out API details to include media uploads to your app. For an user avatar upload, this is all you need: ```tsx import { Media } from "bknd/elements" +import "bknd/dist/main.css" export function UserAvatar() { return ): ModuleBuildCon emgr: new EventManager(), guard: new Guard(), flags: Module.ctx_flags, + logger: new DebugLogger(false), ...overrides }; } diff --git a/app/package.json b/app/package.json index ebd8f9b..a04b686 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.1", + "version": "0.6.2", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index 7c2e926..c8584bf 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -1,10 +1,10 @@ -import { cloneDeep, get, has, mergeWith, omit, set } from "lodash-es"; +import { get, has, omit, set } from "lodash-es"; import { Default, type Static, type TObject, getFullPathKeys, - mark, + mergeObjectWith, parse, stripMark } from "../utils"; @@ -33,7 +33,7 @@ export class SchemaObject { ) { this._default = Default(_schema, {} as any) as any; this._value = initial - ? parse(_schema, cloneDeep(initial as any), { + ? parse(_schema, structuredClone(initial as any), { forceParse: this.isForceParse(), skipMark: this.isForceParse() }) @@ -64,8 +64,12 @@ export class SchemaObject { return this._config; } + clone() { + return structuredClone(this._config); + } + async set(config: Static, noEmit?: boolean): Promise> { - const valid = parse(this._schema, cloneDeep(config) as any, { + const valid = parse(this._schema, structuredClone(config) as any, { forceParse: true, skipMark: this.isForceParse() }); @@ -114,7 +118,7 @@ export class SchemaObject { } async patch(path: string, value: any): Promise<[Partial>, Static]> { - const current = cloneDeep(this._config); + const current = this.clone(); const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -122,11 +126,13 @@ export class SchemaObject { // overwrite arrays and primitives, only deep merge objects // @ts-ignore - const config = mergeWith(current, partial, (objValue, srcValue) => { + //console.log("---alt:new", _jsonp(mergeObject(current, partial))); + const config = mergeObjectWith(current, partial, (objValue, srcValue) => { if (Array.isArray(objValue) && Array.isArray(srcValue)) { return srcValue; } }); + //console.log("---new", _jsonp(config)); //console.log("overwritePaths", this.options?.overwritePaths); if (this.options?.overwritePaths) { @@ -164,14 +170,14 @@ export class SchemaObject { } } - //console.log("patch", { path, value, partial, config, current }); + //console.log("patch", _jsonp({ path, value, partial, config, current })); const newConfig = await this.set(config); return [partial, newConfig]; } async overwrite(path: string, value: any): Promise<[Partial>, Static]> { - const current = cloneDeep(this._config); + const current = this.clone(); const partial = path.length > 0 ? (set({}, path, value) as Partial>) : value; this.throwIfRestricted(partial); @@ -192,7 +198,7 @@ export class SchemaObject { if (p.length > 1) { const parent = p.slice(0, -1).join("."); if (!has(this._config, parent)) { - console.log("parent", parent, JSON.stringify(this._config, null, 2)); + //console.log("parent", parent, JSON.stringify(this._config, null, 2)); throw new Error(`Parent path "${parent}" does not exist`); } } @@ -207,7 +213,7 @@ export class SchemaObject { throw new Error(`Path "${path}" does not exist`); } - const current = cloneDeep(this._config); + const current = this.clone(); const removed = get(current, path) as Partial>; const config = omit(current, path); const newConfig = await this.set(config); diff --git a/app/src/core/utils/objects.ts b/app/src/core/utils/objects.ts index f8ae7a0..ab5b807 100644 --- a/app/src/core/utils/objects.ts +++ b/app/src/core/utils/objects.ts @@ -4,6 +4,14 @@ export function _jsonp(obj: any, space = 2): string { return JSON.stringify(obj, null, space); } +export function isPlainObject(value: unknown): value is Record { + return Object.prototype.toString.call(value) === "[object Object]"; +} + +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object"; +} + export function safelyParseObjectValues(obj: T): T { return Object.entries(obj).reduce((acc, [key, value]) => { try { @@ -97,15 +105,6 @@ export function objectEach, U>( ); } -/** - * Simple object check. - * @param item - * @returns {boolean} - */ -export function isObject(item) { - return item && typeof item === "object" && !Array.isArray(item); -} - /** * Deep merge two objects. * @param target @@ -197,3 +196,73 @@ export function objectCleanEmpty(obj: Obj): return acc; }, {} as any); } + +/** + * Lodash's merge implementation caused issues in Next.js environments + * From: https://thescottyjam.github.io/snap.js/#!/nolodash/merge + * NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge. + * @param object + * @param sources + */ +export function mergeObject(object, ...sources) { + for (const source of sources) { + for (const [key, value] of Object.entries(source)) { + if (value === undefined) { + continue; + } + + // These checks are a week attempt at mimicking the various edge-case behaviors + // that Lodash's `_.merge()` exhibits. Feel free to simplify and + // remove checks that you don't need. + if (!isPlainObject(value) && !Array.isArray(value)) { + object[key] = value; + } else if (Array.isArray(value) && !Array.isArray(object[key])) { + object[key] = value; + } else if (!isObject(object[key])) { + object[key] = value; + } else { + mergeObject(object[key], value); + } + } + } + + return object; +} + +/** + * Lodash's mergeWith implementation caused issues in Next.js environments + * From: https://thescottyjam.github.io/snap.js/#!/nolodash/mergeWith + * NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge. + * @param object + * @param sources + * @param customizer + */ +export function mergeObjectWith(object, source, customizer) { + for (const [key, value] of Object.entries(source)) { + const mergedValue = customizer(object[key], value, key, object, source); + if (mergedValue !== undefined) { + object[key] = mergedValue; + continue; + } + // Otherwise, fall back to default behavior + + if (value === undefined) { + continue; + } + + // These checks are a week attempt at mimicking the various edge-case behaviors + // that Lodash's `_.merge()` exhibits. Feel free to simplify and + // remove checks that you don't need. + if (!isPlainObject(value) && !Array.isArray(value)) { + object[key] = value; + } else if (Array.isArray(value) && !Array.isArray(object[key])) { + object[key] = value; + } else if (!isObject(object[key])) { + object[key] = value; + } else { + mergeObjectWith(object[key], value, customizer); + } + } + + return object; +} diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index 210c834..45edb15 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -13,7 +13,15 @@ import { type AppDataConfig, dataConfigSchema } from "./data-schema"; export class AppData extends Module { override async build() { - const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => { + const { + entities: _entities = {}, + relations: _relations = {}, + indices: _indices = {} + } = this.config; + + this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities)); + + const entities = transformObject(_entities, (entityConfig, name) => { return constructEntity(name, entityConfig); }); @@ -21,14 +29,14 @@ export class AppData extends Module { const name = typeof _e === "string" ? _e : _e.name; const entity = entities[name]; if (entity) return entity; - throw new Error(`Entity "${name}" not found`); + throw new Error(`[AppData] Entity "${name}" not found`); }; - const relations = transformObject(this.config.relations ?? {}, (relation) => + const relations = transformObject(_relations, (relation) => constructRelation(relation, _entity) ); - const indices = transformObject(this.config.indices ?? {}, (index, name) => { + const indices = transformObject(_indices, (index, name) => { const entity = _entity(index.entity)!; const fields = index.fields.map((f) => entity.field(f)!); return new EntityIndex(entity, fields, index.unique, name); @@ -52,6 +60,7 @@ export class AppData extends Module { ); this.ctx.guard.registerPermissions(Object.values(DataPermissions)); + this.ctx.logger.clear(); this.setBuilt(); } diff --git a/app/src/index.ts b/app/src/index.ts index 84a5d97..44b1ca5 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -14,3 +14,6 @@ export { registries } from "modules/registries"; export type * from "./adapter"; export { Api, type ApiOptions } from "./Api"; + +export type { MediaFieldSchema } from "media/AppMedia"; +export type { UserFieldSchema } from "auth/AppAuth"; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 0d5b8bf..ef0bc81 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -1,6 +1,6 @@ import type { App } from "App"; import type { Guard } from "auth"; -import { SchemaObject } from "core"; +import { type DebugLogger, SchemaObject } from "core"; import type { EventManager } from "core/events"; import type { Static, TSchema } from "core/utils"; import { @@ -35,6 +35,7 @@ export type ModuleBuildContext = { em: EntityManager; emgr: EventManager; guard: Guard; + logger: DebugLogger; flags: (typeof Module)["ctx_flags"]; }; diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 9a09bf9..efe7a09 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -231,7 +231,8 @@ export class ModuleManager { em: this.em, emgr: this.emgr, guard: this.guard, - flags: Module.ctx_flags + flags: Module.ctx_flags, + logger: this.logger }; } diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index 7ab5d9b..315d692 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -43,7 +43,11 @@ export function useBkndData() { return { config: async (partial: Partial): Promise => { console.log("patch config", entityName, partial); - return await bkndActions.patch("data", `entities.${entityName}.config`, partial); + return await bkndActions.overwrite( + "data", + `entities.${entityName}.config`, + partial + ); }, fields: entityFieldActions(bkndActions, entityName) }; diff --git a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx index 8b79f70..f73ce83 100644 --- a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx +++ b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx @@ -16,6 +16,7 @@ export type JsonSchemaFormProps = any & { uiSchema?: any; direction?: "horizontal" | "vertical"; onChange?: (value: any, isValid: () => boolean) => void; + cleanOnChange?: boolean; }; export type JsonSchemaFormRef = { @@ -36,6 +37,7 @@ export const JsonSchemaForm = forwardRef templates, fields, widgets, + cleanOnChange, ...props }, ref @@ -51,8 +53,8 @@ export const JsonSchemaForm = forwardRef return false; }; const handleChange = ({ formData }: any, e) => { - const clean = JSON.parse(JSON.stringify(formData)); - //console.log("Data changed: ", clean, JSON.stringify(formData, null, 2)); + const clean = cleanOnChange !== false ? JSON.parse(JSON.stringify(formData)) : formData; + console.log("Data changed: ", clean, { cleanOnChange }); setValue(clean); onChange?.(clean, () => isValid(clean)); }; diff --git a/app/src/ui/elements/auth/AuthScreen.tsx b/app/src/ui/elements/auth/AuthScreen.tsx index 340a89e..c828c8a 100644 --- a/app/src/ui/elements/auth/AuthScreen.tsx +++ b/app/src/ui/elements/auth/AuthScreen.tsx @@ -7,10 +7,23 @@ export type AuthScreenProps = { action?: "login" | "register"; logo?: ReactNode; intro?: ReactNode; + formOnly?: boolean; }; -export function AuthScreen({ method = "POST", action = "login", logo, intro }: AuthScreenProps) { +export function AuthScreen({ + method = "POST", + action = "login", + logo, + intro, + formOnly +}: AuthScreenProps) { const { strategies, basepath, loading } = useAuthStrategies(); + const Form = ; + + if (formOnly) { + if (loading) return null; + return Form; + } return (
@@ -25,7 +38,7 @@ export function AuthScreen({ method = "POST", action = "login", logo, intro }: A

Enter your credentials below to get access.

)} - + {Form} )} diff --git a/app/src/ui/elements/auth/index.ts b/app/src/ui/elements/auth/index.ts index b73224a..68843bd 100644 --- a/app/src/ui/elements/auth/index.ts +++ b/app/src/ui/elements/auth/index.ts @@ -1,3 +1,4 @@ +import { useAuthStrategies } from "../hooks/use-auth"; import { AuthForm } from "./AuthForm"; import { AuthScreen } from "./AuthScreen"; import { SocialLink } from "./SocialLink"; @@ -7,3 +8,5 @@ export const Auth = { Form: AuthForm, SocialLink: SocialLink }; + +export { useAuthStrategies }; diff --git a/app/src/ui/elements/index.ts b/app/src/ui/elements/index.ts index c2d2109..9072c1e 100644 --- a/app/src/ui/elements/index.ts +++ b/app/src/ui/elements/index.ts @@ -1,2 +1,2 @@ -export { Auth } from "./auth"; +export * from "./auth"; export * from "./media"; diff --git a/app/src/ui/elements/media/DropzoneContainer.tsx b/app/src/ui/elements/media/DropzoneContainer.tsx index adca51d..fa41f90 100644 --- a/app/src/ui/elements/media/DropzoneContainer.tsx +++ b/app/src/ui/elements/media/DropzoneContainer.tsx @@ -1,31 +1,32 @@ -import type { RepoQuery, RepoQueryIn } from "data"; +import type { RepoQueryIn } from "data"; import type { MediaFieldSchema } from "media/AppMedia"; import type { TAppMediaConfig } from "media/media-schema"; -import { useId } from "react"; -import { useApi, useBaseUrl, useEntityQuery, useInvalidate } from "ui/client"; +import { type ReactNode, createContext, useContext, useId } from "react"; +import { useApi, useEntityQuery, useInvalidate } from "ui/client"; import { useEvent } from "ui/hooks/use-event"; import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone"; import { mediaItemsToFileStates } from "./helper"; export type DropzoneContainerProps = { - children?: (props: DropzoneRenderProps) => JSX.Element; + children?: ReactNode; initialItems?: MediaFieldSchema[]; entity?: { name: string; id: number; field: string; }; + media?: Pick; query?: RepoQueryIn; -} & Partial> & - Partial; +} & Omit, "children" | "initialItems">; + +const DropzoneContainerContext = createContext(undefined!); export function DropzoneContainer({ initialItems, - basepath = "/api/media", - storage = {}, - entity_name = "media", + media, entity, query, + children, ...props }: DropzoneContainerProps) { const id = useId(); @@ -33,10 +34,11 @@ export function DropzoneContainer({ const baseUrl = api.baseUrl; const invalidate = useInvalidate(); const limit = query?.limit ? query?.limit : props.maxItems ? props.maxItems : 50; - console.log("dropzone:baseUrl", baseUrl); + const entity_name = (media?.entity_name ?? "media") as "media"; + //console.log("dropzone:baseUrl", baseUrl); const $q = useEntityQuery( - entity_name as "media", + entity_name, undefined, { ...query, @@ -89,6 +91,18 @@ export function DropzoneContainer({ autoUpload initialItems={_initialItems} {...props} - /> + > + {children + ? (props) => ( + + {children} + + ) + : undefined} + ); } + +export function useDropzone() { + return useContext(DropzoneContainerContext); +} diff --git a/app/src/ui/elements/media/index.ts b/app/src/ui/elements/media/index.ts index 142d2a7..ff5c8f8 100644 --- a/app/src/ui/elements/media/index.ts +++ b/app/src/ui/elements/media/index.ts @@ -1,11 +1,14 @@ import { PreviewWrapperMemoized } from "./Dropzone"; -import { DropzoneContainer } from "./DropzoneContainer"; +import { DropzoneContainer, useDropzone } from "./DropzoneContainer"; export const Media = { Dropzone: DropzoneContainer, - Preview: PreviewWrapperMemoized + Preview: PreviewWrapperMemoized, + useDropzone: useDropzone }; +export { useDropzone as useMediaDropzone }; + export type { PreviewComponentProps, FileState, diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index f7281ef..3ef6a49 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -201,7 +201,7 @@ const EntityContextMenu = ({ separator, { icon: IconSettings, - label: "Settings", + label: "Advanced", onClick: () => navigate(routes.settings.path(["data", "entities", entity.name]), { absolute: true diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index d096731..960a4b6 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -40,8 +40,8 @@ export function DataEntityList({ params }) { useBrowserTitle(["Data", entity?.label ?? params.entity]); const [navigate] = useNavigate(); const search = useSearch(searchSchema, { - select: undefined, - sort: undefined + select: entity.getSelect(undefined, "form"), + sort: entity.getDefaultSort() }); const $q = useApiQuery( diff --git a/app/src/ui/routes/test/tests/dropzone-element-test.tsx b/app/src/ui/routes/test/tests/dropzone-element-test.tsx index 3abaebe..6e53d95 100644 --- a/app/src/ui/routes/test/tests/dropzone-element-test.tsx +++ b/app/src/ui/routes/test/tests/dropzone-element-test.tsx @@ -1,4 +1,4 @@ -import { type DropzoneRenderProps, Media } from "ui/elements"; +import { Media } from "ui/elements"; import { Scrollable } from "ui/layouts/AppShell/AppShell"; export default function DropzoneElementTest() { @@ -12,7 +12,7 @@ export default function DropzoneElementTest() { maxItems={1} overwrite > - {(props) => } +
@@ -49,12 +49,13 @@ export default function DropzoneElementTest() { ); } -function CustomUserAvatarDropzone({ - wrapperRef, - inputProps, - state: { files, isOver, isOverAccepted, showPlaceholder }, - actions: { openFileInput } -}: DropzoneRenderProps) { +function CustomUserAvatarDropzone() { + const { + wrapperRef, + inputProps, + state: { files, isOver, isOverAccepted, showPlaceholder }, + actions: { openFileInput } + } = Media.useDropzone(); const file = files[0]; return ( diff --git a/bun.lockb b/bun.lockb index 19836ad..1b049f9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/usage/elements.mdx b/docs/usage/elements.mdx index 0a517fa..299ecd3 100644 --- a/docs/usage/elements.mdx +++ b/docs/usage/elements.mdx @@ -3,23 +3,134 @@ title: "React Elements" description: "Speed up your frontend development" --- - - The documentation is currently a work in progress and not complete. - - - Not only creating and maintaing a backend is time-consuming, but also integrating it into your frontend can be a hassle. With `bknd/elements`, you can easily add media uploads and authentication forms to your app without having to figure out API details. -## Media uploads + + In order to use these exported elements, make sure to wrap your app inside `ClientProvider`. See the [React Setup](/usage/react#setup) for more information. + + +# Media + +## Media.Dropzone +The `Media.Dropzone` element allows retrieving from and uploading media items to your bknd instance. Without any properties specified, it will behave similar to your media library inside the bknd Admin UI. Here is how to get the last 10 items: ```tsx import { Media } from "bknd/elements" -export function UserAvatar() { +export default function MediaGallery() { + return +} +``` + +Since you can also upload media to a specific entity, you can also point that `Dropzone` to it. Here is an example of a single user avatar that gets overwritten on re-upload: + +```tsx +import { Media } from "bknd/elements"; + +export default function UserAvatar() { return +} +``` + +### Props +- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects. +- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity. +- `query?: RepoQueryIn`: Query to filter the media items. +- `overwrite?: boolean`: If true, the media item will be overwritten on entity media uploads if limit was reached. +- `maxItems?: number`: Maximum number of media items that can be uploaded. +- `autoUpload?: boolean`: If true, the media items will be uploaded automatically. +- `onRejected?: (files: FileWithPath[]) => void`: Callback when a file is rejected. +- `onDeleted?: (file: FileState) => void`: Callback when a file is deleted. +- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded. +- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present. + +### Customize Rendering +You can also customize the rendering of the media items and its uploading by passing a react element as a child. Here is an example of a custom `Media.Dropzone` that renders an user avatar (styled using tailwind): + +```tsx +import { Media, useMediaDropzone } from "bknd/elements"; + +export default function CustomUserAvatar() { + return + + +} + +function CustomUserAvatar() { + const { + wrapperRef, + inputProps, + state: { files, isOver, isOverAccepted, showPlaceholder }, + actions: { openFileInput } + } = useMediaDropzone(); + + const file = files[0]; + + return ( +
+
+ +
+ {showPlaceholder && <>{isOver && isOverAccepted ? "let it drop" : "drop here"}} + {file && ( + + )} +
+ ); +} +``` + +# Auth +Adding authentication to your app with bknd is as easy as adding a `
` with an action pointing to the action (`login` or `register`) to the strategy you want to use, e.g. for the password strategy, use `/api/auth/password/login`. But to make it even easier, you can use the `Auth.*` elements. + +## `Auth.Screen` +The `Auth.Screen` element is a wrapper around the `Auth.Form` element that provides a full page screen. The current layout is admittedly very basic, but there will be more customization options in the future. + +```tsx +import { Auth } from "bknd/elements" + +export default function LoginScreen() { + return +} +``` + +### Props +Note that this component doesn't require any strategy-specific information, as it gathers it itself. + +- `action: "login" | "register"`: The action to perform. +- `method?: "POST" | "GET"`: The method to use for the form. + + + +## `Auth.Form` +If you only wish to render the form itself without the screen, you can use the `Auth.Form` element. Unlike the `Auth.Screen`, this element requires the `strategy` prop to be set to the strategy you want to use. You can either specify it manually, use use the exported hook `useAuthStrategies()` for fetch them from your bknd instance. + +```tsx +import { Auth, useAuthStrategies } from "bknd/elements" + +export default function LoginForm() { + const { strategies, basepath, loading } = useAuthStrategies(); + if (loading) return null; + + return } ``` \ No newline at end of file diff --git a/docs/usage/react.mdx b/docs/usage/react.mdx index 753e93b..1e45325 100644 --- a/docs/usage/react.mdx +++ b/docs/usage/react.mdx @@ -3,7 +3,7 @@ title: 'SDK (React)' description: 'Use the bknd SDK for React' --- -bknd exports 4 useful hooks to work with your backend: +There are 4 useful hooks to work with your backend: 1. simple hooks which are solely based on the [API](/usage/sdk): - [`useApi`](#useapi) - [`useEntity`](#useentity) @@ -11,6 +11,22 @@ bknd exports 4 useful hooks to work with your backend: - [`useApiQuery`](#useapiquery) - [`useEntityQuery`](#useentityquery) + +## Setup +In order to use them, make sure you wrap your `` inside ``, so that these hooks point to your bknd instance: + +```tsx +import { ClientProvider } from "bknd/client"; + +export default function App() { + return + {/* your app */} + +} +``` + +For all other examples below, we'll assume that your app is wrapped inside the `ClientProvider`. + ## `useApi()` To use the simple hook that returns the Api, you can use: ```tsx