Compare commits

..

14 Commits

Author SHA1 Message Date
dswbx 91470d530f bump version for release 0.2.0 2024-12-01 09:02:32 +01:00
dswbx 0c8f3cd22a Merge pull request #9 from bknd-io/feat/astro-adapter-2
improved astro adapter (serving api) + added documentation
2024-12-01 09:00:40 +01:00
dswbx feeb13c053 improved astro adapter (serving api) + added documentation 2024-12-01 08:58:08 +01:00
dswbx b55fdd7516 fix invalid discord invite + added missing updates from astro adapter 2024-11-30 07:20:17 +01:00
dswbx 894a90fca9 Merge pull request #5 from bknd-io/feat/astro-adapter
Feat: Astro Adapter
2024-11-29 21:23:26 +01:00
dswbx 582dbd4272 added minimal astro adapter + improved the example 2024-11-29 21:21:59 +01:00
dswbx 6eb0d2242f Merge remote-tracking branch 'origin/main' into feat/astro-adapter 2024-11-29 20:14:31 +01:00
dswbx 85c9f35860 Merge pull request #8 from bknd-io/fix/auth-cookie-middleware
hotfix: cookies may not be added to static assets
2024-11-28 18:08:03 +01:00
dswbx 509d6826f0 hotfix: cookies may not be added to static assets 2024-11-28 18:07:29 +01:00
dswbx 4693be5615 added db for astro 2024-11-28 11:22:02 +01:00
dswbx bdc6eb55bf added bknd admin config override to make it easier to include <Admin /> 2024-11-28 11:15:07 +01:00
dswbx 54b38401d8 added fully working astro example 2024-11-28 10:22:18 +01:00
dswbx 49c2a7b4db Merge remote-tracking branch 'origin/main' into feat/astro-adapter 2024-11-28 10:12:37 +01:00
dswbx 6718419d41 prepare admin for astro: inject clientprovider props directly from admin component 2024-11-28 10:04:14 +01:00
47 changed files with 756 additions and 88 deletions
+1
View File
@@ -17,6 +17,7 @@ packages/media/.env
**/*/vite.config.ts.timestamp*
.history
**/*/.db/*
**/*/*.db
**/*/*.db-shm
**/*/*.db-wal
.npmrc
+5
View File
@@ -179,3 +179,8 @@ await tsup.build({
platform: "node",
format: ["esm", "cjs"]
});
await tsup.build({
...baseConfig("astro"),
format: ["esm", "cjs"]
});
+6 -1
View File
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.1.0",
"version": "0.2.0",
"scripts": {
"build:all": "bun run build && bun run build:cli",
"dev": "vite",
@@ -160,6 +160,11 @@
"import": "./dist/adapter/node/index.js",
"require": "./dist/adapter/node/index.cjs"
},
"./adapter/astro": {
"types": "./dist/adapter/astro/index.d.ts",
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs"
},
"./dist/styles.css": "./dist/ui/main.css",
"./dist/manifest.json": "./dist/static/manifest.json"
},
+31
View File
@@ -0,0 +1,31 @@
import { Api, type ApiOptions } from "bknd";
import { App, type CreateAppConfig } from "bknd";
type TAstro = {
request: Request;
};
export type Options = {
mode?: "static" | "dynamic";
} & Omit<ApiOptions, "host"> & {
host?: string;
};
export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
return new Api({
host: new URL(Astro.request.url).origin,
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
});
}
let app: App;
export function serve(config: CreateAppConfig) {
return async (args: TAstro) => {
if (!app) {
app = App.create(config);
await app.build();
}
return app.fetch(args.request);
};
}
+1
View File
@@ -0,0 +1 @@
export * from "./astro.adapter";
+3 -7
View File
@@ -1,4 +1,5 @@
import { withApi } from "bknd/adapter/nextjs";
import type { BkndAdminProps } from "bknd/ui";
import type { InferGetServerSidePropsType } from "next";
import dynamic from "next/dynamic";
@@ -10,15 +11,10 @@ export const getServerSideProps = withApi(async (context) => {
};
});
export function adminPage() {
export function adminPage(adminProps?: BkndAdminProps) {
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { ssr: false });
const ClientProvider = dynamic(() => import("bknd/ui").then((mod) => mod.ClientProvider));
return (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
if (typeof document === "undefined") return null;
return (
<ClientProvider user={props.user}>
<Admin />
</ClientProvider>
);
return <Admin withProvider={{ user: props.user }} {...adminProps} />;
};
}
+3 -2
View File
@@ -1,6 +1,7 @@
import type { BkndAdminProps } from "bknd/ui";
import { Suspense, lazy, useEffect, useState } from "react";
export function adminPage() {
export function adminPage(props?: BkndAdminProps) {
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
return () => {
const [loaded, setLoaded] = useState(false);
@@ -12,7 +13,7 @@ export function adminPage() {
return (
<Suspense>
<Admin />
<Admin {...props} />
</Suspense>
);
};
+22 -3
View File
@@ -1,5 +1,5 @@
import type { AppAuth } from "auth";
import type { ClassController } from "core";
import { type ClassController, isDebug } from "core";
import { Hono, type MiddlewareHandler } from "hono";
export class AuthController implements ClassController {
@@ -10,8 +10,27 @@ export class AuthController implements ClassController {
}
getMiddleware: MiddlewareHandler = async (c, next) => {
const user = await this.auth.authenticator.resolveAuthFromRequest(c);
this.auth.ctx.guard.setUserContext(user);
// @todo: ONLY HOTFIX
// middlewares are added for all routes are registered. But we need to make sure that
// only HTML/JSON routes are adding a cookie to the response. Config updates might
// also use an extension "syntax", e.g. /api/system/patch/data/entities.posts
// This middleware should be extracted and added by each Controller individually,
// but it requires access to the auth secret.
// Note: This doesn't mean endpoints aren't protected, just the cookie is not set.
const url = new URL(c.req.url);
const last = url.pathname.split("/")?.pop();
const ext = last?.includes(".") ? last.split(".")?.pop() : undefined;
if (
!this.auth.authenticator.isJsonRequest(c) &&
["GET", "HEAD", "OPTIONS"].includes(c.req.method) &&
ext &&
["js", "css", "png", "jpg", "jpeg", "svg", "ico"].includes(ext)
) {
isDebug() && console.log("Skipping auth", { ext }, url.pathname);
} else {
const user = await this.auth.authenticator.resolveAuthFromRequest(c);
this.auth.ctx.guard.setUserContext(user);
}
await next();
};
+2 -1
View File
@@ -228,9 +228,9 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
async requestCookieRefresh(c: Context) {
if (this.config.cookie.renew) {
console.log("renewing cookie", c.req.url);
const token = await this.getAuthCookie(c);
if (token) {
console.log("renewing cookie", c.req.url);
await this.setAuthCookie(c, token);
}
}
@@ -249,6 +249,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
}
// @todo: move this to a server helper
isJsonRequest(c: Context): boolean {
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
return c.req.header("Content-Type") === "application/json";
+2 -4
View File
@@ -1,4 +1,4 @@
import { type Client, type InStatement, createClient } from "@libsql/client/web";
import { type Client, type Config, type InStatement, createClient } from "@libsql/client/web";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
@@ -8,9 +8,7 @@ import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector";
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
export type LibSqlCredentials = {
url: string;
authToken?: string;
export type LibSqlCredentials = Config & {
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
};
+1 -1
View File
@@ -8,5 +8,5 @@ export {
type ModuleSchemas
} from "modules/ModuleManager";
export * from "./adapter";
export type * from "./adapter";
export { Api, type ApiOptions } from "./Api";
+16 -6
View File
@@ -1,26 +1,36 @@
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import type { ModuleConfigs } from "modules";
import React from "react";
import { FlashMessage } from "ui/modules/server/FlashMessage";
import { BkndProvider, ClientProvider, useBknd } from "./client";
import { BkndProvider, ClientProvider, type ClientProviderProps, useBknd } from "./client";
import { createMantineTheme } from "./lib/mantine/theme";
import { BkndModalsProvider } from "./modals";
import { Routes } from "./routes";
export type BkndAdminProps = {
baseUrl?: string;
withProvider?: boolean;
// @todo: add admin config override
withProvider?: boolean | ClientProviderProps;
config?: ModuleConfigs["server"]["admin"];
};
export default function Admin({ baseUrl: baseUrlOverride, withProvider = false }: BkndAdminProps) {
export default function Admin({
baseUrl: baseUrlOverride,
withProvider = false,
config
}: BkndAdminProps) {
const Component = (
<BkndProvider>
<BkndProvider adminOverride={config}>
<AdminInternal />
</BkndProvider>
);
return withProvider ? (
<ClientProvider baseUrl={baseUrlOverride}>{Component}</ClientProvider>
<ClientProvider
baseUrl={baseUrlOverride}
{...(typeof withProvider === "object" ? withProvider : {})}
>
{Component}
</ClientProvider>
) : (
Component
);
+11 -2
View File
@@ -14,6 +14,7 @@ type BkndContext = {
requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>;
app: AppReduced;
adminOverride?: ModuleConfigs["server"]["admin"];
};
const BkndContext = createContext<BkndContext>(undefined!);
@@ -21,8 +22,9 @@ export type { TSchemaActions };
export function BkndProvider({
includeSecrets = false,
adminOverride,
children
}: { includeSecrets?: boolean; children: any }) {
}: { includeSecrets?: boolean; children: any } & Pick<BkndContext, "adminOverride">) {
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
const [schema, setSchema] =
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
@@ -64,6 +66,13 @@ export function BkndProvider({
permissions: []
} as any);
if (adminOverride) {
schema.config.server.admin = {
...schema.config.server.admin,
...adminOverride
};
}
startTransition(() => {
setSchema(schema);
setWithSecrets(_includeSecrets);
@@ -86,7 +95,7 @@ export function BkndProvider({
const actions = getSchemaActions({ client, setSchema, reloadSchema });
return (
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
{children}
</BkndContext.Provider>
);
+7 -5
View File
@@ -17,11 +17,13 @@ export const queryClient = new QueryClient({
}
});
export const ClientProvider = ({
children,
baseUrl,
user
}: { children?: any; baseUrl?: string; user?: TApiUser | null }) => {
export type ClientProviderProps = {
children?: any;
baseUrl?: string;
user?: TApiUser | null | undefined;
};
export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => {
const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
const winCtx = useBkndWindowContext();
+1 -1
View File
@@ -1,4 +1,4 @@
export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider";
export { ClientProvider, type ClientProviderProps, useClient, useBaseUrl } from "./ClientProvider";
export { BkndProvider, useBknd } from "./BkndProvider";
export { useAuth } from "./schema/auth/use-auth";
+2 -2
View File
@@ -1,11 +1,11 @@
import type { ComponentPropsWithoutRef } from "react";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
export type AlertProps = ComponentPropsWithoutRef<"div"> & {
className?: string;
visible?: boolean;
title?: string;
message?: string;
message?: ReactNode | string;
};
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) =>
+1 -1
View File
@@ -1,4 +1,4 @@
export { default as Admin } from "./Admin";
export { default as Admin, type BkndAdminProps } from "./Admin";
export { Button } from "./components/buttons/Button";
export { Context } from "./components/Context";
export {
@@ -135,10 +135,10 @@ type FormInputElement = HTMLInputElement | HTMLTextAreaElement;
function EntityFormField({ fieldApi, field, action, data, ...props }: EntityFormFieldProps) {
const handleUpdate = useEvent((e: React.ChangeEvent<FormInputElement> | any) => {
if (typeof e === "object" && "target" in e) {
console.log("handleUpdate", e.target.value);
//console.log("handleUpdate", e.target.value);
fieldApi.handleChange(e.target.value);
} else {
console.log("handleUpdate-", e);
//console.log("handleUpdate-", e);
fieldApi.handleChange(e);
}
});
@@ -234,7 +234,7 @@ export function Setting<Schema extends TObject = any>({
<Breadcrumbs path={path} />
</AppShell.SectionHeader>
<AppShell.Scrollable key={path.join("-")}>
{typeof showAlert === "string" && <Alert.Warning message={showAlert} />}
{typeof showAlert !== "undefined" && <Alert.Warning message={showAlert} />}
<div className="flex flex-col flex-grow p-3 gap-3">
<JsonSchemaForm
+2 -36
View File
@@ -12,6 +12,7 @@ import { Setting } from "./components/Setting";
import { AuthSettings } from "./routes/auth.settings";
import { DataSettings } from "./routes/data.settings";
import { FlowsSettings } from "./routes/flows.settings";
import { ServerSettings } from "./routes/server.settings";
function SettingsSidebar() {
const { version, schema } = useBknd();
@@ -39,36 +40,6 @@ function SettingsSidebar() {
))}
</nav>
</div>
{/*<button
onClick={() =>
modals.openContextModal({
modal: "test",
title: "Test Modal",
innerProps: { modalBody: "This is a test modal" }
})
}
>
modal
</button>
<button
onClick={() =>
bkndModals.open(bkndModals.ids.test, { modalBody: "test" }, { title: "what" })
}
>
modal2
</button>
<button onClick={() => bkndModals.open("test", { modalBody: "test" })}>modal</button>
<button
onClick={() =>
bkndModals.open("debug", {
data: {
one: { what: 1 }
}
})
}
>
debug
</button>*/}
</AppShell.Scrollable>
</AppShell.Sidebar>
);
@@ -145,12 +116,7 @@ const SettingRoutesRoutes = () => {
return (
<>
<FallbackRoutes
module="server"
schema={schema}
config={config}
uiSchema={uiSchema.server}
/>
<ServerSettings schema={schema.server} config={config.server} />
<DataSettings schema={schema.data} config={config.data} />
<AuthSettings schema={schema.auth} config={config.auth} />
<FallbackRoutes module="media" schema={schema} config={config} uiSchema={uiSchema.media} />
@@ -0,0 +1,55 @@
import { cloneDeep } from "lodash-es";
import { useBknd } from "ui";
import { Setting } from "ui/routes/settings/components/Setting";
import { Route } from "wouter";
const uiSchema = {
cors: {
allow_methods: {
"ui:widget": "checkboxes"
},
allow_headers: {
"ui:options": {
orderable: false
}
}
}
};
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
const { app, adminOverride } = useBknd();
const { basepath } = app.getAdminConfig();
const _schema = cloneDeep(_unsafe_copy);
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const schema = _schema;
if (adminOverride) {
schema.properties.admin.readOnly = true;
}
return (
<Route path="/server" nest>
<Route
path="/"
component={() => (
<Setting
options={{
showAlert: () => {
if (adminOverride) {
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
}
return;
}
}}
schema={schema}
uiSchema={uiSchema}
config={config}
prefix={`${prefix}/server`}
path={["server"]}
/>
)}
nest
/>
</Route>
);
};
BIN
View File
Binary file not shown.
+120
View File
@@ -0,0 +1,120 @@
---
title: 'Astro'
description: 'Run bknd inside Astro'
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
Install bknd as a dependency:
<InstallBknd />
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
npx astro add react
```
You also need to make sure to set the output to `hybrid` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
```
<Note>
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI
statically using Astro's middleware. In case you're interested in this, feel free to reach
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new).
</Note>
## Serve the API
Create a new catch-all route at `src/pages/api/[...api].ts`:
```ts src/pages/api/[...api].ts
import { serve } from "bknd/adapter/astro";
export const prerender = false;
export const ALL = serve({
connection: {
type: "libsql",
config: {
url: "http://127.0.0.1:8080"
}
}
});
```
For more information about the connection object, refer to the [Setup](/setup) guide. In the
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
## Enabling the Admin UI
Create a new catch-all route at `src/pages/admin/[...admin].astro`:
```jsx src/pages/admin/[...admin].astro
---
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
export const prerender = false;
---
<html>
<body>
<Admin
withProvider={{ user }}
config={{ basepath: "/admin", color_scheme: "dark" }}
client:load
/>
</body>
</html>
```
## Example usage of the API
You use the API in both static and SSR pages. Just note that on static pages, authentication
might not work as expected, because Cookies are not available in the static context.
Here is an example of using the API in static context:
```jsx
---
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro);
const { data } = await api.data.readMany("todos");
---
<ul>
{data.map((todo) => (
<li>{todo.title}</li>
))}
</ul>
```
On SSR pages, you can also access the authenticated user:
```jsx
---
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
const { data } = await api.data.readMany("todos");
export const prerender = false;
---
{user
? <p>Logged in as <b>{user.email}</b>.</p>
: <p>Not authenticated.</p>}
<ul>
{data.map((todo) => (
<li>{todo.title}</li>
))}
</ul>
```
Check the [astro example](https://github.com/bknd-io/bknd/tree/main/examples/astro) for more implementation details.
+3 -1
View File
@@ -38,7 +38,9 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs";
import "bknd/dist/styles.css";
export { getServerSideProps };
export default adminPage();
export default adminPage({
config: { basepath: "/admin" }
});
```
## Example usage of the API in pages dir
+3 -1
View File
@@ -81,7 +81,9 @@ Create a new splat route file at `app/routes/admin.$.tsx`:
import { adminPage } from "bknd/adapter/remix";
import "bknd/dist/styles.css";
export default adminPage();
export default adminPage({
config: { basepath: "/admin" }
});
```
## Example usage of the API
+9
View File
@@ -46,6 +46,15 @@ in the future, so stay tuned!
</div>}
href="/integration/remix"
/>
<Card
title="Astro"
icon={<div className="text-primary-light">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/><path fill="currentColor" d="M9.24 19.035c-.901-.826-1.164-2.561-.789-3.819c.65.793 1.552 1.044 2.486 1.186c1.44.218 2.856.137 4.195-.524c.153-.076.295-.177.462-.278c.126.365.159.734.115 1.11c-.107.915-.56 1.622-1.283 2.158c-.289.215-.594.406-.892.608c-.916.622-1.164 1.35-.82 2.41l.034.114a2.4 2.4 0 0 1-1.07-.918a2.6 2.6 0 0 1-.412-1.401c-.003-.248-.003-.497-.036-.74c-.081-.595-.36-.86-.883-.876a1.034 1.034 0 0 0-1.075.843q-.013.058-.033.126M4.1 15.007s2.666-1.303 5.34-1.303l2.016-6.26c.075-.304.296-.51.544-.51c.25 0 .47.206.545.51l2.016 6.26c3.167 0 5.34 1.303 5.34 1.303L15.363 2.602c-.13-.366-.35-.602-.645-.602H9.283c-.296 0-.506.236-.645.602c-.01.024-4.538 12.405-4.538 12.405"/>
</svg>
</div>}
href="/integration/astro"
/>
<Card
title="Cloudflare"
icon={<div className="text-primary-light">
+3 -8
View File
@@ -22,8 +22,8 @@
},
"topbarLinks": [
{
"name": "Support",
"url": "mailto:hi@help.bknd.io"
"name": "Discord",
"url": "https://discord.gg/952SFk8Tb8"
}
],
"topbarCtaButton": {
@@ -56,12 +56,6 @@
"iconType": "solid",
"url": "/"
},
{
"name": "Discord",
"icon": "discord",
"iconType": "solid",
"url": "https://discord.gg/YAHnKXr5"
},
{
"name": "GitHub",
"icon": "github",
@@ -114,6 +108,7 @@
"integration/bun",
"integration/vite",
"integration/express",
"integration/astro",
"integration/nodejs",
"integration/deno",
"integration/browser"
+1 -1
View File
@@ -5,6 +5,6 @@
"dev": "mintlify dev"
},
"devDependencies": {
"mintlify": "^4.0.269"
"mintlify": "^4.0.285"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1732785435939
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="astro/client" />
+24
View File
@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
+47
View File
@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
+10
View File
@@ -0,0 +1,10 @@
// @ts-check
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
+25
View File
@@ -0,0 +1,25 @@
{
"name": "astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"db": "turso dev --db-file test.db",
"db:check": "sqlite3 test.db \"PRAGMA wal_checkpoint(FULL);\"",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.3",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^4.16.16",
"bknd": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.7.2"
}
}
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

