diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f35b032..7a0eda5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,6 +13,7 @@
"@stripe/connect-js": "^3.3.31",
"@stripe/react-connect-js": "^3.3.31",
"@tanstack/react-query": "^5.90.10",
+ "@types/react-syntax-highlighter": "^15.5.13",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
"i18next": "^25.6.3",
@@ -25,6 +26,7 @@
"react-i18next": "^16.3.5",
"react-phone-number-input": "^3.4.14",
"react-router-dom": "^7.9.6",
+ "react-syntax-highlighter": "^16.1.0",
"recharts": "^3.5.0"
},
"devDependencies": {
@@ -1882,6 +1884,15 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1898,11 +1909,16 @@
"undici-types": "~7.16.0"
}
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1918,6 +1934,21 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-syntax-highlighter": {
+ "version": "15.5.13",
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
+ "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2177,6 +2208,36 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -2222,6 +2283,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2433,6 +2504,19 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2824,6 +2908,19 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
+ "node_modules/fault": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2925,6 +3022,14 @@
"node": ">= 6"
}
},
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -3111,6 +3216,36 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -3128,6 +3263,21 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/highlight.js": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/highlightjs-vue": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
+ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
+ "license": "CC0-1.0"
+ },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -3260,6 +3410,40 @@
"node": ">=12"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3281,6 +3465,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3681,6 +3875,20 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lowlight": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^1.0.0",
+ "highlight.js": "~10.7.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3888,6 +4096,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4003,6 +4236,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4020,6 +4262,16 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -4195,6 +4447,26 @@
"react-dom": ">=18"
}
},
+ "node_modules/react-syntax-highlighter": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
+ "integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "highlight.js": "^10.4.1",
+ "highlightjs-vue": "^1.0.0",
+ "lowlight": "^1.17.0",
+ "prismjs": "^1.30.0",
+ "refractor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 16.20.2"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0"
+ }
+ },
"node_modules/recharts": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.0.tgz",
@@ -4241,6 +4513,22 @@
"redux": "^5.0.0"
}
},
+ "node_modules/refractor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
+ "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/prismjs": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "parse-entities": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -4351,6 +4639,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9daac16..7a78d40 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,6 +9,7 @@
"@stripe/connect-js": "^3.3.31",
"@stripe/react-connect-js": "^3.3.31",
"@tanstack/react-query": "^5.90.10",
+ "@types/react-syntax-highlighter": "^15.5.13",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
"i18next": "^25.6.3",
@@ -21,6 +22,7 @@
"react-i18next": "^16.3.5",
"react-phone-number-input": "^3.4.14",
"react-router-dom": "^7.9.6",
+ "react-syntax-highlighter": "^16.1.0",
"recharts": "^3.5.0"
},
"devDependencies": {
diff --git a/frontend/public/plugin-logos/appointment-reminder-24hr.png b/frontend/public/plugin-logos/appointment-reminder-24hr.png
new file mode 100644
index 0000000..5062e76
Binary files /dev/null and b/frontend/public/plugin-logos/appointment-reminder-24hr.png differ
diff --git a/frontend/public/plugin-logos/appointment_reminder.svg b/frontend/public/plugin-logos/appointment_reminder.svg
new file mode 100644
index 0000000..1f0ffa8
--- /dev/null
+++ b/frontend/public/plugin-logos/appointment_reminder.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/plugin-logos/backup_database.svg b/frontend/public/plugin-logos/backup_database.svg
new file mode 100644
index 0000000..d9a4d50
--- /dev/null
+++ b/frontend/public/plugin-logos/backup_database.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/plugin-logos/birthday-greetings.png b/frontend/public/plugin-logos/birthday-greetings.png
new file mode 100644
index 0000000..648d415
Binary files /dev/null and b/frontend/public/plugin-logos/birthday-greetings.png differ
diff --git a/frontend/public/plugin-logos/cleanup_old_events.svg b/frontend/public/plugin-logos/cleanup_old_events.svg
new file mode 100644
index 0000000..5c1238c
--- /dev/null
+++ b/frontend/public/plugin-logos/cleanup_old_events.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/plugin-logos/daily-appointment-summary.png b/frontend/public/plugin-logos/daily-appointment-summary.png
new file mode 100644
index 0000000..55f545a
Binary files /dev/null and b/frontend/public/plugin-logos/daily-appointment-summary.png differ
diff --git a/frontend/public/plugin-logos/daily_report.svg b/frontend/public/plugin-logos/daily_report.svg
new file mode 100644
index 0000000..5c99d8d
--- /dev/null
+++ b/frontend/public/plugin-logos/daily_report.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/plugin-logos/generate_all_logos_gemini.py b/frontend/public/plugin-logos/generate_all_logos_gemini.py
new file mode 100644
index 0000000..38d41d5
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_all_logos_gemini.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+from google import genai
+from google.genai import types
+from PIL import Image
+from io import BytesIO
+import os
+import time
+
+# Setup Client
+client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos"
+
+# Plugin configurations with detailed prompts
+plugins = [
+ {
+ 'filename': 'daily-appointment-summary.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Indigo blue gradient background (#4f46e5 to lighter blue).
+Icon: Simple white envelope overlaid with a small calendar icon in the corner.
+Style: Flat design, clean geometric shapes, professional SaaS aesthetic.
+The icon should be crisp and recognizable at 48x48 pixels.'''
+ },
+ {
+ 'filename': 'no-show-tracker.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Red gradient background (#dc2626 to darker red).
+Icon: Simple white person silhouette with a bold white X symbol overlaid.
+Style: Flat design, clean geometric shapes, professional warning aesthetic.
+The icon should clearly convey "missed appointment" at small sizes.'''
+ },
+ {
+ 'filename': 'birthday-greetings.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Pink gradient background (#ec4899 to lighter pink).
+Icon: Simple white birthday cake with 3 candles or a white gift box with a bow.
+Style: Flat design, clean geometric shapes, cheerful yet professional aesthetic.
+The icon should feel celebratory and friendly at small sizes.'''
+ },
+ {
+ 'filename': 'monthly-revenue-report.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Green gradient background (#10b981 to darker green).
+Icon: Simple white upward trending line chart or ascending bar graph.
+Style: Flat design, clean geometric shapes, professional business analytics aesthetic.
+The icon should convey growth and success at small sizes.'''
+ },
+ {
+ 'filename': 'appointment-reminder-24hr.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Amber/orange gradient background (#f59e0b to darker orange).
+Icon: Simple white notification bell with a small red circular alert badge.
+Style: Flat design, clean geometric shapes, attention-grabbing yet professional aesthetic.
+The icon should clearly indicate alerts and reminders at small sizes.'''
+ },
+ {
+ 'filename': 'inactive-customer-reengagement.png',
+ 'prompt': '''Create a modern, minimalist app icon in square format with rounded corners.
+Design: Purple gradient background (#8b5cf6 to darker purple).
+Icon: Simple white heart symbol with a circular white refresh/return arrow around it.
+Style: Flat design, clean geometric shapes, warm and welcoming professional aesthetic.
+The icon should convey customer care and returning customers at small sizes.'''
+ }
+]
+
+def generate_logo(prompt, filename):
+ """Generate a logo using Gemini 2.5 Flash Image"""
+ print(f"\nGenerating {filename}...")
+
+ try:
+ response = client.models.generate_content(
+ model='gemini-2.5-flash-image',
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_modalities=["IMAGE"]
+ )
+ )
+
+ # Save the Image
+ if response.candidates[0].content.parts:
+ for part in response.candidates[0].content.parts:
+ if part.inline_data:
+ image_data = part.inline_data.data
+ image = Image.open(BytesIO(image_data))
+
+ # Save to output directory
+ output_path = os.path.join(OUTPUT_DIR, filename)
+ image.save(output_path)
+ print(f"✓ Saved: {output_path}")
+ return True
+ else:
+ print(f"✗ Model returned text instead of image: {part.text[:100]}")
+ return False
+ else:
+ print("✗ No content parts in response")
+ return False
+
+ except Exception as e:
+ error_msg = str(e)
+ if "RESOURCE_EXHAUSTED" in error_msg or "quota" in error_msg.lower():
+ print(f"✗ Quota exceeded. Please try again tomorrow when quota resets.")
+ print(f" Error: {error_msg[:200]}...")
+ else:
+ print(f"✗ Error: {type(e).__name__}: {error_msg[:200]}")
+ return False
+
+def main():
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+ print("=" * 70)
+ print("Generating Plugin Logos with Gemini 2.5 Flash Image")
+ print("=" * 70)
+
+ success_count = 0
+ for i, plugin in enumerate(plugins):
+ if i > 0:
+ # Wait 3 seconds between requests to avoid rate limiting
+ print("\nWaiting 3 seconds before next request...")
+ time.sleep(3)
+
+ if generate_logo(plugin['prompt'], plugin['filename']):
+ success_count += 1
+
+ print("\n" + "=" * 70)
+ print(f"Generation complete: {success_count}/{len(plugins)} successful")
+ print("=" * 70)
+
+ if success_count == 0:
+ print("\nNote: If you hit quota limits, try again tomorrow when the quota resets!")
+
+if __name__ == "__main__":
+ main()
diff --git a/frontend/public/plugin-logos/generate_daily_summary.py b/frontend/public/plugin-logos/generate_daily_summary.py
new file mode 100644
index 0000000..d7872f3
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_daily_summary.py
@@ -0,0 +1,77 @@
+import os
+import sys
+
+try:
+ from PIL import Image, ImageDraw
+except ImportError:
+ print("Error: Pillow library not found.")
+ print("Please install it using: pip install Pillow")
+ sys.exit(1)
+
+# Configuration
+OUTPUT_PATH = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos/daily-appointment-summary.png"
+SIZE = (144, 144) # Generated at 3x resolution (144px) for high quality on 48px displays
+BG_COLOR = "#4f46e5" # Indigo/Blue
+ICON_COLOR = "white"
+
+def create_logo():
+ # Create a new image with a transparent background
+ img = Image.new('RGBA', SIZE, (0, 0, 0, 0))
+ draw = ImageDraw.Draw(img)
+
+ # 1. Draw Background (Rounded Square)
+ radius = 30
+ rect_bounds = [0, 0, SIZE[0], SIZE[1]]
+ draw.rounded_rectangle(rect_bounds, radius=radius, fill=BG_COLOR)
+
+ # 2. Draw Envelope Icon (Email concept)
+ # Centered, slightly offset up to make room for calendar
+ env_w = 90
+ env_h = 60
+ env_x = (SIZE[0] - env_w) // 2
+ env_y = (SIZE[1] - env_h) // 2 - 10
+
+ # Envelope body
+ draw.rectangle([env_x, env_y, env_x + env_w, env_y + env_h], outline=ICON_COLOR, width=5)
+
+ # Envelope flap (V shape)
+ draw.line([env_x, env_y, env_x + env_w // 2, env_y + env_h // 2 + 5], fill=ICON_COLOR, width=5)
+ draw.line([env_x + env_w, env_y, env_x + env_w // 2, env_y + env_h // 2 + 5], fill=ICON_COLOR, width=5)
+
+ # 3. Draw Calendar Badge (Scheduling concept)
+ # Bottom right corner
+ cal_size = 50
+ cal_x = env_x + env_w - (cal_size // 2)
+ cal_y = env_y + env_h - (cal_size // 2)
+
+ # Clear background behind calendar for separation
+ border = 4
+ draw.rounded_rectangle(
+ [cal_x - border, cal_y - border, cal_x + cal_size + border, cal_y + cal_size + border],
+ radius=10, fill=BG_COLOR
+ )
+
+ # Calendar body
+ draw.rounded_rectangle([cal_x, cal_y, cal_x + cal_size, cal_y + cal_size], radius=8, fill="white")
+
+ # Calendar red header (using a lighter indigo/blue to match theme)
+ header_h = 14
+ draw.rounded_rectangle(
+ [cal_x, cal_y, cal_x + cal_size, cal_y + header_h],
+ radius=8, corners=(True, True, False, False), fill="#818cf8"
+ )
+
+ # Calendar 'lines'
+ line_x = cal_x + 10
+ line_w = cal_size - 20
+ draw.line([line_x, cal_y + 22, line_x + line_w, cal_y + 22], fill=BG_COLOR, width=3)
+ draw.line([line_x, cal_y + 32, line_x + line_w, cal_y + 32], fill=BG_COLOR, width=3)
+ draw.line([line_x, cal_y + 42, line_x + line_w * 0.6, cal_y + 42], fill=BG_COLOR, width=3)
+
+ # Save the file
+ os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
+ img.save(OUTPUT_PATH)
+ print(f"Success! Logo saved to: {OUTPUT_PATH}")
+
+if __name__ == "__main__":
+ create_logo()
diff --git a/frontend/public/plugin-logos/generate_plugin_logos.py b/frontend/public/plugin-logos/generate_plugin_logos.py
new file mode 100644
index 0000000..6144a82
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_plugin_logos.py
@@ -0,0 +1,155 @@
+import os
+import sys
+
+try:
+ from PIL import Image, ImageDraw, ImageFont
+except ImportError:
+ print("Error: Pillow library not found.")
+ print("Please install it using: pip install Pillow")
+ sys.exit(1)
+
+# Configuration
+OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos"
+SIZE = (144, 144)
+RADIUS = 30
+
+def create_base_logo(bg_color):
+ """Creates the base image with rounded corners and background color."""
+ img = Image.new('RGBA', SIZE, (0, 0, 0, 0))
+ draw = ImageDraw.Draw(img)
+ rect_bounds = [0, 0, SIZE[0], SIZE[1]]
+ draw.rounded_rectangle(rect_bounds, radius=RADIUS, fill=bg_color)
+ return img, draw
+
+def save_logo(img, filename):
+ """Saves the image to the output directory."""
+ path = os.path.join(OUTPUT_DIR, filename)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ img.save(path)
+ print(f"Saved: {path}")
+
+# 1. No-Show Customer Tracker (Red, Person with X)
+def create_no_show_logo():
+ img, draw = create_base_logo("#dc2626") # Red
+
+ # Person Silhouette
+ cx, cy = 72, 72
+ # Head
+ draw.ellipse([cx-15, 35, cx+15, 65], fill="white")
+ # Shoulders/Body
+ draw.rounded_rectangle([cx-30, 70, cx+30, 130], radius=10, fill="white")
+
+ # Cancel Symbol (Red X on white circle or just overlaid)
+ # Let's use a thick Red X overlay
+ x_center_y = 85
+ half = 20
+ width = 8
+
+ # Draw X
+ draw.line([cx - half, x_center_y - half, cx + half, x_center_y + half], fill="#dc2626", width=width)
+ draw.line([cx + half, x_center_y - half, cx - half, x_center_y + half], fill="#dc2626", width=width)
+
+ save_logo(img, "no-show-tracker.png")
+
+# 2. Birthday Greeting Campaign (Pink, Gift)
+def create_birthday_logo():
+ img, draw = create_base_logo("#ec4899") # Pink
+
+ # Gift Box
+ box_w, box_h = 70, 60
+ box_x = (SIZE[0] - box_w) // 2
+ box_y = (SIZE[1] - box_h) // 2 + 10
+
+ # Box body
+ draw.rectangle([box_x, box_y, box_x + box_w, box_y + box_h], fill="white")
+
+ # Ribbons (Lighter Pink)
+ r_w = 12
+ ribbon_color = "#fbcfe8"
+ # Vertical
+ draw.rectangle([box_x + (box_w - r_w)//2, box_y, box_x + (box_w + r_w)//2, box_y + box_h], fill=ribbon_color)
+ # Horizontal
+ draw.rectangle([box_x, box_y + (box_h - r_w)//2, box_x + box_w, box_y + (box_h + r_w)//2], fill=ribbon_color)
+
+ # Bow
+ bow_w = 20
+ draw.ellipse([72 - bow_w, box_y - 15, 72, box_y], fill="white")
+ draw.ellipse([72, box_y - 15, 72 + bow_w, box_y], fill="white")
+
+ save_logo(img, "birthday-greetings.png")
+
+# 3. Monthly Revenue Report (Green, Chart)
+def create_revenue_logo():
+ img, draw = create_base_logo("#10b981") # Green
+
+ # Bar Chart
+ margin_bottom = 110
+ bar_w = 20
+ gap = 10
+ start_x = (144 - (3 * bar_w + 2 * gap)) // 2
+
+ # Bars
+ heights = [30, 50, 75]
+ for i, h in enumerate(heights):
+ x = start_x + i * (bar_w + gap)
+ draw.rectangle([x, margin_bottom - h, x + bar_w, margin_bottom], fill="white")
+
+ # Trend Arrow (Rising)
+ arrow_start = (start_x - 5, margin_bottom - 10)
+ arrow_end = (start_x + 3 * bar_w + 2 * gap + 5, margin_bottom - 90)
+
+ # Simple line for trend
+ # draw.line([arrow_start, arrow_end], fill="white", width=4)
+
+ save_logo(img, "monthly-revenue-report.png")
+
+# 4. Appointment Reminder (24hr) (Amber, Bell)
+def create_reminder_logo():
+ img, draw = create_base_logo("#f59e0b") # Amber
+
+ cx, cy = 72, 70
+ r = 30
+
+ # Bell Dome
+ draw.chord([cx - r, cy - r, cx + r, cy + r], 180, 0, fill="white")
+ draw.rectangle([cx - r, cy, cx + r, cy + 20], fill="white")
+
+ # Bell Flare
+ draw.polygon([(cx - r, cy + 20), (cx + r, cy + 20), (cx + r + 5, cy + 30), (cx - r - 5, cy + 30)], fill="white")
+
+ # Clapper
+ draw.ellipse([cx - 8, cy + 28, cx + 8, cy + 42], fill="white")
+
+ # Notification Dot
+ draw.ellipse([cx + 15, cy - 25, cx + 35, cy - 5], fill="#dc2626")
+
+ save_logo(img, "appointment-reminder-24hr.png")
+
+# 5. Inactive Customer Re-engagement (Purple, Heart)
+def create_inactive_logo():
+ img, draw = create_base_logo("#8b5cf6") # Purple
+
+ hx, hy = 72, 75
+ size = 18
+
+ # Heart Shape
+ # Left circle
+ draw.ellipse([hx - size * 2, hy - size, hx, hy + size], fill="white")
+ # Right circle
+ draw.ellipse([hx, hy - size, hx + size * 2, hy + size], fill="white")
+ # Bottom triangle
+ draw.polygon([(hx - size * 2 + 1, hy + 4), (hx + size * 2 - 1, hy + 4), (hx, hy + size * 2 + 8)], fill="white")
+
+ # Refresh/Loop Arrow (Subtle arc above)
+ bbox = [25, 25, 119, 119]
+ draw.arc(bbox, start=180, end=0, fill="#ddd6fe", width=5)
+ draw.polygon([(119, 72), (110, 60), (128, 60)], fill="#ddd6fe")
+
+ save_logo(img, "inactive-customer-reengagement.png")
+
+if __name__ == "__main__":
+ create_no_show_logo()
+ create_birthday_logo()
+ create_revenue_logo()
+ create_reminder_logo()
+ create_inactive_logo()
diff --git a/frontend/public/plugin-logos/generate_with_2_0_flash.py b/frontend/public/plugin-logos/generate_with_2_0_flash.py
new file mode 100644
index 0000000..5571a5f
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_with_2_0_flash.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+from google import genai
+from google.genai import types
+from PIL import Image
+from io import BytesIO
+
+# Setup Client
+client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+# Test with a simple prompt
+prompt = "Create a simple modern app icon with a blue background and white envelope symbol, square format, flat design, minimalist"
+
+print(f"Testing gemini-2.0-flash-exp...")
+print(f"Prompt: '{prompt}'...")
+
+try:
+ response = client.models.generate_content(
+ model='gemini-2.0-flash-exp',
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_modalities=["IMAGE"]
+ )
+ )
+
+ # Save the Image
+ if response.candidates[0].content.parts:
+ for part in response.candidates[0].content.parts:
+ if part.inline_data:
+ image_data = part.inline_data.data
+ image = Image.open(BytesIO(image_data))
+ image.save("test_2_0_flash.png")
+ print("✓ Success! Saved test_2_0_flash.png")
+ else:
+ print(f"✗ Model returned text: {part.text[:100]}")
+ else:
+ print("✗ No content parts in response")
+
+except Exception as e:
+ print(f"✗ Error: {type(e).__name__}: {str(e)[:300]}")
diff --git a/frontend/public/plugin-logos/generate_with_gemini.py b/frontend/public/plugin-logos/generate_with_gemini.py
new file mode 100644
index 0000000..7e8cbe1
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_with_gemini.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+import os
+import sys
+import requests
+import base64
+from pathlib import Path
+
+# Gemini API configuration
+API_KEY = "AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw"
+API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"
+
+OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos"
+
+# Plugin configurations
+plugins = [
+ {
+ 'filename': 'daily-appointment-summary.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "Daily Appointment Summary Email".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Indigo/blue (#4f46e5)
+- Icon should combine email and calendar/scheduling concepts
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Professional and trustworthy appearance
+
+The icon represents a plugin that sends daily email summaries of appointments to staff members.'''
+ },
+ {
+ 'filename': 'no-show-tracker.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "No-Show Customer Tracker".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Red (#dc2626)
+- Icon should represent missed appointments or absent customers
+- Could show a person silhouette with an X, slash, or cancel symbol
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Professional appearance
+
+The icon represents a plugin that tracks customers who miss their appointments.'''
+ },
+ {
+ 'filename': 'birthday-greetings.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "Birthday Greeting Campaign".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Pink (#ec4899)
+- Icon should represent birthdays and celebrations
+- Could show a birthday cake, gift, party hat, or balloon
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Friendly and celebratory appearance
+
+The icon represents a plugin that sends birthday emails with special offers to customers.'''
+ },
+ {
+ 'filename': 'monthly-revenue-report.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "Monthly Revenue Report".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Green (#10b981)
+- Icon should represent business growth, analytics, and financial reporting
+- Could show an upward trending chart, graph, or money symbol
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Professional and successful appearance
+
+The icon represents a plugin that generates comprehensive monthly business statistics and revenue reports.'''
+ },
+ {
+ 'filename': 'appointment-reminder-24hr.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "Appointment Reminder (24hr)".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Amber/Orange (#f59e0b)
+- Icon should represent notifications, alerts, and reminders
+- Could show a bell with a notification badge, clock, or alarm
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Attention-grabbing but professional appearance
+
+The icon represents a plugin that sends reminder emails to customers 24 hours before their appointments.'''
+ },
+ {
+ 'filename': 'inactive-customer-reengagement.png',
+ 'prompt': '''Create a modern, professional icon/logo for a plugin called "Inactive Customer Re-engagement".
+
+Design requirements:
+- Square icon, 512x512 pixels
+- Flat design style with a slight gradient
+- Primary color: Purple (#8b5cf6)
+- Icon should represent customer retention, returning customers, or re-engagement
+- Could show a heart, person with return arrow, refresh symbol, or comeback concept
+- Clean, minimalist design suitable for a SaaS application
+- White or light elements on the colored background
+- Rounded corners (similar to modern app icons)
+- Warm and welcoming appearance
+
+The icon represents a plugin that wins back customers who haven't booked appointments recently.'''
+ }
+]
+
+def generate_image(prompt, filename):
+ """Generate an image using Gemini API"""
+ print(f"\nGenerating {filename}...")
+
+ headers = {
+ "Content-Type": "application/json"
+ }
+
+ payload = {
+ "contents": [{
+ "parts": [{
+ "text": prompt
+ }]
+ }],
+ "generationConfig": {
+ "temperature": 1,
+ "topK": 40,
+ "topP": 0.95,
+ "maxOutputTokens": 8192,
+ }
+ }
+
+ try:
+ response = requests.post(
+ f"{API_URL}?key={API_KEY}",
+ headers=headers,
+ json=payload,
+ timeout=120
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+
+ # Check if there's an image in the response
+ if 'candidates' in result and len(result['candidates']) > 0:
+ candidate = result['candidates'][0]
+ if 'content' in candidate and 'parts' in candidate['content']:
+ for part in candidate['content']['parts']:
+ if 'inlineData' in part:
+ # Extract and save the image
+ image_data = base64.b64decode(part['inlineData']['data'])
+ output_path = os.path.join(OUTPUT_DIR, filename)
+ with open(output_path, 'wb') as f:
+ f.write(image_data)
+ print(f"✓ Saved: {output_path}")
+ return True
+ elif 'text' in part:
+ print(f"Response: {part['text'][:200]}...")
+
+ print(f"✗ No image generated. Response: {result}")
+ return False
+ else:
+ print(f"✗ API Error ({response.status_code}): {response.text}")
+ return False
+
+ except Exception as e:
+ print(f"✗ Error: {str(e)}")
+ return False
+
+def main():
+ # Create output directory
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+ print("Starting logo generation with Gemini API...")
+ print("=" * 60)
+
+ success_count = 0
+ for plugin in plugins:
+ if generate_image(plugin['prompt'], plugin['filename']):
+ success_count += 1
+
+ print("\n" + "=" * 60)
+ print(f"Generation complete: {success_count}/{len(plugins)} successful")
+
+if __name__ == "__main__":
+ main()
diff --git a/frontend/public/plugin-logos/generate_with_gemini_sdk.py b/frontend/public/plugin-logos/generate_with_gemini_sdk.py
new file mode 100644
index 0000000..742694a
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_with_gemini_sdk.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+import google.generativeai as genai
+import os
+from PIL import Image
+import io
+
+# Configure API Key
+genai.configure(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos"
+
+# Plugin configurations
+plugins = [
+ {
+ 'filename': 'daily-appointment-summary.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Indigo blue gradient background (#4f46e5). White simple envelope icon combined with a small calendar symbol.
+Style: Flat design, clean geometric shapes, professional SaaS application aesthetic.
+The icon should be instantly recognizable at 48x48 pixels.'''
+ },
+ {
+ 'filename': 'no-show-tracker.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Red gradient background (#dc2626). White simple person silhouette with a bold X or cancel symbol overlay.
+Style: Flat design, clean geometric shapes, professional SaaS application aesthetic.
+The icon should clearly convey "missed appointment" at small sizes.'''
+ },
+ {
+ 'filename': 'birthday-greetings.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Pink gradient background (#ec4899). White simple birthday cake with candles or gift box with bow.
+Style: Flat design, clean geometric shapes, cheerful yet professional aesthetic.
+The icon should feel celebratory and friendly at small sizes.'''
+ },
+ {
+ 'filename': 'monthly-revenue-report.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Green gradient background (#10b981). White simple upward trending line chart or bar graph showing growth.
+Style: Flat design, clean geometric shapes, professional business analytics aesthetic.
+The icon should convey success and growth at small sizes.'''
+ },
+ {
+ 'filename': 'appointment-reminder-24hr.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Amber/orange gradient background (#f59e0b). White simple notification bell with a small red alert dot or badge.
+Style: Flat design, clean geometric shapes, attention-grabbing yet professional aesthetic.
+The icon should clearly indicate alerts and reminders at small sizes.'''
+ },
+ {
+ 'filename': 'inactive-customer-reengagement.png',
+ 'prompt': '''Create a modern, minimalist app icon in a square format with rounded corners.
+Design: Purple gradient background (#8b5cf6). White simple heart symbol with a circular refresh/return arrow around it.
+Style: Flat design, clean geometric shapes, warm and welcoming professional aesthetic.
+The icon should convey customer care and comeback at small sizes.'''
+ }
+]
+
+def generate_image(prompt, filename):
+ """Generate an image using Gemini 2.0 Flash Image Generation"""
+ print(f"\nGenerating {filename}...")
+
+ try:
+ # Use the image generation model
+ model = genai.GenerativeModel('gemini-2.0-flash-exp-image-generation')
+
+ # Generate the image
+ response = model.generate_content(prompt)
+
+ # Check for generated image
+ if hasattr(response, 'parts') and response.parts:
+ for part in response.parts:
+ if hasattr(part, 'inline_data') and part.inline_data:
+ # Extract image data
+ image_data = part.inline_data.data
+
+ # Save the image
+ output_path = os.path.join(OUTPUT_DIR, filename)
+ with open(output_path, 'wb') as f:
+ f.write(image_data)
+
+ print(f"✓ Saved: {output_path} ({len(image_data)} bytes)")
+ return True
+
+ # If no image data found, check if there's text response
+ if hasattr(response, 'text'):
+ print(f"✗ No image generated. Response text: {response.text[:200]}")
+ else:
+ print(f"✗ No image generated. Response: {response}")
+
+ return False
+
+ except Exception as e:
+ print(f"✗ Error: {type(e).__name__}: {str(e)}")
+ return False
+
+def main():
+ # Create output directory
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+ print("Starting logo generation with Gemini 2.0 Flash Image Generation...")
+ print("=" * 60)
+
+ success_count = 0
+ for plugin in plugins:
+ if generate_image(plugin['prompt'], plugin['filename']):
+ success_count += 1
+
+ print("\n" + "=" * 60)
+ print(f"Generation complete: {success_count}/{len(plugins)} successful")
+
+if __name__ == "__main__":
+ main()
diff --git a/frontend/public/plugin-logos/generate_with_imagen.py b/frontend/public/plugin-logos/generate_with_imagen.py
new file mode 100644
index 0000000..9aef91b
--- /dev/null
+++ b/frontend/public/plugin-logos/generate_with_imagen.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+import os
+import sys
+import requests
+import base64
+import time
+from pathlib import Path
+
+# Imagen API configuration
+API_KEY = "AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw"
+MODEL = "imagen-4.0-generate-preview-06-06"
+API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:predict"
+
+OUTPUT_DIR = "/home/poduck/Desktop/smoothschedule2/frontend/public/plugin-logos"
+
+# Plugin configurations
+plugins = [
+ {
+ 'filename': 'daily-appointment-summary.png',
+ 'prompt': '''A modern, minimalist app icon for "Daily Appointment Summary Email" plugin.
+Square format with rounded corners. Indigo blue gradient background (#4f46e5).
+White icon showing a combination of an envelope and a calendar.
+Flat design, clean lines, professional SaaS application style.
+The icon should be simple and recognizable at small sizes.'''
+ },
+ {
+ 'filename': 'no-show-tracker.png',
+ 'prompt': '''A modern, minimalist app icon for "No-Show Customer Tracker" plugin.
+Square format with rounded corners. Red gradient background (#dc2626).
+White icon showing a person silhouette with an X or cancel symbol overlay.
+Flat design, clean lines, professional SaaS application style.
+The icon should convey missed appointments clearly at small sizes.'''
+ },
+ {
+ 'filename': 'birthday-greetings.png',
+ 'prompt': '''A modern, minimalist app icon for "Birthday Greeting Campaign" plugin.
+Square format with rounded corners. Pink gradient background (#ec4899).
+White icon showing a birthday cake or gift box with a bow.
+Flat design, clean lines, cheerful yet professional style.
+The icon should be celebratory and friendly at small sizes.'''
+ },
+ {
+ 'filename': 'monthly-revenue-report.png',
+ 'prompt': '''A modern, minimalist app icon for "Monthly Revenue Report" plugin.
+Square format with rounded corners. Green gradient background (#10b981).
+White icon showing an upward trending chart or bar graph.
+Flat design, clean lines, professional business analytics style.
+The icon should convey growth and success at small sizes.'''
+ },
+ {
+ 'filename': 'appointment-reminder-24hr.png',
+ 'prompt': '''A modern, minimalist app icon for "Appointment Reminder" plugin.
+Square format with rounded corners. Amber/orange gradient background (#f59e0b).
+White icon showing a notification bell with a small red badge or alert dot.
+Flat design, clean lines, attention-grabbing yet professional style.
+The icon should convey alerts and reminders clearly at small sizes.'''
+ },
+ {
+ 'filename': 'inactive-customer-reengagement.png',
+ 'prompt': '''A modern, minimalist app icon for "Inactive Customer Re-engagement" plugin.
+Square format with rounded corners. Purple gradient background (#8b5cf6).
+White icon showing a heart with a circular refresh arrow around it.
+Flat design, clean lines, warm and welcoming professional style.
+The icon should convey customer care and return at small sizes.'''
+ }
+]
+
+def generate_image(prompt, filename):
+ """Generate an image using Imagen API"""
+ print(f"\nGenerating {filename}...")
+
+ headers = {
+ "Content-Type": "application/json"
+ }
+
+ payload = {
+ "instances": [{
+ "prompt": prompt
+ }],
+ "parameters": {
+ "sampleCount": 1
+ }
+ }
+
+ try:
+ response = requests.post(
+ f"{API_URL}?key={API_KEY}",
+ headers=headers,
+ json=payload,
+ timeout=120
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+
+ # Check if there's an image in the predictions
+ if 'predictions' in result and len(result['predictions']) > 0:
+ prediction = result['predictions'][0]
+ if 'bytesBase64Encoded' in prediction:
+ # Extract and save the image
+ image_data = base64.b64decode(prediction['bytesBase64Encoded'])
+ output_path = os.path.join(OUTPUT_DIR, filename)
+ with open(output_path, 'wb') as f:
+ f.write(image_data)
+ print(f"✓ Saved: {output_path} ({len(image_data)} bytes)")
+ return True
+ elif 'image' in prediction and 'bytesBase64Encoded' in prediction['image']:
+ image_data = base64.b64decode(prediction['image']['bytesBase64Encoded'])
+ output_path = os.path.join(OUTPUT_DIR, filename)
+ with open(output_path, 'wb') as f:
+ f.write(image_data)
+ print(f"✓ Saved: {output_path} ({len(image_data)} bytes)")
+ return True
+
+ print(f"✗ No image generated. Response: {str(result)[:300]}...")
+ return False
+ else:
+ print(f"✗ API Error ({response.status_code}): {response.text[:500]}")
+ return False
+
+ except Exception as e:
+ print(f"✗ Error: {str(e)}")
+ return False
+
+def main():
+ # Create output directory
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+ print("Starting logo generation with Imagen 4.0 API...")
+ print("=" * 60)
+
+ success_count = 0
+ for i, plugin in enumerate(plugins):
+ if i > 0:
+ # Add delay between requests to avoid rate limiting
+ print("Waiting 2 seconds before next request...")
+ time.sleep(2)
+
+ if generate_image(plugin['prompt'], plugin['filename']):
+ success_count += 1
+
+ print("\n" + "=" * 60)
+ print(f"Generation complete: {success_count}/{len(plugins)} successful")
+
+if __name__ == "__main__":
+ main()
diff --git a/frontend/public/plugin-logos/inactive-customer-reengagement.png b/frontend/public/plugin-logos/inactive-customer-reengagement.png
new file mode 100644
index 0000000..357b4cd
Binary files /dev/null and b/frontend/public/plugin-logos/inactive-customer-reengagement.png differ
diff --git a/frontend/public/plugin-logos/monthly-revenue-report.png b/frontend/public/plugin-logos/monthly-revenue-report.png
new file mode 100644
index 0000000..e440e3a
Binary files /dev/null and b/frontend/public/plugin-logos/monthly-revenue-report.png differ
diff --git a/frontend/public/plugin-logos/no-show-tracker.png b/frontend/public/plugin-logos/no-show-tracker.png
new file mode 100644
index 0000000..2ab4ee6
Binary files /dev/null and b/frontend/public/plugin-logos/no-show-tracker.png differ
diff --git a/frontend/public/plugin-logos/send_email.svg b/frontend/public/plugin-logos/send_email.svg
new file mode 100644
index 0000000..652110d
--- /dev/null
+++ b/frontend/public/plugin-logos/send_email.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/public/plugin-logos/test_gemini_image.py b/frontend/public/plugin-logos/test_gemini_image.py
new file mode 100644
index 0000000..399d18a
--- /dev/null
+++ b/frontend/public/plugin-logos/test_gemini_image.py
@@ -0,0 +1,43 @@
+import google.generativeai as genai
+import os
+from PIL import Image
+import io
+
+# Configure API Key
+genai.configure(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+# Try to initialize the model
+try:
+ generation_model = genai.GenerativeModel('gemini-pro-vision')
+ print("Model initialized successfully")
+
+ # Define prompt
+ prompt = "A modern app icon with a blue background and white envelope symbol"
+ print(f"Generating image for: '{prompt}'...")
+
+ # Try to generate
+ response = generation_model.generate_content(prompt)
+
+ print(f"Response type: {type(response)}")
+ print(f"Response: {response}")
+
+ if hasattr(response, 'parts') and response.parts:
+ print(f"Parts: {response.parts}")
+ if hasattr(response.parts[0], 'inline_data'):
+ print("Has inline_data!")
+ image_data = response.parts[0].inline_data.data
+ image = Image.open(io.BytesIO(image_data))
+ image.save("test_output.png")
+ print("Success! Image saved to test_output.png")
+ else:
+ print("No inline_data attribute")
+ print(f"Part attributes: {dir(response.parts[0])}")
+ else:
+ print("No parts in response or response.parts doesn't exist")
+ if hasattr(response, 'text'):
+ print(f"Response text: {response.text}")
+
+except Exception as e:
+ print(f"Error: {type(e).__name__}: {str(e)}")
+ import traceback
+ traceback.print_exc()
diff --git a/frontend/public/plugin-logos/test_gemini_native_image.py b/frontend/public/plugin-logos/test_gemini_native_image.py
new file mode 100644
index 0000000..5c5eb92
--- /dev/null
+++ b/frontend/public/plugin-logos/test_gemini_native_image.py
@@ -0,0 +1,41 @@
+from google import genai
+from google.genai import types
+from PIL import Image
+from io import BytesIO
+import os
+
+# Setup Client
+client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+# Define the Prompt
+prompt = "Create a simple modern app icon with a blue background and white envelope symbol, square format, flat design, minimalist"
+
+print(f"Asking Gemini to generate: '{prompt}'...")
+
+# Call the Gemini Model
+try:
+ response = client.models.generate_content(
+ model='gemini-2.5-flash-image',
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_modalities=["IMAGE"] # Tell Gemini to draw, not talk
+ )
+ )
+
+ # Save the Image
+ if response.candidates[0].content.parts:
+ for part in response.candidates[0].content.parts:
+ if part.inline_data:
+ image_data = part.inline_data.data
+ image = Image.open(BytesIO(image_data))
+ image.save("test_gemini_native.png")
+ print("Success! Saved test_gemini_native.png")
+ else:
+ print("Model returned text instead of image:", part.text)
+ else:
+ print("No content parts in response")
+
+except Exception as e:
+ print(f"Error: {type(e).__name__}: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/frontend/public/plugin-logos/test_google_genai.py b/frontend/public/plugin-logos/test_google_genai.py
new file mode 100644
index 0000000..298044c
--- /dev/null
+++ b/frontend/public/plugin-logos/test_google_genai.py
@@ -0,0 +1,41 @@
+from google import genai
+from google.genai import types
+from PIL import Image
+from io import BytesIO
+import os
+
+# Initialize the Client
+client = genai.Client(api_key="AIzaSyB-nR0nkeftKrd42NrNIDcFCj3yFP8JLtw")
+
+# Define the prompt
+prompt = "A simple modern app icon with a blue background and white envelope symbol, square format, flat design"
+
+print(f"Generating image for: '{prompt}'...")
+
+# Call the API
+try:
+ response = client.models.generate_images(
+ model='gemini-2.5-flash-image',
+ prompt=prompt,
+ config=types.GenerateImagesConfig(
+ number_of_images=1,
+ aspect_ratio="1:1",
+ safety_filter_level="BLOCK_LOW_AND_ABOVE",
+ person_generation="ALLOW_ADULT"
+ )
+ )
+
+ # Handle the response
+ for i, generated_image in enumerate(response.generated_images):
+ # Convert raw bytes to an image
+ image = Image.open(BytesIO(generated_image.image.image_bytes))
+
+ # Save to disk
+ filename = f"test_generated_image_{i}.png"
+ image.save(filename)
+ print(f"Success! Saved {filename}")
+
+except Exception as e:
+ print(f"Error occurred: {type(e).__name__}: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/frontend/public/plugin-logos/webhook.svg b/frontend/public/plugin-logos/webhook.svg
new file mode 100644
index 0000000..854198c
--- /dev/null
+++ b/frontend/public/plugin-logos/webhook.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c6331dc..607fb5a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -65,6 +65,7 @@ import HelpPluginDocs from './pages/HelpPluginDocs'; // Import Plugin documentat
import PlatformSupport from './pages/PlatformSupport'; // Import Platform Support page (for businesses to contact SmoothSchedule)
import PluginMarketplace from './pages/PluginMarketplace'; // Import Plugin Marketplace page
import MyPlugins from './pages/MyPlugins'; // Import My Plugins page
+import Tasks from './pages/Tasks'; // Import Tasks page for scheduled plugin executions
import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications
const queryClient = new QueryClient({
@@ -536,6 +537,16 @@ const AppContent: React.FC = () => {
)
}
/>
+
+ ) : (
+
+ )
+ }
+ />
} />
{'{{PROMPT:variable|description|default}}'}
- Optional field with default value
+
+ {'{{PROMPT:variable|description|default|textarea}}'}
+ - Multi-line text input (for email bodies, long messages)
+
+
+
+
+ Field Type Detection
+
+
+ The system automatically detects field types from variable names and descriptions: email for email validation,
+ number for numeric inputs, message/body/content for textareas, url/webhook for URLs.
+ You can override this by explicitly specifying the type as the 4th parameter.
+
@@ -813,9 +827,65 @@ result = {'total': len(appointments), 'by_status': stats}`}
+ Use insertion codes within your PROMPT template text (like email bodies) to inject dynamic content
+ at runtime. These are automatically replaced with actual values when the plugin executes.
+
+
+
+ Business Information:
+
+
+
{'{{BUSINESS_NAME}}'} - Your business name
+
{'{{BUSINESS_EMAIL}}'} - Business contact email
+
{'{{BUSINESS_PHONE}}'} - Business phone number
+
+
+
+
+ Customer & Appointment Data:
+
+
+
{'{{CUSTOMER_NAME}}'} - Customer's name (in appointment contexts)
+
{'{{CUSTOMER_EMAIL}}'} - Customer's email address
+
{'{{APPOINTMENT_TIME}}'} - Full appointment date and time
+
{'{{APPOINTMENT_DATE}}'} - Appointment date only
+
{'{{APPOINTMENT_SERVICE}}'} - Service name
+
+
+
+
+ Date & Time:
+
+
+
{'{{TODAY}}'} - Today's date (YYYY-MM-DD)
+
{'{{NOW}}'} - Current date and time
+
+
+
+
Example: Email Template with Insertion Codes
+
+{`email_body = '{{PROMPT:email_body|Email Message|Hi {{CUSTOMER_NAME}},
+
+This is a reminder about your appointment:
+
+Date/Time: {{APPOINTMENT_TIME}}
+Service: {{APPOINTMENT_SERVICE}}
+
+If you have questions, contact us at {{BUSINESS_EMAIL}}
+
+Best regards,
+{{BUSINESS_NAME}}||textarea}}'`}
+
+
+
+ {/* 5. Validation & Types */}
+
+ 5. Automatic Validation
The system automatically detects field types and validates input:
@@ -830,7 +900,8 @@ result = {'total': len(appointments), 'by_status': stats}`}
Pro Tip: Combine all template types for maximum power! Use CONTEXT
- for business info, DATE for time logic, and PROMPT only when user input is truly needed.
+ for business info, DATE for time logic, PROMPT for user configuration, and Insertion Codes
+ within your email templates for personalized dynamic content.