Initial commit 2

This commit is contained in:
DrHaid
2025-12-13 14:09:00 +01:00
parent 25d7dd7ad2
commit 5a8dc3b057
13 changed files with 2016 additions and 2 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.next
# testing
/coverage
# misc
.DS_Store
*.pem
.idea
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
dist

View File

@@ -1,2 +1,24 @@
# todo-tree-chopper
Miro app for chopping down TODO trees
## TODO Tree Chopper
Efficiently chop down your TODO trees in Miro 👌
![todo-tree](./docs/todo-tree.png)
### How to start locally
- Run `npm i` to install dependencies.
- Run `npm start` to start developing. \
Your URL should be similar to this example:
```
http://localhost:3000
```
- Paste the URL under **App URL** in your
[app settings](https://developers.miro.com/docs/build-your-first-hello-world-app#step-3-configure-your-app-in-miro).
- Open a board; you should see your app in the app toolbar or in the **Apps**
panel.
### How to build the app
- Run `npm run build`. \
This generates a static output inside [`dist/`](./dist), which you can host on a static hosting
service.

15
app.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://miro.com/app/static/sdk/v2/miro.js"></script>
<title>TODO Tree Chopper 🪓</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>

BIN
docs/todo-tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

2
global.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// https://vitejs.dev/guide/features.html#typescript-compiler-options
/// <reference types="vite/client" />

36
index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/src/assets/style.css" />
<script src="https://miro.com/app/static/sdk/v2/miro.js"></script>
<title>TODO Tree Chopper</title>
</head>
<body>
<div id="root" aria-hidden="true">
<div class="grid container">
<div class="cs1 ce12">
<p style="font-size: large">Great, your app is running locally</p>
<p>
You can now create your Developer team to get your app running in
Miro.
</p>
</div>
<div class="cs1 ce12">
<a
class="button button-primary"
href="https://developers.miro.com/docs/create-a-developer-team"
target="_blank"
>
Create a Developer team
</a>
</div>
<div class="cs1 ce12">
<p>To see your app, open it in a app panel on Miro.com, or preview it at <a href="/app.html" class="link link-primary">this url</a></p>
</div>
</div>
</div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>

1675
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "todo-tree-chopper",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"mirotone": "5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "3.0.3",
"@mirohq/websdk-types": "latest",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"@vitejs/plugin-react": "^2.2.0",
"typescript": "4.9.5",
"@types/node": "^18.8.2"
}
}

39
src/app.tsx Normal file
View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import {createRoot} from 'react-dom/client';
import '../src/assets/style.css';
import { findExistingAxe } from '.';
const App: React.FC = () => {
const addAxe = async () => {
var axe = await findExistingAxe()
if (!axe) {
axe = await miro.board.createImage({
url: 'https://www.svgrepo.com/show/395800/axe.svg',
width: 200,
title: 'todo-tree-axe'
});
}
await miro.board.viewport.zoomTo(axe);
};
return (
<div className="grid wrapper">
<div className="cs1 ce12">
<h1>TODO Tree Chopper 🪓</h1>
</div>
<div className="cs1 ce12">
<button className="button button-primary" onClick={addAxe}>
Create Axe
</button>
</div>
</div>
);
};
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(<App />);
}

22
src/assets/style.css Normal file
View File

@@ -0,0 +1,22 @@
@import 'mirotone/dist/styles.css';
*,
*:before,
*:after {
box-sizing: border-box;
}
body {
display: flex;
}
#root {
width: 100%;
overflow: auto;
padding: var(--space-medium);
}
img {
max-width: 100%;
height: auto;
}

87
src/index.ts Normal file
View File

@@ -0,0 +1,87 @@
import { StickyNote, Image } from "@mirohq/websdk-types";
export async function init() {
miro.board.ui.on('icon:click', async () => {
await miro.board.ui.openPanel({ url: 'app.html' });
});
await miro.board.ui.on('custom:chop-todo-tree', handleChoppingAction);
await miro.board.experimental.action.register(
{
"event": "chop-todo-tree",
"ui": {
"label": {
"en": "Chop TODO tree",
},
"icon": "scissors",
"description": "Chop down TODO tree staring here",
},
"scope": "local",
"predicate": {
"type": "sticky_note"
},
"contexts": {
"item": {}
}
}
);
}
interface AxeAnimationArgs {
axe: Image,
stickyNote: StickyNote,
onChopped: () => void
}
const playAxeAnimation = async ({axe, stickyNote, onChopped}: AxeAnimationArgs) => {
await miro.board.bringToFront(axe);
axe.x = stickyNote.x;
axe.y = stickyNote.y;
await axe.sync();
for (let i = 0; i < 3; i++) {
axe.rotation = 90;
await axe.sync();
await new Promise(resolve => setTimeout(resolve, 200));
axe.rotation = 0;
await axe.sync();
await new Promise(resolve => setTimeout(resolve, 200));
}
onChopped();
}
async function postInfoNotification(message: string) {
await miro.board.notifications.showInfo(message);
}
const handleChoppingAction = async ({items}: {items:StickyNote[]}) => {
const axe = await findExistingAxe();
if (items.length === 0) {
postInfoNotification("Couldn't find anything to chop 🤔")
return;
}
if (!axe) {
postInfoNotification('No axe found. Create it first.');
return;
}
const target = items[0];
playAxeAnimation({
axe,
stickyNote: target,
onChopped: async () => await miro.board.remove(target)
});
};
export const findExistingAxe = async () => {
const items = await miro.board.get({ type: 'image' });
const axe = items.find(item => item.title === 'todo-tree-axe');
return axe;
};
init();

37
tsconfig.json Normal file
View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": [
"esnext",
"dom"
],
"jsx": "preserve",
"moduleResolution": "node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@mirohq"
],
"allowJs": true,
"incremental": true,
"isolatedModules": true
},
"include": [
"src",
"pages",
"*.ts"
],
"exclude": [
"node_modules"
]
}

31
vite.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import dns from 'dns';
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/server-options.html#server-host
dns.setDefaultResultOrder('verbatim');
// make sure vite picks up all html files in root, needed for vite build
const allHtmlEntries = fs
.readdirSync('.')
.filter((file) => path.extname(file) === '.html')
.reduce((acc, file) => {
acc[path.basename(file, '.html')] = path.resolve(__dirname, file);
return acc;
}, {});
// https://vitejs.dev/config/
export default defineConfig({
build: {
rollupOptions: {
input: allHtmlEntries,
},
},
plugins: [react()],
server: {
port: 3000,
},
});