diff --git a/.env b/.env deleted file mode 100644 index 53f9a19..0000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -REACT_APP_PUBLIC_URL="https://beadi.onrender.com/" -REACT_APP_BETA_PUBLIC_URL="https://stagingbeadi.onrender.com/" -# Will be overwritten in production -REACT_APP_BRANCH="staging" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d29575..434a010 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +node_modules /.pnp .pnp.js @@ -9,7 +9,7 @@ /coverage # production -/build +/app/build # misc .DS_Store @@ -21,3 +21,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +. \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e7740ff --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 140 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cf205bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "auto-close-tag.disableOnLanguage": [ + "php", + "javascript", + "typescript", + "plaintext" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a11a49..52530f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Version 0.4 + +Oh wow! This is a big one! + +Large parts of the underlying engine were rewritten to make way for a new plugin-based architecture. + +TODO The rest of what changed. + # Version 0.3 This update brings the first draft of a mobile interface, so you aren't bound to your PC anymore and can take the fun anywhere. You can create your flows using the web-app on a PC as usual and then transfer them to your mobile device using an exported json file or by simply pressing a button to Export it to [litterbox.catbox.moe](https://litterbox.catbox.moe). diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..ea2b80f --- /dev/null +++ b/app/.env @@ -0,0 +1,6 @@ +VITE_APP_REMOTE_SERVER_URL="wss://beadi-serve.onrender.com:6969/" +# REACT_APP_REMOTE_SERVER_URL="wss://stagingbeadi.onrender.com:6969/" +VITE_APP_PUBLIC_URL="https://beadi.onrender.com/" +VITE_APP_BETA_PUBLIC_URL="https://stagingbeadi.onrender.com/" +# Will be overwritten in production +VITE_APP_BRANCH="staging" \ No newline at end of file diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/app/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..1ebe379 --- /dev/null +++ b/app/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/app/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..2692b6b --- /dev/null +++ b/app/package.json @@ -0,0 +1,40 @@ +{ + "name": "beadi", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "tsc": "tsc --noEmit --watch", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@beadi/components": "workspace:^", + "@beadi/engine": "workspace:^", + "@beadi/plugin-device-sensors": "workspace:^", + "@beadi/plugin-intiface": "workspace:^", + "@beadi/plugin-media": "workspace:^", + "@beadi/plugin-remote": "workspace:^", + "clsx": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "=4.7.1", + "react-markdown": "=8.0.4", + "react-router-dom": "=6.14.1" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@types/lodash": "^4.14.191", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "eslint": "^8.45.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +} diff --git a/app/postcss.config.js b/app/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/app/public/vite.svg b/app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/App.tsx b/app/src/App.tsx new file mode 100644 index 0000000..a99d467 --- /dev/null +++ b/app/src/App.tsx @@ -0,0 +1,64 @@ +import { BeadiContext, BeadiContextProvider } from "@beadi/engine"; +import { intifacePlugin } from "@beadi/plugin-intiface"; +import { FunctionComponent } from "react"; +import { makeRemotePlugin } from "../../plugins/remote/src"; +import { beadiAppPlugin } from "./beadiAppPlugin"; +import { HomePage } from "./home/HomePage"; +import { ChangelogPage } from "./pages/Changelog"; +import { GuidePage } from "./pages/Guide"; +import { Introduction } from "./pages/Introduction"; +import { Privacy } from "./pages/Privacy"; +import { EditorPage } from "./editor/EditorPage"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; + +export const EDITOR_ROOT_URL = "/edit"; + +const context = new BeadiContext({ + rootUrl: EDITOR_ROOT_URL, + plugins: [ + beadiAppPlugin, + makeRemotePlugin({ + remoteServerUrl: import.meta.env.VITE_APP_REMOTE_SERVER_URL, + }), + intifacePlugin, + ], +}); +const routes = [ + { + path: "/", + element: , + children: [ + { + path: "/", + element: , + }, + { + path: "guide", + element: , + }, + { + path: "changelog", + element: , + }, + { + path: "privacy", + element: , + }, + ], + }, + { + path: EDITOR_ROOT_URL, + element: , + children: context.createRoutes(), + }, +]; + +const router = createBrowserRouter(routes); + +export const App: FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/app/src/assets/HeroNodes.png b/app/src/assets/HeroNodes.png new file mode 100644 index 0000000..89c1f27 Binary files /dev/null and b/app/src/assets/HeroNodes.png differ diff --git a/app/src/assets/react.svg b/app/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/beadiAppPlugin/WelcomeNode.tsx b/app/src/beadiAppPlugin/WelcomeNode.tsx new file mode 100644 index 0000000..a4dc097 --- /dev/null +++ b/app/src/beadiAppPlugin/WelcomeNode.tsx @@ -0,0 +1,70 @@ +import { FunctionComponent, useCallback } from "react"; +import { ReactMarkdown } from "react-markdown/lib/react-markdown"; +import { Button, NodeShell } from "@beadi/components"; +import { BeadiFileData, UnknownBeadiNodeProps, useFileStore } from "@beadi/engine"; + +// import SimpleWaveExample from "EXAMPLES/SimpleNodes.json"; + +// const examples = [ +// { +// name: "Simple Wave", +// data: SimpleWaveExample, +// }, +// { +// name: "Button Control", +// data: "", +// }, +// ]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const examples: Record = {}; + +//TODO Fix examples +// function importAll(r: any) { +// r.keys().forEach((key: any) => (examples[key] = r(key))); +// } +// importAll((require as any).context("../../../examples", false, /\.json$/)); + +export const ExampleList: FunctionComponent = () => { + const overwriteStore = useFileStore((it) => it.overwrite); + const loadExample = useCallback( + (data: BeadiFileData) => { + overwriteStore(data); + }, + [overwriteStore] + ); + + return ( +
    + {Object.entries(examples).map(([key, value]) => ( +
  • + +
  • + ))} +
