diff --git a/site/app/registry/[...slug]/page.tsx b/site/app/registry/[...slug]/page.tsx index f52caf3..22387e9 100644 --- a/site/app/registry/[...slug]/page.tsx +++ b/site/app/registry/[...slug]/page.tsx @@ -1,4 +1,5 @@ import { type Plugin, PluginPage } from "@/components/plugin-page"; +import { getPluginData, getPlugins } from "@/lib/registry"; import type { Metadata } from "next"; import { NextPage } from "next"; import { notFound, useRouter } from "next/navigation"; @@ -20,51 +21,12 @@ const Page = async ({ params }: { params: { slug: string[] } }) => { return ; }; -/** - * Returns versions in descending order - * - * @param title The title of the plugin - * @param version The version of the plugin. If not provided, the latest version is returned - */ -const getPluginData = async ( - title: string, - version?: string, -): Promise => { - const versions = [ - { version: "0.1.2", date: new Date() }, - { version: "0.1.1", date: new Date() }, - { version: "0.1.0", date: new Date() }, - ]; - - return { - title, - version: - versions.find((v) => v.version === version)?.version ?? - versions[0].version, - versions, - configSchema: - '{"properties": { "config": { "properties": { "ip": { "description": "The ip address of the device to connect to.", "items": [ { "format": "uint8", "minimum": 0.0, "type": "integer" }, { "format": "uint8", "minimum": 0.0, "type": "integer" }, { "format": "uint8", "minimum": 0.0, "type": "integer" }, { "format": "uint8", "minimum": 0.0, "type": "integer" } ], "maxItems": 4, "minItems": 4, "type": "array" } }, "required": [ "ip" ], "type": "object" }, "plugin": { "const": "tasmota@0.1.1" }},"required": [ "plugin", "config"],"type": "object"}', - author: "Alex Lyon", - source: "https://github.com/arlyon/litehouse", - capabilities: ["http-client"], - homepage: "https://github.com/arlyon/litehouse", - description: "A real cool plugin!", - size: 60345, - }; -}; - -const getPlugins = async (): Promise< - { title: string; versions: string[] }[] -> => { - return [{ title: "tasmota", versions: ["0.1.2", "0.1.1", "0.1.0"] }]; -}; - export default Page; export async function generateStaticParams() { const results = (await getPlugins()).flatMap((page) => [undefined, ...page.versions].map((version) => ({ - slug: [page.title, version].filter((x) => x !== undefined), + slug: [page.title, version?.version].filter((x) => x !== undefined), })), ); return results; @@ -82,8 +44,6 @@ export async function generateMetadata({ pluginData.versions.find((v) => v.version === params.slug[1]) ?? pluginData.versions[0]; - console.log(params); - // if (page == null) notFound(); return { diff --git a/site/app/registry/page.tsx b/site/app/registry/page.tsx index fc2f743..3604ab3 100644 --- a/site/app/registry/page.tsx +++ b/site/app/registry/page.tsx @@ -1,19 +1,15 @@ import { RegistryPage } from "@/components/registry-page"; +import { getPluginData, getPlugins } from "@/lib/registry"; + +export default async function HomePage() { + const packages = await getPlugins(); -export default function HomePage() { return (
diff --git a/site/bun.lockb b/site/bun.lockb index 003886f..c7c73da 100755 Binary files a/site/bun.lockb and b/site/bun.lockb differ diff --git a/site/components/add-button.tsx b/site/components/add-button.tsx index 0958928..67670dd 100644 --- a/site/components/add-button.tsx +++ b/site/components/add-button.tsx @@ -29,7 +29,7 @@ export const AddButton = ({ onClick={() => remove(id)} > - Del + Remove ) : ( ); }; diff --git a/site/components/copy-box.tsx b/site/components/copy-box.tsx index 8f04d91..f5e617a 100644 --- a/site/components/copy-box.tsx +++ b/site/components/copy-box.tsx @@ -29,21 +29,27 @@ function classNames(...classes: (string | undefined)[]) { export function CopyBox({ command, className, + beforeCopy, }: { command: string; className?: string; + beforeCopy?: () => boolean; }) { return (
- {command} +
+ {command} +
{ - console.log(command); + let accept = beforeCopy?.(); + if (accept === false) return; + navigator.clipboard.writeText(command); }} />
diff --git a/site/components/manifest-editor.tsx b/site/components/manifest-editor.tsx index b6298e8..fea840c 100644 --- a/site/components/manifest-editor.tsx +++ b/site/components/manifest-editor.tsx @@ -28,8 +28,6 @@ import { ManifestButton } from "./manifest-button"; export function ManifestEditor() { const { items } = useManifestStore(); - console.log(items); - return ( diff --git a/site/components/plugin-page.tsx b/site/components/plugin-page.tsx index e880fa4..8269fbe 100644 --- a/site/components/plugin-page.tsx +++ b/site/components/plugin-page.tsx @@ -1,50 +1,8 @@ -/** - * This code was generated by v0 by Vercel. - * @see https://v0.dev/t/IeZF9j1Xy5j - * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app - */ - -/** Add fonts into your Next.js project: - -import { Archivo } from 'next/font/google' -import { Rethink_Sans } from 'next/font/google' - -archivo({ - subsets: ['latin'], - display: 'swap', -}) - -rethink_sans({ - subsets: ['latin'], - display: 'swap', -}) - -To read more about using these font, please visit the Next.js documentation: -- App Directory: https://nextjs.org/docs/app/building-your-application/optimizing/fonts -- Pages Directory: https://nextjs.org/docs/pages/building-your-application/optimizing/fonts -**/ - import { GithubStars as GithubBanner } from "@/components/github-stars"; -/** Add border radius CSS variable to your global CSS: - -:root { - --radius: 0rem; -} -**/ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { StarIcon } from "lucide-react"; +import { SchemaEditor } from "./shema-editor"; +import { SVGProps } from "react"; import Link from "next/link"; -import type { SVGProps } from "react"; -import { Suspense } from "react"; import { AddButton } from "./add-button"; -import { CopyBox } from "./copy-box"; export type Plugin = { title: string; @@ -89,8 +47,6 @@ export function PluginPage( }, ) { const format = new Intl.DateTimeFormat("en-US"); - const addCommand = "litehouse::bGl0ZWhvdXNl"; - const id = `${props.title}@${props.version}`; return (
@@ -172,67 +128,7 @@ export function PluginPage(

Configuration

-
-
-
-
-              {JSON.stringify(
-                {
-                  $schema: "./schema.json",
-                  plugins: {
-                    instance: {
-                      lat: 24.0,
-                      lon: 25.0,
-                    },
-                  },
-                  imports: [id],
-                },
-                null,
-                2,
-              )}
-            
-
-
-
- - - - Key - Value - Type - - - - - apiKey - abc123 - string - - - endpoint - https://api.example.com - string - - - debug - true - boolean - - -
-
-
-

Add To Manifest

-

- Run the following command in your project directory to automatically - insert this plugin and config into your manifest. -

- -
-
+
); } diff --git a/site/components/registry-page.tsx b/site/components/registry-page.tsx index 99fbe7b..6995745 100644 --- a/site/components/registry-page.tsx +++ b/site/components/registry-page.tsx @@ -39,19 +39,17 @@ export function RegistryPage(props: { packages: { title: string; description: string; - version: string; + version: { version: string; date: Date }; downloads?: number; }[]; }) { return (
- 4 out of 4 results + {props.pluginCount} out of {props.pluginCount} results
- {props.packages?.map((p) => ( - - ))} + {props.packages?.map((p) => )}
); @@ -60,7 +58,7 @@ export function RegistryPage(props: { function Package(props: { title: string; description: string; - version: string; + version: { version: string; date: Date }; downloads?: number; }) { const formatter = new Intl.NumberFormat("en-US"); @@ -69,7 +67,7 @@ function Package(props: {

- + {props.title}

@@ -77,7 +75,7 @@ function Package(props: {
{props.version ? ( - v{props.version} + v{props.version.version} ) : null} {props.downloads !== undefined && props.version ? ( diff --git a/site/components/shema-editor.tsx b/site/components/shema-editor.tsx new file mode 100644 index 0000000..4616397 --- /dev/null +++ b/site/components/shema-editor.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { StarIcon } from "lucide-react"; +import Link from "next/link"; +import type { SVGProps } from "react"; +import { Suspense, useState } from "react"; +import { AddButton } from "./add-button"; +import { CopyBox } from "./copy-box"; +import z from "zod"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp"; +import { useForm } from "react-hook-form"; +import Ajv from "ajv"; + +const configSchema = (name: string) => + z.object({ + type: z.literal("object"), + required: z.array(z.string()), + properties: z.object({ + config: z.object({ + properties: z.record(z.any()), + required: z.array(z.string()), + type: z.literal("object"), + }), + plugin: z.object({ + const: z.literal(name), + }), + }), + }); + +export const SchemaEditor = ({ id, schema: schemaString }) => { + const form = useForm({ + defaultValues: { + config: {}, + plugin: id, + }, + }); + + const [text, setText] = useState( + JSON.stringify( + { + $schema: "./schema.json", + plugins: { + instance: form.getValues().config, + }, + imports: [id], + }, + null, + 2, + ), + ); + const [addCommand, setAddCommand] = useState(null); + + const schemaData = JSON.parse(schemaString); + const schema = configSchema(id).safeParse(schemaData); + if (!schema.success) { + console.log(schemaData, schema.error); + return
Invalid schema
; + } + + const nonConstFields = schema.data.properties.config.properties; + + form.watch((data, { name, type }) => { + setText( + JSON.stringify( + { + $schema: "./schema.json", + plugins: { + instance: data, + }, + imports: [id], + }, + null, + 2, + ), + ); + + const conf = JSON.stringify({ + instance: data.config, + }); + + console.log(conf, window.btoa(conf)); + + setAddCommand(window.btoa(conf)); + }); + + return ( +
+
+
+
{text}
+
+
+
{ + console.log(data); + })} + > + {Object.entries(nonConstFields).map(([key, value]) => ( +
+
+ {key} + + {value.description} + +
+ {matchInput(key, value, form)} +
+ ))} +
+
+

Add To Manifest

+

+ Run the following command in your project directory to automatically + insert this plugin and config into your manifest. +

+ { + const ajv = new Ajv({ + formats: { + uint8: { + type: "number", + async: false, + validate: (x) => x >= 0 && x <= 255, + }, + double: { + type: "number", + async: false, + validate: (x) => typeof x === "number", + }, + }, + }); + const valid = ajv.validate(schemaData, form.getValues()); + if (!valid) { + console.log("ERROR", ajv.errors); + return false; + } else { + return true; + } + }} + className="text-sm" + command={`litehouse add ${id}${addCommand ? `#${addCommand}` : ""}`} + /> +
+
+ ); +}; + +const matchInput = (key: string, value: object, form: any) => { + const validators = [ + [ + (value) => value.type === "array" && value.minItems === value.maxItems, + + {value?.items?.map((item, i) => ( + + ))} + , + ] as const, + [ + (value) => value.type === "number", + , + ], + ]; + + const result = validators.find(([validator]) => validator(value)); + return result?.[1]; +}; + +const InputGroup = ({ children }) => { + return
{children}
; +}; + +const InputGroupItem = ({ type, ...props }) => { + return ( + + ); +}; + +// convert into form props +const getType = (props: object): object => { + const ret = {}; + + switch (props.type) { + case "string": + ret.type = "text"; + break; + case "integer": + ret.type = "number"; + break; + case "boolean": + ret.type = "checkbox"; + break; + } + if (props.minimum) { + ret.min = props.minimum; + } + + return ret; +}; diff --git a/site/components/ui/input-otp.tsx b/site/components/ui/input-otp.tsx new file mode 100644 index 0000000..05b5355 --- /dev/null +++ b/site/components/ui/input-otp.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { OTPInput, OTPInputContext } from "input-otp" +import { Dot } from "lucide-react" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/site/hooks/use-indexed-db.ts b/site/hooks/use-indexed-db.ts index 9c4ae00..4129811 100644 --- a/site/hooks/use-indexed-db.ts +++ b/site/hooks/use-indexed-db.ts @@ -27,7 +27,6 @@ export const useIndexedDb = ( }; dbReq.onupgradeneeded = (event) => { - console.log("upgrading db"); // @ts-expect-error const db = event.target.result; const store = db.createObjectStore(storeName, { keyPath }); @@ -57,11 +56,10 @@ export const useIndexedDb = ( .delete(id); if (!val) return; val.onsuccess = () => { - console.log("removed item", id, generation); increment(); }; val.onerror = (e) => { - console.log(e); + console.error(e); }; }, [db, generation, increment, storeName], @@ -74,7 +72,6 @@ export const useIndexedDb = ( const valueGen = useRef(null); // current generation of value if (!db) { - console.log("db not ready"); return undefined; } @@ -82,12 +79,6 @@ export const useIndexedDb = ( valueGen.current === generation || inflightGen.current === generation ) { - console.log( - "returning cached", - valueGen.current, - inflightGen.current, - generation, - ); return value; } @@ -100,12 +91,7 @@ export const useIndexedDb = ( } const req = callback(store); - console.log( - "launching query", - valueGen.current, - inflightGen.current, - generation, - ); + inflightGen.current = generation; req.onsuccess = (event) => { if (valueGen.current && valueGen.current > generation) return; // newer data is here @@ -114,7 +100,6 @@ export const useIndexedDb = ( setValue(event.target.result); }; - console.log("returning value"); return value; }, [db, generation, storeName], @@ -154,7 +139,6 @@ export const useManifestStore = () => { ); const remove = useCallback( (id: string) => { - console.log("REMOVE", id, removeInner); removeInner?.(id); }, [removeInner], diff --git a/site/lib/registry.ts b/site/lib/registry.ts new file mode 100644 index 0000000..66b4582 --- /dev/null +++ b/site/lib/registry.ts @@ -0,0 +1,66 @@ +import { type Plugin, PluginPage } from "@/components/plugin-page"; + +const PLUGINS = { + tasmota: { + versions: [ + { version: "0.1.2", date: new Date("2024-05-13") }, + { version: "0.1.1", date: new Date("2024-05-09") }, + { version: "0.1.0", date: new Date("2024-05-04") }, + ], + configSchema: + '{"properties":{"config":{"properties":{"ip":{"description":"The ip address of the device to connect to.","items":[{"format":"uint8","minimum":0,"type":"integer"},{"format":"uint8","minimum":0,"type":"integer"},{"format":"uint8","minimum":0,"type":"integer"},{"format":"uint8","minimum":0,"type":"integer"}],"maxItems":4,"minItems":4,"type":"array"}},"required":["ip"],"type":"object"},"plugin":{"const":"tasmota@0.1.1"}},"required":["plugin","config"],"type":"object"}', + source: "https://github.com/arlyon/litehouse", + capabilities: ["http-client"], + homepage: "https://github.com/arlyon/litehouse", + description: "Control tasmota-based smart devices.", + size: 60345, + }, + weather: { + versions: [ + { version: "0.1.1", date: new Date("2024-05-09") }, + { version: "0.1.0", date: new Date("2024-05-04") }, + ], + configSchema: + '{"properties":{"config":{"properties":{"lat":{"description":"The latitude to fetch the weather for.","format":"double","type":"number"},"lon":{"description":"The longitude to fetch the weather for.","format":"double","type":"number"}},"required":["lat","lon"],"type":"object"},"plugin":{"const":"weather@0.1.1"}},"required":["plugin","config"],"type":"object"}', + source: "https://github.com/arlyon/litehouse", + capabilities: ["http-client"], + homepage: "https://github.com/arlyon/litehouse", + description: + "Fetch weather data from the internet using api.open-meteo.com.", + size: 60345, + }, +}; + +/** + * Returns versions in descending order + * + * @param title The title of the plugin + * @param version The version of the plugin. If not provided, the latest version is returned + */ +export const getPluginData = async ( + title: string, + version?: string, +): Promise => { + const plugin = PLUGINS[title]; + return { + version: version || plugin.versions[0].version, + title, + ...plugin, + }; +}; + +export const getPlugins = async (): Promise< + { + title: string; + versions: { version: string; date: Date }[]; + description?: string; + version: { version: string; date: Date }; + }[] +> => { + return Object.entries(PLUGINS).map(([title, data]) => ({ + title, + versions: data.versions, + description: data.description, + version: data.versions[0], + })); +}; diff --git a/site/package.json b/site/package.json index cb80693..06d3a1d 100644 --- a/site/package.json +++ b/site/package.json @@ -16,18 +16,21 @@ "@radix-ui/react-slot": "^1.0.2", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", + "ajv": "^8.13.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", "fumadocs-core": "^11.0.5", "fumadocs-mdx": "^8.2.14", "fumadocs-ui": "^11.0.5", + "input-otp": "^1.2.4", "lucide-react": "^0.378.0", "next": "^14.3.0-canary.64", "next-axiom": "^1.2.0", "next-themes": "^0.3.0", "react": "^19.0.0-rc.0", "react-dom": "^19.0.0-rc.0", + "react-hook-form": "^7.51.4", "remark-code-import": "^1.2.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7",