+73
View File
@@ -0,0 +1,73 @@
---
interface Props {
title: string;
body: string;
done?: boolean;
}
const { done, title, body } = Astro.props;
---
<li class="link-card" data-done={done ? 1 : undefined}>
<div class="inner">
<div class="check">
<span>{done ? "✅" : "🔘"}</span>
</div>
<h2>
{title}
</h2>
<p>
{body}
</p>
</div>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card[data-done] {
background-color: #0c3e29;
}
.link-card > .inner {
position: relative;
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
.inner .check {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>
+1
View File
@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />
+137
View File
@@ -0,0 +1,137 @@
---
import Card from "../components/Card.astro";
interface Props {
title: string;
}
const { title } = Astro.props;
const path = new URL(Astro.request.url).pathname;
const items = [
{ href: "/", text: "Home (static)" },
{ href: "/ssr", text: "SSR (with auth)" },
{ href: "/admin", text: "Admin" }
];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<nav>
{items.map((item) => (
<a href={item.href} data-active={path === item.href}>{item.text}</a>
))}
</nav>
<main>
<div class="center">
<h1>bknd + <span class="text-gradient">Astro</span></h1>
<slot name="context" />
</div>
<slot />
</main>
</body>
</html>
<style is:global>
:root {
--accent: 136, 58, 234;
--accent-light: 224, 204, 250;
--accent-dark: 49, 10, 101;
--accent-gradient: linear-gradient(
45deg,
rgb(var(--accent)),
rgb(var(--accent-light)) 30%,
white 60%
);
}
html {
font-family: system-ui, sans-serif;
background: #13151a;
}
code {
font-family:
Menlo,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
}
a, a:visited {
color: white;
font-weight: bold;
}
nav {
margin: auto;
padding: 1rem;
width: 800px;
color: white;
display: flex;
flex-direction: row;
justify-content: center;
gap: 1rem;
}
nav a {
color: white;
text-decoration: none;
background-image: none;
}
nav a[data-active],
nav a:hover {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
main {
margin: auto;
padding: 1rem;
width: 800px;
max-width: calc(100% - 2rem);
color: white;
font-size: 20px;
line-height: 1.6;
}
main .center {
text-align: center;
margin-bottom: 2rem;
p {
opacity: 50%;
font-size: 1rem;
}
}
.astro-a {
position: absolute;
top: -32px;
left: 50%;
transform: translatex(-50%);
width: 220px;
height: auto;
z-index: -1;
}
h1 {
font-size: 4rem;
font-weight: 700;
line-height: 1;
text-align: center;
margin-bottom: 0.4em;
}
.text-gradient {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
</style>
@@ -0,0 +1,21 @@
---
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
export const prerender = false;
---
<html>
<body>
<Admin
withProvider={{ user }}
config={{ basepath: "/admin", color_scheme: "dark" }}
client:load
/>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
import { serve } from "bknd/adapter/astro";
export const prerender = false;
export const ALL = serve({
connection: {
type: "libsql",
config: {
url: "http://127.0.0.1:8080"
}
}
});
+29
View File
@@ -0,0 +1,29 @@
---
import { getApi } from "bknd/adapter/astro";
import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro";
const api = getApi(Astro);
const { data } = await api.data.readMany("todos");
---
<Layout title="Welcome to Astro.">
<p slot="context">Static Rendering</p>
<ul role="list" class="link-card-grid">
{data.map((todo: any) => (
<Card
done={todo.done}
title={todo.title}
body={todo.description}
/>
))}
</ul>
</Layout>
<style>
.link-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
gap: 2rem;
padding: 0;
}
</style>
+35
View File
@@ -0,0 +1,35 @@
---
import { getApi } from "bknd/adapter/astro";
import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro";
const api = getApi(Astro, { mode: "dynamic" });
const { data } = await api.data.readMany("todos");
const user = api.getUser();
export const prerender = false;
---
<Layout title="Welcome to Astro.">
<p slot="context">Server Side Rendering</p>
<ul role="list" class="link-card-grid">
{data.map((todo: any) => (
<Card
done={todo.done}
title={todo.title}
body={todo.description}
/>
))}
</ul>
<div class="center">
{user ? <p>Logged in as <b>{user?.email}</b>. <a href="/api/auth/logout">Logout</a></p> : <p>Not authenticated. <a href="/admin/auth/login">Sign in</a></p>}
</div>
</Layout>
<style>
.link-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
gap: 2rem;
padding: 0;
}
</style>
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}
@@ -2,4 +2,8 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs";
import "bknd/dist/styles.css";
export { getServerSideProps };
export default adminPage();
export default adminPage({
config: {
basepath: "/admin"
}
});
Binary file not shown.
+5 -1
View File
@@ -1,4 +1,8 @@
import { adminPage } from "bknd/adapter/remix";
import "bknd/dist/styles.css";
export default adminPage();
export default adminPage({
config: {
basepath: "/admin"
}
});