+ ); +}; + +export const WelcomeNodeContent: FunctionComponent = () => { + //TODO Reintroduce Changelog + // const changelog = BEADI_CHANGELOG; + const changelog = "TODO CHANGELOG"; + + return ( +
+
+ +
+
+ ); +}; + +export const WelcomeNode: FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/app/src/beadiAppPlugin/index.ts b/app/src/beadiAppPlugin/index.ts new file mode 100644 index 0000000..406a39d --- /dev/null +++ b/app/src/beadiAppPlugin/index.ts @@ -0,0 +1,9 @@ +import { plugin } from "@beadi/engine"; +import { WelcomeNode } from "./WelcomeNode"; + +export const beadiAppPlugin = plugin({ + id: "beadiApp", + extraNodeRenderers: { + welcome: WelcomeNode, + }, +}); diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx new file mode 100644 index 0000000..1b29fa8 --- /dev/null +++ b/app/src/components/Footer.tsx @@ -0,0 +1,36 @@ +import { VBar } from "@beadi/components"; +import { FunctionComponent } from "react"; +import { BsGithub, BsMastodon } from "react-icons/bs"; +import { Link } from "react-router-dom"; + +export const Footer: FunctionComponent = () => { + return ( +
+
+
+ Beadi + + Made with 💜 by That Bat Luna +
+
+ + + + + + +
+
+
+
+ Imprint/Impressum + + Privacy Policy + + Cookies + +
© 2023 Mona Mayrhofer - Linz, Austria
+
+
+ ); +}; diff --git a/app/src/components/NavBar.tsx b/app/src/components/NavBar.tsx new file mode 100644 index 0000000..68b09fa --- /dev/null +++ b/app/src/components/NavBar.tsx @@ -0,0 +1,48 @@ +import clsx from "clsx"; +import { FunctionComponent, ReactNode } from "react"; +import { MdChevronRight } from "react-icons/md"; +import { NavLink, To } from "react-router-dom"; +import { EDITOR_ROOT_URL } from "../App"; + +type NavBarItemProps = { + to: To; + children?: ReactNode; +}; +const NavBarItem: FunctionComponent = ({ to, children }) => { + return ( +
  • + + clsx("px-12 pb-4 pt-5 text-lg font-bold border-b-purple-500 hover:text-white flex flex-row items-center", { + "border-b-8": isActive, + "border-b": !isActive, + "text-purple-100": !isActive, + }) + } + > + {children} + +
  • + ); +}; + +export const NavBar: FunctionComponent = () => { + return ( + + ); +}; diff --git a/app/src/editor/EditorPage.tsx b/app/src/editor/EditorPage.tsx new file mode 100644 index 0000000..9f4cac1 --- /dev/null +++ b/app/src/editor/EditorPage.tsx @@ -0,0 +1,18 @@ +import { FunctionComponent, useState } from "react"; +import { SaveSelector } from "./SaveSelector"; +import { SaveEditor } from "./SaveEditor"; + +export const EditorPage: FunctionComponent = () => { + const [saveFile, setSaveFile] = useState(sessionStorage.getItem("open_file")); + + const openSave = (save: string) => { + sessionStorage.setItem("open_file", save); + setSaveFile(save); + }; + + if (saveFile === null) { + return setSaveFile(s)} onCreateNew={() => openSave(`TODO_NEW_FILENAME${new Date().getTime()}`)} />; + } else { + return ; + } +}; diff --git a/app/src/editor/SaveEditor.tsx b/app/src/editor/SaveEditor.tsx new file mode 100644 index 0000000..e2cfc54 --- /dev/null +++ b/app/src/editor/SaveEditor.tsx @@ -0,0 +1,58 @@ +import { useBeadi, BeadiInstance, BeadiEditor } from "@beadi/engine"; +import { FunctionComponent, useMemo } from "react"; + +function loadSaveFile(name: string): Record { + const item = localStorage.getItem("beadi_saves"); + if (item == null) { + return {}; + } + const parsed = JSON.parse(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return parsed?.items?.find((it: any) => it.name === name)?.content ?? {}; +} + +//TODO Ok yea this is really stupid, we are reading and reencoding ALL existing files ... +//And this method is called on EVERY CHANGE which is OFTEN +function writeSaveFile(name: string, data: Record) { + const item = localStorage.getItem("beadi_saves") ?? `{"items": []}`; + const parsed = JSON.parse(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const file = parsed.items.find((it: any) => it.name === name); + if (file != null) { + file.content = data; + } else { + parsed.items.push({ + name: name, + content: data, + }); + } + + // console.log("Saving all: ", parsed); + + localStorage.setItem("beadi_saves", JSON.stringify(parsed)); +} + +type SaveEditorProps = { + saveFile: string; +}; +export const SaveEditor: FunctionComponent = ({ saveFile }) => { + const beadiContext = useBeadi(); + const instance = useMemo(() => { + const file = loadSaveFile(saveFile); + if (file === null) { + return null; + } + return new BeadiInstance({ + beadiContext, + initialData: file, + writePersistentData: (data) => { + writeSaveFile(saveFile, data); + }, + }); + }, [beadiContext, saveFile]); + + if (instance !== null) { + return ; + } + return

    TODO Error when loading file

    ; +}; diff --git a/app/src/editor/SaveSelector.tsx b/app/src/editor/SaveSelector.tsx new file mode 100644 index 0000000..e859931 --- /dev/null +++ b/app/src/editor/SaveSelector.tsx @@ -0,0 +1,43 @@ +import { Button, Typo } from "@beadi/components"; +import { FunctionComponent, useMemo } from "react"; + +//TODO Saves should really be in IndexedDB and not in localstorage + +type BeadiSaveFile = { + name: string; + content: string; +}; +type BeadiSaves = { + items: BeadiSaveFile[]; +}; + +function load(): BeadiSaves { + const item = localStorage.getItem("beadi_saves"); + if (item == null) { + return { items: [] }; + } + const parsed = JSON.parse(item); + return parsed; +} + +export type SaveSelectorProps = { + onSaveSelected: (fileName: string) => void; + onCreateNew: () => void; +}; +export const SaveSelector: FunctionComponent = ({ onSaveSelected, onCreateNew }) => { + const saves = useMemo(() => load(), []); + + return ( +
    + Open File +
      + {saves.items.map((it) => ( +
    • + +
    • + ))} +
    + +
    + ); +}; diff --git a/app/src/home/HomePage.tsx b/app/src/home/HomePage.tsx new file mode 100644 index 0000000..1fb5fdf --- /dev/null +++ b/app/src/home/HomePage.tsx @@ -0,0 +1,75 @@ +import { FunctionComponent, useMemo } from "react"; +import { Outlet } from "react-router-dom"; +import { Logo } from "@beadi/components"; +import { BeadiContext, BeadiInstance, BeadiContextProvider, Viewport, ViewportFlowProvider, BeadiInstanceProvider } from "@beadi/engine"; +import { NavBar } from "../components/NavBar"; +import { Footer } from "../components/Footer"; +import { previewSave } from "./beadiPreviewSave"; + +export const PreviewBeadi: FunctionComponent = () => { + const [context, instance] = useMemo(() => { + const context = new BeadiContext({ + rootUrl: "", + plugins: [], + }); + const instance = new BeadiInstance({ + beadiContext: context, + initialData: previewSave, + writePersistentData: () => {}, + }); + return [context, instance]; + }, []); + + return ( + + + + + + + + ); +}; + +export const Hero: FunctionComponent = () => { + return ( +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +

    Next Generation Customization for Remote Sex Toy Control

    + {/* + Start Creating + + */} +
    +
    +
    + ); +}; + +export const HomePage: FunctionComponent = () => { + return ( +
    + +
    + +
    + +
    +
    +
    +
    + ); +}; diff --git a/app/src/home/beadiPreviewSave.ts b/app/src/home/beadiPreviewSave.ts new file mode 100644 index 0000000..f925aaa --- /dev/null +++ b/app/src/home/beadiPreviewSave.ts @@ -0,0 +1,102 @@ +import { BeadiEdge, BeadiFileData, UnknownBeadiNode } from "@beadi/engine"; +import _ from "lodash"; + +export const previewSave = { + nodes: { + nodes: _.keyBy( + [ + { + id: "waveA", + position: { + x: 100, + y: 100, + }, + type: "wave", + data: { + displaySettings: {}, + inputHandles: { + amplitude: { + value: 1.0, + }, + frequency: { + value: 0.2, + }, + phase: { + value: 0.0, + }, + }, + outputHandles: {}, + settings: {}, + name: "Wave", + }, + } satisfies UnknownBeadiNode, + { + id: "waveB", + position: { + x: 120, + y: 500, + }, + type: "wave", + data: { + displaySettings: {}, + inputHandles: { + amplitude: { + value: 1.0, + }, + frequency: { + value: 0.2, + }, + phase: { + value: 0.0, + }, + }, + outputHandles: {}, + settings: {}, + name: "Wave", + }, + } satisfies UnknownBeadiNode, + { + id: "mix", + position: { + x: 400, + y: 250, + }, + type: "math", + data: { + displaySettings: {}, + outputHandles: { + result: { + preview: true, + }, + }, + inputHandles: {}, + settings: { + operation: "mix", + }, + }, + } satisfies UnknownBeadiNode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any, + (it) => it.id + ), + edges: _.keyBy( + [ + { + id: "waveA_mix", + source: "waveA", + sourceHandle: "value", + target: "mix", + targetHandle: "a", + }, + { + id: "waveB_mix", + source: "waveB", + sourceHandle: "value", + target: "mix", + targetHandle: "b", + }, + ] satisfies BeadiEdge[], + (it) => it.id + ), + } satisfies BeadiFileData, +}; diff --git a/app/src/index.css b/app/src/index.css new file mode 100644 index 0000000..7e169da --- /dev/null +++ b/app/src/index.css @@ -0,0 +1,20 @@ +html, +body, +#root { + height: 100vh; + width: 100vw; +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + .markdown h1 { + @apply text-xl mt-2 mb-1; + } + + .hero-tilt { + transform: perspective(30cm) translateY(-10%) translateX(-10%) translateZ(500px) rotateX(40deg) rotateZ(25deg); + } +} diff --git a/app/src/main.tsx b/app/src/main.tsx new file mode 100644 index 0000000..265f344 --- /dev/null +++ b/app/src/main.tsx @@ -0,0 +1,14 @@ +import "./index.css"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); + +// startBeadi({ +// rootElement: "root", +// }); diff --git a/app/src/pages/Changelog.tsx b/app/src/pages/Changelog.tsx new file mode 100644 index 0000000..fbf03b8 --- /dev/null +++ b/app/src/pages/Changelog.tsx @@ -0,0 +1,18 @@ +import { Typo } from "@beadi/components"; +import { FunctionComponent } from "react"; +import ReactMarkdown from "react-markdown"; + +export const ChangelogPage: FunctionComponent = () => { + const changelog = BEADI_CHANGELOG; + return ( +
    + + Changelog + + +
    + +
    +
    + ); +}; diff --git a/app/src/pages/Guide.tsx b/app/src/pages/Guide.tsx new file mode 100644 index 0000000..238430c --- /dev/null +++ b/app/src/pages/Guide.tsx @@ -0,0 +1,27 @@ +import { Typo, TypoLink } from "@beadi/components"; +import { FunctionComponent } from "react"; +import { ExampleList } from "../beadiAppPlugin/WelcomeNode"; +import { EDITOR_ROOT_URL } from "../App"; + +export const GuidePage: FunctionComponent = () => { + return ( +
    + + Getting Started + +

    + You can get started by taking a look at some of the examples below or just opening the{" "} + Editor and dragging in some nodes from the left. If you want to connect to the outside + world, click on one of the icons on the right to either add an Intiface connection or enable Remote Control. +

    +

    + Drag in Input nodes to access sensors or remote control. Drag in Output nodes to write values to toys via + Intiface. +

    + + Examples + + +
    + ); +}; diff --git a/app/src/pages/Introduction.tsx b/app/src/pages/Introduction.tsx new file mode 100644 index 0000000..9d1c133 --- /dev/null +++ b/app/src/pages/Introduction.tsx @@ -0,0 +1,53 @@ +import { Card, Typo, TypoLink } from "@beadi/components"; +import { FunctionComponent } from "react"; +// import { ExampleList } from "../beadiAppPlugin/WelcomeNode"; +// import { EDITOR_ROOT_URL } from "@beadi/engine"; + +export const Introduction: FunctionComponent = () => { + return ( +
    + + Hello! + +

    + I am Luna the bat, and this is Beadi, a platform for visual, node-based, programming for{" "} + + Buttplug.io + {" "} + compatible Sextoys. +

    + + Features + +
    + + Extensive Collection of Nodes +

    A collection of versatile Nodes enables you to create everything from simple waves up to complex event driven control flows

    +
    + + Customizable Interfaces +

    Create Sliders, Buttons and switches to act as inputs for your graph

    +
    + + Remote Control +

    Let other people remotely control your interfaces or even connect multiple graphs together to create a network of fun.

    +
    + + Adapters for I/O +

    + Access the sensors of your mobile phone or connect to Intiface to make your toys + vibe. +

    +
    +
    +
    +

    And many more to come!

    +
    + You can visit my Github to take a look at current developments or + suggest new features via Issues or on{" "} + Social Media +
    +
    +
    + ); +}; diff --git a/app/src/pages/Privacy.tsx b/app/src/pages/Privacy.tsx new file mode 100644 index 0000000..6b2f43f --- /dev/null +++ b/app/src/pages/Privacy.tsx @@ -0,0 +1,219 @@ +import { Typo, TypoLink } from "@beadi/components"; +import { FunctionComponent } from "react"; + +export const Privacy: FunctionComponent = ({}) => { + return ( +
    + + Privacy Policy for Beadi + + +

    + One of my ("Mona Mayrhofer", "we", "our", "us", "I", "me", or "mine") main priorities with Beadi is to retain the privacy and + discretion of the visitors and I strive to minimize it to the absolute necessary minimum. This Privacy Policy will explain how we + use the personal data we collect from you when you use the website, at + {import.meta.env.VITE_APP_PUBLIC_URL} or + {import.meta.env.VITE_BETA_APP_PUBLIC_URL}. +

    + +

    If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us at:

    + + + + What data do we collect? + +
      +
    • + + Information you provide us voluntarily + + We collect any data that you decide to provide us directly, by filling out forms or inputs on the website, sending e-mails or + otherwisely communicate with us. +
    • +
    • + + Information you provide us by using the service + + We collect the data you provide when using the service, like remote-control activity, chat, cloud-saves etc. +
    • +
    • + + Information about the usage of the website + + We collect regular usage information about the usage of the website and the editor, like which pages are visited or which features + are used within the editor. +
    • +
    + + + How do we collect your data? + + +

    You directly provide most of the data we collect. We collect data and process data when you

    +
      +
    • Voluntarily send messages, e-mails or communicate with us in any other way
    • +
    • + Use or view the website, either via log-files or via automated reports about which pages are viewed or what features of the editor + are used. +
    • +
    • Use features of our service that requires data to be sent to our servers, like remote-control, chat or cloud-saves
    • +
    + + + How will we use your data? + +

    We collect your data so that we can:

    +
      +
    • Provide you with functionality of our service that requires data to be sent to our servers.
    • +
    • Get a rough estimate about how many users are using our services and what features are most important to them
    • +
    • Get information about possible errors or crashes, so that we can fix them and improve our services.
    • +
    + + + How do we store your data? + +

    + We securely store your data in Linz, Austria and make sure to keep them to a bare minimum. We are aware of the possible risks that + come with storing user data and are committed to keep it safe. +

    + +

    Data associated with your user account will be stored until you delete you account.

    + +

    Data collected for analytical/statistical purposes will be anonymized as soon as we get it.

    + + + How do we share your information? + +
      +
    • + + The public and other users of the service + +

      + Content that you publish to public areas of the service (chat, remote-control, etc.) will be shared with other users of the + service. +

      +
    • +
    • + + Service Providers and Processors + +

      + In order to provide services to you we rely on others to provide us services. Certain Service providers (also known as + "processors") critical services, such as hosting (storing and delivering the webpage). We authorize such service providers to + use or disclose the Personal Information shared with them +

      +
    • +
    + + + What are your data protection rights? + +

    We would like to make sure you are fully aware of all your data protection rights. Every user is entitled to the following:

    +
      +
    • + + The right to access + + You have the right to request us for copies of the data we collected from you. If the extent of your request exceeds reasonable + bounds, we may charge you a small fee for this service. +
    • +
    • + + The right to rectification + + You have the right to request that we correct any information you believe is inaccurate. You also have the right to request us to + complete information you believe is incomplete. +
    • +
    • + + The right to erasure + + You have the right to request that we erase your personal data, under certain conditions. +
    • +
    • + + The right to restrict processing + + You have the right to request us to restrict the processing of your personal data, under certain conditions. +
    • +
    • + + The right to object to processing + + You have the right to object to our processing of your personal data, under certain conditions. +
    • +
    • + + The right to data portability + + You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under + certain conditions. +
    • +
    + +

    + If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us at + our email: lunathebat@proton.me +

    + + + What are cookies/localstorage/IndexedDB? + +

    + Cookies/localstorage/IndexedDB and similar technologies are a way, provided by your Browser, to store small amounts of data on your + PC. They are primarily used by us to enhance the function of our service (e.g enabling us to store settings/saves locally instead of + having to transfer them to our servers). They can also be used to track visitors and user behaviour. +

    +

    For further information, visit allaboutcookies.org

    + + + How do we use cookies/localstorage/IndexedDB? + +

    We use cookies/localstorage/IndexedDB exclusively to

    +
      +
    • Keep you signed in if you have a user account
    • +
    • Store settings and saves of the editor directly on your pc
    • +
    + + + What types of cookies do we use? + +

    There are a number of different types of cookies, however our website ues:

    +
      +
    • + Functionality - We use these cookies so that we can store your saves, settings and recognizing you when you return to our website. +
    • +
    • We do not use any cookies for advertising
    • +
    + + + How to manage cookies + +

    + You can set your browser not to accept cookies, and the website mentioned above will tell you how to remove cookies. However as most + of our functionality relies on cookies, the quality of our services may degrade. +

    + + + Privacy Policies of other websites + +

    + The Beadi website contains links to other websites. This privacy policy only applies to our websites, so if you click a link to + another website, you should read their privacy policy. +

    + + + Changes to our privacy policy + +

    + We keep our privacy policy under regular review and place any updates on this website. This privacy policy was last updated on the + 16 August 2023 +

    +
    + ); +}; diff --git a/app/src/vite-env.d.ts b/app/src/vite-env.d.ts new file mode 100644 index 0000000..71ee9f9 --- /dev/null +++ b/app/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const BEADI_CHANGELOG: string; diff --git a/app/tailwind.config.js b/app/tailwind.config.js new file mode 100644 index 0000000..2343980 --- /dev/null +++ b/app/tailwind.config.js @@ -0,0 +1,69 @@ +/** @type {import('tailwindcss').Config} */ +const purple = { + 100: "#fcacfa", + 200: "#f38bef", + 300: "#ea6de5", + 400: "#e251d9", + 500: "#d938cc", + 600: "#c217b4", + 700: "#a211a2", + 800: "#7b0e83", + 900: "#580d63", + 1000: "#390b43", + 1100: "#1d0724" + }; +const fuchsia = { + 100: "#fcacc4", + 200: "#fb90b1", + 300: "#f9749f", + 400: "#f8598e", + 500: "#f63f7e", + 600: "#e71b63", + 700: "#c0144f", + 800: "#99103e", + 900: "#720e2e", + 1000:"#4b0c1f", + 1100:"#240710" +}; + + +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./node_modules/@beadi/*/src/**/*.{js,jsx,ts,tsx}" +], + theme: { + extend: { + colors: { + primary: { + 400: "#4E4A5B", + 500: "#443F50", + 600: "#393546", + 700: "#2F2A3B", + 800: "#252030", + 900: "#1B1726", + 1000: "#120F1B", + 1100: "#0A0810", + }, + purple: purple, + accent: purple, + fuchsia: fuchsia, + secondary: fuchsia, + }, + boxShadow: { + "error": "0 0 10px 3px red", + "glow": "0 0 30px 0 white" + }, + dropShadow: { + "primary": [ + "0 0 5px black", + "0 0 10px white", + `0 0 20px ${purple[300]}`, + `0 0 50px ${purple[800]}` + ] + } + }, + + }, + plugins: [], +}; diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..924da70 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src/**/*", "types/**/*"] +} diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..e16f0c2 --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import fs from "fs"; + +// https://vitejs.dev/config/ +export default defineConfig({ + define: { + BEADI_CHANGELOG: JSON.stringify(fs.readFileSync("../CHANGELOG.md").toString()), + }, + plugins: [react()], +}); diff --git a/core/components/.eslintrc.cjs b/core/components/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/core/components/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/core/components/.gitignore b/core/components/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/core/components/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/core/components/README.md b/core/components/README.md new file mode 100644 index 0000000..1ebe379 --- /dev/null +++ b/core/components/README.md @@ -0,0 +1,27 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/core/components/package.json b/core/components/package.json new file mode 100644 index 0000000..af396e1 --- /dev/null +++ b/core/components/package.json @@ -0,0 +1,34 @@ +{ + "name": "@beadi/components", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": ["./src/index.ts"], + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "clsx": "=1.2.1", + "lodash": "=4.17.21", + "react-icons": "=4.7.1", + "react-router-dom": "=6.14.1" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@types/lodash": "^4.14.191", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react": "^4.0.3", + "eslint": "^8.45.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +} diff --git a/core/components/src/card/Card.tsx b/core/components/src/card/Card.tsx new file mode 100644 index 0000000..2c22465 --- /dev/null +++ b/core/components/src/card/Card.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent, ReactNode } from "react"; +import clsx from "clsx"; + +export type CardProps = { + header?: ReactNode | FunctionComponent>; + children?: ReactNode | FunctionComponent>; + forceExpanded?: boolean | undefined; + startExpanded?: boolean; +}; +export const Card: FunctionComponent = ({ header: Header, children: Children }) => { + return ( +
    + {Header !== undefined && ( +
    + {typeof Header === "function" ?
    : Header} +
    + )} +
    + {typeof Children === "function" ? : Children} +
    +
    + ); +}; diff --git a/core/components/src/card/CollapsibleCard.tsx b/core/components/src/card/CollapsibleCard.tsx new file mode 100644 index 0000000..7aea40a --- /dev/null +++ b/core/components/src/card/CollapsibleCard.tsx @@ -0,0 +1,35 @@ +import { FunctionComponent, ReactNode, useState } from "react"; +import clsx from "clsx"; +import { Button } from "../input/Button"; +import { MdExpandLess, MdExpandMore } from "react-icons/md"; + +export type CollapsibleCardProps = { + header?: ReactNode | FunctionComponent>; + children?: ReactNode | FunctionComponent>; + forceExpanded?: boolean | undefined; + startExpanded?: boolean; +}; +export const CollapsibleCard: FunctionComponent = ({ + header: Header, + children: Children, + startExpanded, + forceExpanded, +}) => { + const [expanded, setExpanded] = useState(startExpanded ?? true); + + const actuallyExpanded = expanded || forceExpanded; + + return ( +
    +
    + {typeof Header === "function" ?
    : Header} + + {!expanded && forceExpanded === undefined && } + {expanded && forceExpanded === undefined && } +
    + {actuallyExpanded && ( +
    {typeof Children === "function" ? : Children}
    + )} +
    + ); +}; diff --git a/core/components/src/graph/Graph.tsx b/core/components/src/graph/Graph.tsx new file mode 100644 index 0000000..676a55b --- /dev/null +++ b/core/components/src/graph/Graph.tsx @@ -0,0 +1,76 @@ +import { FunctionComponent } from "react"; + +export type GraphProps = { + history: (number | undefined)[]; + index: number; + fixed: boolean; + height: number; + minHeight: number; +}; +export const Graph: FunctionComponent = ({ history, index, fixed, minHeight, height }) => { + let min = 0; + let max = 0; + + const tmpHistory = history.map((it) => { + const val = it || 0.0; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + return val; + }); + + if (fixed) { + min = 0.0; + max = 1.0; + } else { + const mean = (max + min) / 2; + if (max - min < minHeight) { + max = mean + minHeight / 2.0; + min = mean - minHeight / 2.0; + } + } + + const offset = -(min + max) / 2; + const scale = height / 2 / Math.max(0.001, Math.abs(max + offset)); + + const correctedHistory = tmpHistory.map((it) => (it + offset) * scale); + + const parts = new Array(history.length) + .fill("") + .map((_, i) => { + const realIndex = (i + index) % history.length; + const point = correctedHistory[realIndex] || 0; + + const coords = -point; + return `L${i} ${coords.toFixed(4)}`; + }) + .join(" "); + + const start = correctedHistory[index]; + + const zero = (0 + offset) * scale; + const maxLine = (max + offset) * scale; + const minLine = (min + offset) * scale; + + return ( + + + + + + {" "} + 0 + + + {max.toFixed(2)} + + + {min.toFixed(2)} + + + + ); +}; diff --git a/core/components/src/index.ts b/core/components/src/index.ts new file mode 100644 index 0000000..2bd00bc --- /dev/null +++ b/core/components/src/index.ts @@ -0,0 +1,35 @@ +export { CollapsibleCard } from "./card/CollapsibleCard"; +export type { CollapsibleCardProps } from "./card/CollapsibleCard"; +export { Card } from "./card/Card"; +export type { CardProps } from "./card/Card"; + +export { Button } from "./input/Button"; +export type { ButtonProps } from "./input/Button"; + +export { Checkbox } from "./input/Checkbox"; +export type { CheckboxProps } from "./input/Checkbox"; + +export { NumberInput } from "./input/NumberInput"; +export type { NumberInputProps } from "./input/NumberInput"; + +export { Select } from "./input/Select"; +export type { SelectProps } from "./input/Select"; + +export { TextInput } from "./input/TextInput"; +export type { TextInputProps } from "./input/TextInput"; + +export { Graph } from "./graph/Graph"; +export type { GraphProps } from "./graph/Graph"; +export { Typo } from "./typo/Typo"; +export type { TypoProps } from "./typo/Typo"; +export { TypoLink } from "./typo/TypoLink"; +export type { TypoLinkProps } from "./typo/TypoLink"; + +export { NodeShell } from "./node/NodeShell"; +export type { NodeShellProps } from "./node/NodeShell"; + +export { Logo } from "./logo/Logo"; +export type { LogoProps } from "./logo/Logo"; + +export { VBar } from "./typo/VBar"; +export type { VBarProps } from "./typo/VBar"; diff --git a/core/components/src/input/Button.tsx b/core/components/src/input/Button.tsx new file mode 100644 index 0000000..046ab52 --- /dev/null +++ b/core/components/src/input/Button.tsx @@ -0,0 +1,45 @@ +import clsx from "clsx"; +import { ButtonHTMLAttributes, DetailedHTMLProps, FunctionComponent, PropsWithChildren, ReactNode } from "react"; + +type ButtonElementProps = ButtonHTMLAttributes; +export type ButtonProps = { + variant?: "normal" | "big"; + children?: ReactNode; + disabledNotice?: string; + icon?: ReactNode; + endIcon?: ReactNode; + Element?: React.ComponentType>; +} & DetailedHTMLProps, HTMLButtonElement>; + +export const Button: FunctionComponent = ({ + children, + disabled, + icon, + endIcon, + variant = "normal", + // disabledNotice, + className, + Element = "button", + ...buttonProps +}) => { + const isIcon = icon !== undefined || endIcon !== undefined; + const big = variant === "big"; + return ( + + {icon} + {children &&
    {children}
    } + {endIcon} +
    + ); +}; diff --git a/core/components/src/input/Checkbox.tsx b/core/components/src/input/Checkbox.tsx new file mode 100644 index 0000000..b26530e --- /dev/null +++ b/core/components/src/input/Checkbox.tsx @@ -0,0 +1,17 @@ +import { FunctionComponent } from "react"; + +export type CheckboxProps = { + label: string; + checked: boolean; + onChange?: (checked: boolean) => void; +}; +export const Checkbox: FunctionComponent = ({ label, checked, onChange }) => { + return ( +
    + +
    + ); +}; diff --git a/src/components/input/NumberInput.tsx b/core/components/src/input/NumberInput.tsx similarity index 75% rename from src/components/input/NumberInput.tsx rename to core/components/src/input/NumberInput.tsx index e96fc2c..bfc4c28 100644 --- a/src/components/input/NumberInput.tsx +++ b/core/components/src/input/NumberInput.tsx @@ -7,7 +7,6 @@ import { PointerEventHandler, ReactNode, useCallback, - useEffect, useMemo, useRef, useState, @@ -15,9 +14,10 @@ import { export type ChangeEvent = { value: number; + intermediate: boolean; }; -type NumberInputProps = { +export type NumberInputProps = { id: string; name: string; onChange?: (e: ChangeEvent) => void; @@ -27,25 +27,28 @@ type NumberInputProps = { max?: number; }; -const NumberInput: FunctionComponent = ({ - id, - name, - onChange, - value: officialValue, - label, - min, - max, -}) => { - const [value, setValue] = useState(officialValue || 0); +export const NumberInput: FunctionComponent = ({ id, name, onChange, value: officialValue, label, min, max }) => { + const [value, setValueState] = useState(officialValue || 0); const [sliding, setSliding] = useState(false); const [startX, setStartX] = useState(null); const [textEdit, setTextEdit] = useState(false); - useEffect(() => { - if (officialValue !== undefined) { - setValue(officialValue); - } - }, [setValue, officialValue]); + const setValue = useCallback( + (v: number) => { + setValueState(v); + onChange?.({ + intermediate: true, + value: v, + }); + }, + [onChange, setValueState] + ); + + // useEffect(() => { + // if (officialValue !== undefined) { + // setValue(officialValue); + // } + // }, [setValue, officialValue]); const inputElement = useRef(null); @@ -57,7 +60,7 @@ const NumberInput: FunctionComponent = ({ } setValue(value); - onChange?.({ value: value }); + onChange?.({ value: value, intermediate: false }); }, [setValue, onChange] ); @@ -79,7 +82,7 @@ const NumberInput: FunctionComponent = ({ if (!textEdit) { (e.target as HTMLElement).releasePointerCapture(e.pointerId); setSliding(false); - onChange?.({ value: value }); + onChange?.({ value: value, intermediate: false }); if (startX === e.pageX) { setTextEdit(true); } @@ -102,8 +105,8 @@ const NumberInput: FunctionComponent = ({ e.stopPropagation(); - setValue((old) => { - let num = old + e.movementX * multiplier; + { + let num = value + e.movementX * multiplier; if (min !== undefined) { num = Math.max(num, min); @@ -112,14 +115,14 @@ const NumberInput: FunctionComponent = ({ num = Math.min(num, max); } - return Number(num.toFixed(3)); - }); + setValue(Number(num.toFixed(3))); + } } }, - [setValue, sliding, textEdit, min, max] + [setValue, sliding, textEdit, value, min, max] ); - const onBlur: FocusEventHandler = (e) => { + const onBlur: FocusEventHandler = () => { setTextEdit(false); }; @@ -129,7 +132,7 @@ const NumberInput: FunctionComponent = ({ } }; - const stopProp = useCallback((e: any) => { + const stopProp = useCallback((e: { stopPropagation: () => void }) => { e.stopPropagation(); }, []); @@ -171,12 +174,7 @@ const NumberInput: FunctionComponent = ({ } )} > - {sliderWidth !== null && ( -
    - )} + {sliderWidth !== null &&
    } {label && {label}} {value} @@ -184,5 +182,3 @@ const NumberInput: FunctionComponent = ({ ); }; - -export default NumberInput; diff --git a/src/components/input/Select.tsx b/core/components/src/input/Select.tsx similarity index 64% rename from src/components/input/Select.tsx rename to core/components/src/input/Select.tsx index ffeeb36..faa3a78 100644 --- a/src/components/input/Select.tsx +++ b/core/components/src/input/Select.tsx @@ -1,20 +1,16 @@ import _ from "lodash"; import { ChangeEventHandler, ReactNode, useCallback, useMemo } from "react"; -type SelectProps = { - options: T[]; +const NULL_TEXT = "unselect"; + +export type SelectProps = { + readonly options: readonly T[]; selected: T | null; allowUnselect?: boolean; renderOption: (value: T) => ReactNode; onSelect?: (value: T | null) => void; }; -export function Select({ - options, - selected, - onSelect, - renderOption, - allowUnselect, -}: SelectProps) { +export function Select({ options, selected, onSelect, renderOption, allowUnselect }: SelectProps) { const optionMap = useMemo(() => { return _.chain(options) .map((it, index) => [index, it]) @@ -24,7 +20,11 @@ export function Select({ const onChange: ChangeEventHandler = useCallback( (e) => { - onSelect?.(optionMap[e.target.value]); + if (e.target.value === NULL_TEXT) { + onSelect?.(null); + } else { + onSelect?.(optionMap[e.target.value]); + } }, [onSelect, optionMap] ); @@ -46,14 +46,8 @@ export function Select({ }, [optionMap, selected]); return ( - + {(isSelected(null) || allowUnselect === true) && } {Object.entries(optionMap).map(([key, value]) => (