Initial commit 2
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
|
||||||
26
README.md
26
README.md
@@ -1,2 +1,24 @@
|
|||||||
# todo-tree-chopper
|
## TODO Tree Chopper
|
||||||
Miro app for chopping down TODO trees
|
|
||||||
|
Efficiently chop down your TODO trees in Miro 👌
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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
15
app.html
Normal 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
BIN
docs/todo-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
2
global.d.ts
vendored
Normal file
2
global.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// https://vitejs.dev/guide/features.html#typescript-compiler-options
|
||||||
|
/// <reference types="vite/client" />
|
||||||
36
index.html
Normal file
36
index.html
Normal 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
1675
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal 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
39
src/app.tsx
Normal 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
22
src/assets/style.css
Normal 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
87
src/index.ts
Normal 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
37
tsconfig.json
Normal 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
31
vite.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user