diff --git a/assets/dribbble.png b/assets/dribbble.png new file mode 100644 index 000000000..885a722dc Binary files /dev/null and b/assets/dribbble.png differ diff --git a/package-lock.json b/package-lock.json index fbd3747a8..706ebeb93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,267 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.25.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", @@ -64,6 +325,64 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -448,14 +767,64 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", @@ -510,6 +879,29 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -584,6 +976,44 @@ "node": ">= 0.12" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -641,6 +1071,20 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -2753,20 +3197,22 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.56.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz", - "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==", + "version": "5.61.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.61.5.tgz", + "integrity": "sha512-iG5vqurEOEbv+paP6kW3zPENa99kSIrd1THISJMaTwVlJ+N5yjVDNOUwp9McK2DWqWCXM3v13ubBbAyhxT78UQ==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.56.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz", - "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==", + "version": "5.61.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.5.tgz", + "integrity": "sha512-rjy8aqPgBBEz/rjJnpnuhi8TVkVTorMUsJlM3lMvrRb5wK6yzfk34Er0fnJ7w/4qyF01SnXsLB/QsTBsLF5PaQ==", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.56.2" + "@tanstack/query-core": "5.61.5" }, "funding": { "type": "github", @@ -2796,14 +3242,59 @@ "integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==", "dev": true }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } @@ -3555,6 +4046,26 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", @@ -3568,10 +4079,11 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3771,6 +4283,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -3996,9 +4509,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -4014,11 +4527,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -4126,9 +4640,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001684", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", + "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", "dev": true, "funding": [ { @@ -4143,7 +4657,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -4226,7 +4741,8 @@ "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -4397,11 +4913,19 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4836,6 +5360,10 @@ "resolved": "plugins/doodles", "link": true }, + "node_modules/dribbble": { + "resolved": "plugins/dribbble", + "link": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4843,10 +5371,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz", - "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==", - "dev": true + "version": "1.5.65", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz", + "integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5589,6 +6118,16 @@ "node": ">=10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6077,6 +6616,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6100,6 +6652,19 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7164,9 +7729,10 @@ "link": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -7226,9 +7792,9 @@ "link": true }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -7243,9 +7809,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -7587,9 +8154,10 @@ } }, "node_modules/react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -7627,6 +8195,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", @@ -8875,33 +9453,34 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", - "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==", + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", + "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", + "jiti": "^1.21.6", "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -9177,6 +9756,244 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", + "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.16.0", + "@typescript-eslint/parser": "8.16.0", + "@typescript-eslint/utils": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -9192,9 +10009,9 @@ "link": true }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -9210,9 +10027,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -10270,6 +11088,303 @@ "vite-plugin-framer": "^1.0.1" } }, + "plugins/dribbble": { + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.61.5", + "classnames": "^2.5.1", + "framer-plugin": "^2", + "p-limit": "^6.1.0", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.1.2", + "vite-plugin-mkcert": "^1" + }, + "devDependencies": { + "@eslint/js": "^9", + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react-swc": "^3", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", + "typescript": "^5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5", + "vite-plugin-framer": "^1" + } + }, + "plugins/dribbble/node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/dribbble/node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "plugins/dribbble/node_modules/@eslint/js": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "plugins/dribbble/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "plugins/dribbble/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "plugins/dribbble/node_modules/eslint": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "plugins/dribbble/node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0-rc-fb9a90fa48-20240614", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", + "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "plugins/dribbble/node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/dribbble/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/dribbble/node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/dribbble/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "plugins/dribbble/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "plugins/dribbble/node_modules/globals": { + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "plugins/dribbble/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "plugins/dribbble/node_modules/p-limit": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", + "integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "plugins/dribbble/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "plugins/flip-image": { "version": "0.0.0", "dependencies": { diff --git a/plugins/dribbble/.gitignore b/plugins/dribbble/.gitignore new file mode 100644 index 000000000..255194527 --- /dev/null +++ b/plugins/dribbble/.gitignore @@ -0,0 +1,33 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies + +/node_modules +/.pnp +.pnp.js +.yarn + +# misc + +.DS_Store +\*.pem + +# files + +my-plugin +dev-plugin +dist + +# debug + +npm-debug.log* +yarn-debug.log* +yarn-error.log\* + +# local env files + +.env\*.local + +# Packed plugin + +plugin.zip diff --git a/plugins/dribbble/README.md b/plugins/dribbble/README.md new file mode 100644 index 000000000..02cdb4e37 --- /dev/null +++ b/plugins/dribbble/README.md @@ -0,0 +1,7 @@ +# Dribbble Plugin + +Sync shots from Dribbble + +**By:** @sakib25800 + +![Dribbble Image](../../assets/dribbble.png) diff --git a/plugins/dribbble/eslint.config.js b/plugins/dribbble/eslint.config.js new file mode 100644 index 000000000..6e64b68bd --- /dev/null +++ b/plugins/dribbble/eslint.config.js @@ -0,0 +1,25 @@ +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import tseslint from "typescript-eslint" + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, + } +) diff --git a/plugins/dribbble/framer.json b/plugins/dribbble/framer.json new file mode 100644 index 000000000..f3d299b3d --- /dev/null +++ b/plugins/dribbble/framer.json @@ -0,0 +1,6 @@ +{ + "id": "357ab0", + "name": "Dribbble", + "modes": ["canvas", "configureManagedCollection", "syncManagedCollection"], + "icon": "/icon.svg" +} diff --git a/plugins/dribbble/index.html b/plugins/dribbble/index.html new file mode 100644 index 000000000..564cb9568 --- /dev/null +++ b/plugins/dribbble/index.html @@ -0,0 +1,14 @@ + + + + + + + Dribbble + + +
+ + + + diff --git a/plugins/dribbble/package.json b/plugins/dribbble/package.json new file mode 100644 index 000000000..905e51d7f --- /dev/null +++ b/plugins/dribbble/package.json @@ -0,0 +1,41 @@ +{ + "name": "dribbble", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "pack": "npx framer-plugin-tools@latest pack" + }, + "dependencies": { + "@tanstack/react-query": "^5.61.5", + "classnames": "^2.5.1", + "framer-plugin": "^2", + "p-limit": "^6.1.0", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.1.2", + "vite-plugin-mkcert": "^1" + }, + "devDependencies": { + "@eslint/js": "^9", + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react-swc": "^3", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", + "typescript": "^5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5", + "vite-plugin-framer": "^1" + } +} diff --git a/plugins/dribbble/postcss.config.js b/plugins/dribbble/postcss.config.js new file mode 100644 index 000000000..d41ad6355 --- /dev/null +++ b/plugins/dribbble/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/plugins/dribbble/public/icon.svg b/plugins/dribbble/public/icon.svg new file mode 100644 index 000000000..98c01e88c --- /dev/null +++ b/plugins/dribbble/public/icon.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/plugins/dribbble/src/App.tsx b/plugins/dribbble/src/App.tsx new file mode 100644 index 000000000..d51a178b5 --- /dev/null +++ b/plugins/dribbble/src/App.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react" +import { getPluginContext, PluginContext, useSyncShotsMutation } from "./sync" +import { framer } from "framer-plugin" +import auth from "./auth" +import Authenticate from "./pages/Authenticate" +import MapFields from "./pages/MapFields" + +export function AuthenticatedApp({ context }: { context: PluginContext }) { + const syncMutation = useSyncShotsMutation({ + onSuccess: result => (result.status === "success" ? framer.closePlugin("Synchronization successful") : null), + onError: e => framer.notify(e.message, { variant: "error" }), + }) + + useEffect(() => { + framer.showUI({ + width: 340, + height: 410, + }) + }, []) + + return +} + +export function App({ context }: { context: PluginContext }) { + const [pluginContext, setPluginContext] = useState(context) + + useEffect(() => { + if (!auth.isAuthenticated()) { + framer.showUI({ + width: 260, + height: 345, + }) + } + }, []) + + const handleAuthenticated = async () => { + setPluginContext(await getPluginContext()) + } + + if (!auth.isAuthenticated()) { + return + } + + return +} diff --git a/plugins/dribbble/src/api.ts b/plugins/dribbble/src/api.ts new file mode 100644 index 000000000..41672b66e --- /dev/null +++ b/plugins/dribbble/src/api.ts @@ -0,0 +1,209 @@ +import auth from "./auth" +import pLimit from "p-limit" + +type RequestQueryParams = Record + +interface RequestOptions { + path: string + method?: string + query?: RequestQueryParams + // eslint-disable-next-line + body?: any +} + +interface PaginationParams extends Record { + page: number + per_page: number +} + +export interface Shot { + id: number + title: string + description: string + width: number + height: number + images: { + hidpi: string | null + normal: string + teaser: string + } + low_profile: boolean + published_at: string + updated_at: string + html_url: string + animated: boolean + tags: string[] + attachments: Array<{ + id: number + url: string + thumbnail_url: string + size: number + content_type: string + created_at: string + }> + projects: Array<{ + id: number + name: string + description: string + shots_count: number + created_at: string + updated_at: string + }> + team?: { + id: number + name: string + login: string + html_url: string + avatar_url: string + bio: string + location: string + links: { + web: string + twitter: string + } + type: string + created_at: string + updated_at: string + } + video?: { + id: number + duration: number + width: number + height: number + url: string + small_preview_url: string + large_preview_url: string + xlarge_preview_url: string + } +} + +const DRIBBLE_BASE_URL = "https://api.dribbble.com/v2" +const MAX_PAGE_SIZE = 100 // v2 pagination limit +const CONCURRENCY_LIMIT = 10 // v2 limit: 60 requests per minute + +const request = async ({ + path, + method, + query, + body, +}: RequestOptions): Promise<{ data: T; headers: Headers }> => { + const tokens = auth.tokens.getOrThrow() + const url = new URL(`${DRIBBLE_BASE_URL}${path}`) + + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + if (Array.isArray(value)) { + value.forEach(val => url.searchParams.append(key, decodeURIComponent(val))) + } else { + url.searchParams.append(key, String(value)) + } + } + } + } + + const res = await fetch(url, { + method: method?.toUpperCase() ?? "GET", + body: body ? JSON.stringify(body) : undefined, + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (method === "delete" && res.status === 204) { + return { data: {} as T, headers: res.headers } + } + + if (!res.ok) { + throw new Error(`Failed to fetch Dribbble API: ${res.status}`) + } + + const data = await res.json() + return { data: data as T, headers: res.headers } +} + +const parseLinkHeader = (headers: Headers): { next?: string } => { + const linkHeader = headers.get("Link") + if (!linkHeader) return {} + + const links = linkHeader.split(",").reduce( + (acc, part) => { + const match = part.match(/<(.+)>;\s*rel="(\w+)"/) + if (match) { + const [, url, rel] = match + return { ...acc, [rel]: url } + } + return acc + }, + {} as Record + ) + + return { next: links["next"] } +} + +const paginatedRequest = async (options: RequestOptions): Promise<{ data: T; nextUrl?: string }> => { + const { data, headers } = await request(options) + const { next } = parseLinkHeader(headers) + + return { + data, + nextUrl: next, + } +} + +export const fetchShots = async (params: PaginationParams): Promise<{ shots: Shot[]; nextUrl?: string }> => { + const response = await paginatedRequest({ + path: "/user/shots", + query: params, + }) + + return { + shots: response.data, + nextUrl: response.nextUrl, + } +} + +export const fetchAllShots = async (max: number): Promise => { + const limit = pLimit(CONCURRENCY_LIMIT) + let allShots: Shot[] = [] + + // Get initial page + const { shots: firstPage, nextUrl } = await fetchShots({ + page: 1, + per_page: MAX_PAGE_SIZE, + }) + + allShots = [...firstPage] + + const urlsToFetch: string[] = [] + let currentNextUrl = nextUrl + + // Collect all URLs to fetch + while (currentNextUrl && allShots.length + urlsToFetch.length * MAX_PAGE_SIZE < max) { + urlsToFetch.push(currentNextUrl) + const { nextUrl } = await fetchShots({ + page: Number(new URL(currentNextUrl).searchParams.get("page")), + per_page: MAX_PAGE_SIZE, + }) + currentNextUrl = nextUrl + } + + const results = await Promise.all( + urlsToFetch.map(url => + limit(() => + fetchShots({ + page: Number(new URL(url).searchParams.get("page")), + per_page: MAX_PAGE_SIZE, + }) + ) + ) + ) + + allShots = allShots.concat(...results.map(r => r.shots)) + + if (allShots.length > max) { + allShots = allShots.slice(0, max) + } + + return allShots +} diff --git a/plugins/dribbble/src/auth.ts b/plugins/dribbble/src/auth.ts new file mode 100644 index 000000000..7feb58774 --- /dev/null +++ b/plugins/dribbble/src/auth.ts @@ -0,0 +1,103 @@ +export interface Tokens { + access_token: string + token_type: "bearer" + scope: string +} + +export interface StoredTokens { + createdAt: number + accessToken: string + scope: string +} + +export interface Authorize { + url: string + writeKey: string + readKey: string +} + +const PLUGIN_TOKENS_KEY = "dribbbleTokens" +const isLocal = () => window.location.hostname.includes("localhost") +export const AUTH_URI = isLocal() ? "https://localhost:8787" : "https://oauth.fetch.tools/dribbble-plugin" + +class Auth { + storedTokens?: StoredTokens | null + + async authorize() { + const res = await fetch(`${AUTH_URI}/authorize`, { + method: "POST", + }) + + if (res.status !== 200) { + throw new Error("Failed to generate OAuth URL.") + } + + return (await res.json()) as Authorize + } + + async fetchTokens(readKey: string) { + const res = await fetch(`${AUTH_URI}/poll?readKey=${readKey}`, { + method: "POST", + }) + + if (res.status !== 200) { + throw new Error("Something went wrong polling for tokens.") + } + + const tokens = (await res.json()) as Tokens + + this.tokens.save(tokens) + return tokens + } + + isAuthenticated() { + const tokens = this.tokens.get() + if (!tokens) return false + + return true + } + + logout() { + this.tokens.clear() + } + + public readonly tokens = { + save: (tokens: Tokens) => { + const storedTokens: StoredTokens = { + createdAt: Date.now(), + accessToken: tokens.access_token, + scope: tokens.scope, + } + + this.storedTokens = storedTokens + window.localStorage.setItem(PLUGIN_TOKENS_KEY, JSON.stringify(storedTokens)) + + return storedTokens + }, + get: (): StoredTokens | null => { + if (this.storedTokens) return this.storedTokens + + const serializedTokens = window.localStorage.getItem(PLUGIN_TOKENS_KEY) + if (!serializedTokens) return null + + const storedTokens = JSON.parse(serializedTokens) as StoredTokens + + this.storedTokens = storedTokens + return storedTokens + }, + getOrThrow: (): StoredTokens => { + const tokens = this.tokens.get() + if (!tokens) { + throw new Error("Dribbble API token missing") + } + + return tokens + }, + clear: () => { + this.storedTokens = null + window.localStorage.removeItem(PLUGIN_TOKENS_KEY) + }, + } +} + +export default new Auth() diff --git a/plugins/dribbble/src/cms.ts b/plugins/dribbble/src/cms.ts new file mode 100644 index 000000000..3bc2d7cc8 --- /dev/null +++ b/plugins/dribbble/src/cms.ts @@ -0,0 +1,139 @@ +import { framer, ManagedCollectionField } from "framer-plugin" +import { useEffect } from "react" + +export const MAX_CMS_ITEMS = 10_000 +export const PLUGIN_LOG_SYNC_KEY = "dribbbleLogSyncResult" +export const FIELD_DELIMITER = "rfa4Emr21pUgs0in" +export const SLUG_COMPATIBLE_FIELDS = ["string", "formattedText"] + +export interface ItemResult { + fieldName?: string + message: string +} + +export interface SyncStatus { + errors: ItemResult[] + warnings: ItemResult[] + info: ItemResult[] +} + +export interface SyncResult extends SyncStatus { + status: "success" | "completed_with_errors" +} + +export type FieldsById = Map + +const isLoggingEnabled = () => { + return localStorage.getItem(PLUGIN_LOG_SYNC_KEY) === "true" +} + +export function logSyncResult(result: SyncResult, collectionItems?: Record[]) { + if (!isLoggingEnabled()) return + + if (collectionItems) { + console.table(collectionItems) + } + + if (result.errors.length > 0) { + console.log("Completed errors:") + console.table(result.errors) + } + + if (result.warnings.length > 0) { + console.log("Completed warnings:") + console.table(result.warnings) + } + + console.log("Completed info:") + console.table(result.info) +} + +export const useLoggingToggle = () => { + useEffect(() => { + const isLoggingEnabled = () => localStorage.getItem(PLUGIN_LOG_SYNC_KEY) === "true" + + const toggle = () => { + const newState = !isLoggingEnabled() + localStorage.setItem(PLUGIN_LOG_SYNC_KEY, newState ? "true" : "false") + framer.notify(`Logging ${newState ? "enabled" : "disabled"}`, { variant: "info" }) + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey && e.shiftKey && e.key === "L") { + e.preventDefault() + toggle() + } + } + + document.addEventListener("keydown", handleKeyDown) + + return () => { + document.removeEventListener("keydown", handleKeyDown) + } + }, []) +} + +// Match everything except for letters, numbers and parentheses. +const nonSlugCharactersRegExp = /[^\p{Letter}\p{Number}()]+/gu +// Match leading/trailing dashes, for trimming purposes. +const trimSlugRegExp = /^-+|-+$/gu + +export function getPossibleSlugFields(fields: ManagedCollectionField[]) { + return fields.filter(field => SLUG_COMPATIBLE_FIELDS.includes(field.type)) +} + +/** + * Takes a freeform string and removes all characters except letters, numbers, + * and parentheses. Also makes it lower case, and separates words by dashes. + * This makes the value URL safe. + */ +export function slugify(value: string): string { + return value.toLowerCase().replace(nonSlugCharactersRegExp, "-").replace(trimSlugRegExp, "") +} + +/** + * Generates an 8-character unique ID from a text using the djb2 hash function. + * Converts the 32-bit hash to an unsigned integer and then to a hex string. + */ +export function generateHash(text: string): string { + let hash = 5381 + for (let i = 0, len = text.length; i < len; i++) { + hash = (hash * 33) ^ text.charCodeAt(i) + } + // Convert to unsigned 32-bit integer + const unsignedHash = hash >>> 0 + return unsignedHash.toString(16).padStart(8, "0") +} + +/** + * Creates a consistent hash from an array of field IDs + */ +export function createFieldSetHash(fieldIds: string[]): string { + // Ensure consistent ordering + const sortedIds = [...fieldIds].sort() + return generateHash(sortedIds.join(FIELD_DELIMITER)) +} + +/** + * Processes a field set to determine the complementary fields + */ +export function computeFieldSets(params: { + currentFields: ManagedCollectionField[] + allPossibleFieldIds: string[] + storedHash: string +}) { + const { currentFields, allPossibleFieldIds, storedHash } = params + const currentFieldIds = currentFields.map(field => field.id) + + const includedFieldIds = currentFieldIds + + const excludedFieldIds = allPossibleFieldIds.filter(id => !currentFieldIds.includes(id)) + + const currentHash = createFieldSetHash(includedFieldIds) + + return { + includedFieldIds, + excludedFieldIds, + hasHashChanged: storedHash !== currentHash, + } +} diff --git a/plugins/dribbble/src/components/Button.tsx b/plugins/dribbble/src/components/Button.tsx new file mode 100644 index 000000000..313ef979b --- /dev/null +++ b/plugins/dribbble/src/components/Button.tsx @@ -0,0 +1,13 @@ +import cx from "classnames" +import { Spinner } from "./Spinner" + +interface Props extends React.ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "destructive" + isLoading?: boolean +} + +export const Button = ({ variant = "primary", children, className, isLoading = false, disabled, ...rest }: Props) => ( + +) diff --git a/plugins/dribbble/src/components/CenteredSpinner.tsx b/plugins/dribbble/src/components/CenteredSpinner.tsx new file mode 100644 index 000000000..1f2704655 --- /dev/null +++ b/plugins/dribbble/src/components/CenteredSpinner.tsx @@ -0,0 +1,8 @@ +import { Spinner, SpinnerProps } from "./Spinner" +import cx from "classnames" + +export const CenteredSpinner = ({ className, size }: { className?: string; size?: SpinnerProps["size"] }) => ( +
+ +
+) diff --git a/plugins/dribbble/src/components/CheckboxTextField.tsx b/plugins/dribbble/src/components/CheckboxTextField.tsx new file mode 100644 index 000000000..70c2f2ea7 --- /dev/null +++ b/plugins/dribbble/src/components/CheckboxTextField.tsx @@ -0,0 +1,45 @@ +import { useState } from "react" +import cx from "classnames" + +interface Props { + value: string + disabled: boolean + checked: boolean + onChange: () => void +} + +export function CheckboxTextfield({ value, disabled, checked: initialChecked, onChange }: Props) { + const [checked, setChecked] = useState(initialChecked) + + const toggle = () => { + if (disabled) return + + setChecked(!checked) + onChange() + } + + return ( +
+ + +
+ ) +} diff --git a/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx b/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx new file mode 100644 index 000000000..fe05b1053 --- /dev/null +++ b/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx @@ -0,0 +1,12 @@ +import { QueryErrorResetBoundary } from "@tanstack/react-query" + +export const ErrorBoundaryFallback = () => ( + + {({ reset }) => ( +
+ Something went wrong... + +
+ )} +
+) diff --git a/plugins/dribbble/src/components/FieldMapper.tsx b/plugins/dribbble/src/components/FieldMapper.tsx new file mode 100644 index 000000000..c72db8e27 --- /dev/null +++ b/plugins/dribbble/src/components/FieldMapper.tsx @@ -0,0 +1,111 @@ +import { CheckboxTextfield } from "../components/CheckboxTextField" +import { IconChevron } from "../components/Icons" +import { ScrollFadeContainer } from "../components/ScrollFadeContainer" +import { assert } from "../utils" +import { ManagedCollectionField } from "framer-plugin" +import { Fragment, useMemo } from "react" +import cx from "classnames" + +export interface ManagedCollectionFieldConfig { + field: ManagedCollectionField | null + originalFieldName: string +} + +interface FieldMapperProps { + collectionFieldConfig: ManagedCollectionFieldConfig[] + fieldNameOverrides: Record + isFieldSelected: (fieldId: string) => boolean + onFieldToggle: (fieldId: string) => void + onFieldNameChange: (fieldId: string, value: string) => void + fromLabel?: string + toLabel?: string + className?: string + height?: number +} +const getInitialSortedFields = ( + fields: ManagedCollectionFieldConfig[], + isFieldSelected: (fieldId: string) => boolean +): ManagedCollectionFieldConfig[] => { + return [...fields].sort((a, b) => { + const aIsSelected = a.field && isFieldSelected(a.field.id) + const bIsSelected = b.field && isFieldSelected(b.field.id) + + // Sort based on whether the fields are selected + if (aIsSelected && !bIsSelected) return -1 + if (!aIsSelected && bIsSelected) return 1 + + // Then sort by whether they are supported fields + if (a.field && !b.field) return -1 + if (!a.field && b.field) return 1 + + return 0 + }) +} + +export const FieldMapper = ({ + collectionFieldConfig, + fieldNameOverrides, + isFieldSelected, + onFieldToggle, + onFieldNameChange, + fromLabel = "Column", + toLabel = "Field", + height, + className, +}: FieldMapperProps) => { + // We only want to sort on initial render + const sortedCollectionFieldConfig = useMemo( + () => getInitialSortedFields(collectionFieldConfig, isFieldSelected), + // eslint-disable-next-line react-hooks/exhaustive-deps + [collectionFieldConfig] + ) + + return ( + +
+ {fromLabel} + {toLabel} + {sortedCollectionFieldConfig.map((fieldConfig, i) => { + const isUnsupported = !fieldConfig.field + const isSelected = fieldConfig.field ? isFieldSelected(fieldConfig.field.id) : false + + return ( + + { + assert(fieldConfig.field) + onFieldToggle(fieldConfig.field.id) + }} + /> +
+ +
+ { + assert(fieldConfig.field) + onFieldNameChange(fieldConfig.field.id, e.target.value) + }} + /> +
+ ) + })} +
+
+ ) +} diff --git a/plugins/dribbble/src/components/Icons.tsx b/plugins/dribbble/src/components/Icons.tsx new file mode 100644 index 000000000..3bb1e087e --- /dev/null +++ b/plugins/dribbble/src/components/Icons.tsx @@ -0,0 +1,12 @@ +import cx from "classnames" + +export const IconChevron = ({ className }: { className?: string }) => ( + + + +) diff --git a/plugins/dribbble/src/components/ScrollFadeContainer.tsx b/plugins/dribbble/src/components/ScrollFadeContainer.tsx new file mode 100644 index 000000000..8ce83ae2a --- /dev/null +++ b/plugins/dribbble/src/components/ScrollFadeContainer.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useRef, useState, useCallback } from "react" + +interface Props { + children: React.ReactNode + className?: string + fadeHeight?: number + height: number +} + +export const ScrollFadeContainer = ({ children, className, fadeHeight = 45, height }: Props) => { + const [showTopFade, setShowTopFade] = useState(false) + const [showBottomFade, setShowBottomFade] = useState(false) + const containerRef = useRef(null) + const scrollTimeout = useRef() + + const checkScroll = useCallback(() => { + const element = containerRef.current + if (!element) return + + if (scrollTimeout.current) { + clearTimeout(scrollTimeout.current) + } + + scrollTimeout.current = setTimeout(() => { + requestAnimationFrame(() => { + if (!element) return + const { scrollTop, scrollHeight, clientHeight } = element + const scrollBottom = scrollHeight - scrollTop - clientHeight + + setShowTopFade(scrollTop > 10) + setShowBottomFade(scrollBottom > 10) + }) + }, 50) + }, []) + + useEffect(() => { + const element = containerRef.current + if (!element) return + + requestAnimationFrame(() => { + if (!element) return + const { scrollHeight, clientHeight } = element + setShowBottomFade(scrollHeight > clientHeight) + checkScroll() + }) + + element.addEventListener("scroll", checkScroll, { passive: true }) + + return () => { + if (scrollTimeout.current) { + clearTimeout(scrollTimeout.current) + } + + element.removeEventListener("scroll", checkScroll) + } + }, [checkScroll]) + + return ( +
+
+ {children} +
+
+
+
+ ) +} diff --git a/plugins/dribbble/src/components/Spinner.tsx b/plugins/dribbble/src/components/Spinner.tsx new file mode 100644 index 000000000..594ad9d98 --- /dev/null +++ b/plugins/dribbble/src/components/Spinner.tsx @@ -0,0 +1,40 @@ +import cx from "classnames" +import styles from "./spinner.module.css" + +export interface SpinnerProps { + /** Size of the spinner */ + size?: "normal" | "medium" | "large" + /** Set the spinner to have a static position inline with other content */ + inline?: boolean + className?: string + inheritColor?: boolean +} + +function styleForSize(size: SpinnerProps["size"]) { + switch (size) { + case "normal": + return styles.normalStyle + case "medium": + return styles.mediumStyle + case "large": + return styles.largeStyle + } +} + +function spinnerClassNames(size: SpinnerProps["size"] = "normal") { + return cx(styles.spin, styles.baseStyle, styleForSize(size)) +} + +export const Spinner = ({ size, inline = false, inheritColor, className, ...rest }: SpinnerProps) => { + return ( +
+ ) +} diff --git a/plugins/dribbble/src/components/spinner.module.css b/plugins/dribbble/src/components/spinner.module.css new file mode 100644 index 000000000..a0d1a7ac3 --- /dev/null +++ b/plugins/dribbble/src/components/spinner.module.css @@ -0,0 +1,60 @@ +.baseStyle { + --spinner-translate: 0; + background-color: #fff; +} + +.buttonWithDepthSpinner { + background-color: currentColor; +} + +.normalStyle { + width: 12px; + height: 12px; + -webkit-mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC"); + mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC"); + -webkit-mask-size: 12px; + mask-size: 12px; +} + +.mediumStyle { + width: 24px; + height: 24px; + -webkit-mask: url("data:image/png;base64,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"); + mask: url("data:image/png;base64,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"); + -webkit-mask-size: 24px; + mask-size: 24px; +} + +.largeStyle { + width: 30px; + height: 30px; + -webkit-mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAABGdBTUEAALGPC/xhBQAAC+5JREFUaAXVm2tsVMcVx/furvHaBr9DcUwpSSGiViolUCUxKYVGfVC+VLQFVVGRSqWWqiiU97ME+MAbgfoBVfQLUvIhErRSojZ9SKhBVA1qqqiBpmrj4JS2LuZR2xi/jXe3v//xndu92BivWYN3pPE875nzn/+ZM3Pnrr3IOIXdu3cniouL6xA/e2Bg4OPxeHx6Op2uJE6mrpgYI/ZEo9Fu0rZkMtnked6/Y7HYB9XV1RdWrlzZS33Og5dLiceOHSvv7+//HDLnp1KpOQCIkwqYB5AIYDWeygoxlQGsvAfgGP0jRNUnqf8rz54tLCz87fr161vVKRchJ4CPHDlSh8JLUXQuSklxD2XROSqQBkx1AkgIAVaFgiaGRJii6sez1t8H+TZ1r27btu1dvzzm5L4A792790kU+yaji01RJXlOUQPsGBaoewHWsxn9jHrqMifsAuXjAP8T6ZjCmADv37+/AhP9FgCeJzrGQsyoXgxnAxiwAmFyyNsEOvlinDazHNLflJSUHF6zZs0NPZBNyBowrD7F4C8R5Xj0vCmoVANLUZS0erGVDeDBx8OWIMCSQzDxvjyNI2e3aevWrefUMNqQFeBDhw4tYa2+iPCoNBAwBQ0mxVRmIgKGyPbjoS9iDRfp0kS8UlRU1Ikj6tEzbW1txTw+hX619JtJ1TzyzyInIXnqQ7vJHczamBrbxlM7/Q9t2bLlpPKjCaMCfOLEiYKWlpbvMNBnUYQxzFQNtPIC6hRk0F7y7wDyHcD9GbPrG40irs/JkycTV69efY7yIuQsQn4Zeck3oOQDC1K9IuF1tsCXRzPWPQGzn5bCyA8ROssBdSlKaDBjmroByr+cNGnSG2wjxiDK3Fc4fPhwCTK/jZAVWJYtIYZzPsPA0uYm4AJ9V23fvr1lpEFHBCxmMbstCJoFYyT/Z5K1BEZjWmb8Fm0/w3u2jTTYWNv27dv3COP/gDG/hoxJRJtnpYzrJkAW8B5MvzgS0/GRlGhvb1+BwMeIjJWScI0kpPYYadvt27d/zKw2jiTnftuQL2+8B8Z/DtPHyX+MaF5cyH35YnpeT0/PXsob/bohyaDmQ6ojERzUlxBQryZkpl0XQKaoly03FhQUvLxz585xBevGVbpp06b3YXoZY8sJOqBqsryIQLVl6P49VQ4XhgXMTD4p8xGtPGQRQQJqVaTnmcl969atuzmc0PGsA/R1nOEKxvgFUfprrTmzFmAx/SPOCi8Mp0fmLFn7gQMHynhgD7NVSBpaI+rADP9x8+bNPx1O2IOuQ9ejjLkUxqWnw+JI7MT8F+3YseNapl6uMagD6NcpFIrcQYIHGUamDvSXysrKRr3nBULHKdPb27sdvd6TeAALS2Ys5w1tN3WhEAKMN3yCWZlHDzNdv2dKE0i8yZbzk1WrVt0OSXiIBbbMXg4s3wfsdfQLNHFsA2Ip6/n5oIFMCDDmuoQ6eeEU+SQzNKDOCEhTPv4w1qzGHyn4JvtdgZSa9DWWNQGK1G3NfD4AfPTo0Vk0PKZG35Qt5QGx+/uNGzf+M/PBiZTnPH0BHV9D1QCPn9ckPMtan+/0DTrw4v55gGr7CTyz8rCtm4c33QMTNU0kEkewwh6Ao7K9SzvvLdZfcnob4IMHD06hYo4qZc48JFYtUv4dM9juHpio6dq1a6/B5glipseWukDxvqDTmgoGmE5P+y34rGREkboUaTfhjNryIQBMp7BOol4pddQUPr2iFkDcN4TBmfSnVVCgk16HZNZi+y/yhMrnQ+A1sQP9f02Uy1a0N6zBYuSrwhAFkA7j06lMZkYYTrImdITLq4DevyI6ImWp5rlJn+FlqDg6ZcqUmSAKOmSgG+jq6mrIKOdFtqKi4i0UlVWaOWcoXXDz5s36KBt3LejtkIyt67ycVKTj32G/P+OBvMhyMOrGUs+BIa6I0mJYa1rp3CgYq2kIBSGjwyWleRrOobdbx5YCUOkTcRxTFTEJ0ySBZWs/zvpGcKJMDmQ1EBXiRO3HIlBWO0e0TwaseWc1uDB58uT/uny+pWynH0GezFn4hE3OS+DrVFFAg9ZuCBcv9zm5lwoJfUAFsLSJWTecsAk0oSzO1lMgX+UaXdrc3Jx3DsvpzjG5A1yGUHU+w0pjuh0YNriH8zHdtWuXvcICTKYcigLch73rdBWKNTU1OpDkZYBJkdgsZjPYFfBmueUeMrb3ZqYcOhJ5iRalwaFXpled/gKtgJm/Is/VSQfd7odCX19fJRU5+y4bEv4ACrz07OKVUSPpwk9Mv8IHhV3y0tp+akhDjgsvXaGO+Rj27NkjD53izmsnqWKEJZrGEdvRq4V9S1c4Ol6qTbORxtN9guyYv8OaoIf0B3De+fPn9Z4QIrGuri4VBWgzd1eGVKAVpScMz9Cnloek85iHxVA9/E984cKFkblz53pVVVXe7NmzJS+6bNmydLy0tLSptbVVtxxybTYQ2QinrxifWmZS8aFV5skf35yDPRgMEaLH9bLApaO6doXVJszZAqDNYwNaZ89P5gnOQE0IlFXG+eRqEaAxReqNU3tbwIUPee/Vvkx4/NSpU3mzH6Ovh++ZxGcgb/r06R5fEgOm+YmEXTkbYBb332B5QKwC1CImLcAFDQ0Nnwmmb4JnTp8+ncAzG0iWaVTAUVkYo3hoWezgTYdemgH6D0Cb8wK4nbrUgbqn9GFa+YkcuKyINjU1FbHXeuXl5UYktx9SOQq7SdoH7+kcCBbwu0Q5q7TPrlvLuvV7xvWbqClfFEtgN8aBySJrNsqnIXtjwmsHP7uwmRAIbvyaYPmKWNb6dcB0ZctH70/B8lRXN9FSbZ944mLWb7BmnY5MhNi19au6ALAKrNnzJKI+iOzR+sakG80vT0TTxqnGsMAKzFim7HF+iCmKbUXWccCuMIYA+yx/KFa1jgVcZq4Ay0Xkv8hs6SZhQgR5Zbafylu3bplOYhg9jWXlubXpQ9/Qe30IsFAA9A8w2ivALspd0yTw1bj60OfHh4mcQ0YVABPEGAAtol8EJ6WTojdt2rSuO/UbYvPqwNe2GWBc4lhWnQ9aXyN0Qd/Y2dn5dubaUJ8HFRhXDqmKWOycLEBTvCGJMDNJttrW4X7NY17sTkXPnDnTvnjx4n5A1kqAz7SlsCy2ywFes2DBgv+cPXvWbhfulDFeZS0pwqOATZBG8MAentnDOWmHMQJht2PDhg36aeKQMMSkXQ9+PHIRwME5WqDdPu33qaK8hK9yVe6Z8U71Kz3GmMG4hQKHU4qy70a037q1W1lZ2a1vTHfTZViTdp3lAS9fvvwVgD8iZrVdyYQc4+rHTOtIpmvR93P1Czw3vku17TCu1mtZR0eHDkPSwUwXlrWLpPWSwDru48evV6Wfe/bOdETA6qxZ5Zj2AsKmCjAeXM5Lgyo11lVPuE3awIb/Qa5+B+JPuCa7Cib1+TaN7zDAAuqDMeDcbvTU1tZeX758uau/E6uV7wlYvTTwpUuXniP7uJtZB1jtQkuwgVBEH7KuwEYz8Ua2jk1jNTY2lrIOS2G1lInW56A0gIxJyvr9iSbeWBRwdOrgJxk3RmJWeiqMCvBg10iE9VqH0Ke1T2sgn3Hbs0W3+qGMvtmkAWuTQL9rlFsodwKiGwvodwd5biZielnH6YAnkQBMKSL0awTtgdoN0qxT3cSkeS5Je1CvvNpxVtez+bFNVoClCD+PeJRkPmh0H2bKCCDRGJbJqww7+pohhbQGbAJULyBKhUkKq0zfCCbr+lk9bQFgsSk5AikL03PUJXnPvbJ69Wp98R91uKuXvpsEPOAVmHoTJf+lPr7y1v1u+bvJulc9mIwQHKN+hhHkea6jvr7+o2zBarysGc5UUi8UAK+jTluTMStTFnCiSNCJx5gT26p3DIv14RjmmaBe7ZKrvqpHdjenqGa2zCEnqEy9RsrfF2AnWL+QAdgsylNZj8Z6LgBLvkwbc27nF/lygFmZr9MvM80JYCcQhfTfaLWUpxHLYERvL1kxzDOyCm1BtzCKNv5LrTVX25z0zClgCXTB38oq8dKlmKT+maOIqB+t6r/UbOOm7wDA+qnvZ53KTHt48+ni/rjjXvupGyfb9H8EoZWUgTuYFQAAAABJRU5ErkJggg=="); + mask: url("data:image/png;base64,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"); + -webkit-mask-size: 30px; + mask-size: 30px; +} + +.centeredStyle { + --spinner-translate: -50%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(var(--spinner-translate), var(--spinner-translate)); +} + +.spin { + animation-duration: 800ms; + animation-iteration-count: infinite; + animation-name: spin; + animation-timing-function: linear; +} + +@keyframes spin { + 0% { + transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(0deg); + } + + 100% { + transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(360deg); + } +} diff --git a/plugins/dribbble/src/constants.ts b/plugins/dribbble/src/constants.ts new file mode 100644 index 000000000..82f8841aa --- /dev/null +++ b/plugins/dribbble/src/constants.ts @@ -0,0 +1,66 @@ +import { ManagedCollectionField } from "framer-plugin" + +export const SHOT_FIELDS: ManagedCollectionField[] = [ + { + id: "id", + name: "Id", + type: "number", + userEditable: false, + }, + { + id: "title", + name: "Title", + type: "string", + userEditable: false, + }, + { + id: "images.hidpi", + name: "Image", + type: "image", + userEditable: false, + }, + { + id: "low_profile", + name: "Low Profile", + type: "boolean", + userEditable: false, + }, + { + id: "html_url", + name: "URL", + type: "link", + userEditable: false, + }, + { + id: "published_at", + name: "Published At", + type: "date", + userEditable: false, + }, + { + id: "updated_at", + name: "Updated At", + type: "date", + userEditable: false, + }, + { + id: "animated", + name: "Animated", + type: "boolean", + userEditable: false, + }, + // These fields SHOULD work but the Dribbble API + // returns null for some reason + // { + // id: "description", + // name: "Description", + // type: "formattedText", + // userEditable: false, + // }, + // { + // id: "video.url", + // name: "Video", + // type: "image", + // userEditable: false, + // }, +] diff --git a/plugins/dribbble/src/globals.css b/plugins/dribbble/src/globals.css new file mode 100644 index 000000000..8cbe1cc82 --- /dev/null +++ b/plugins/dribbble/src/globals.css @@ -0,0 +1,64 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + h6 { + @apply font-semibold text-primary leading-[1.2]; + } +} + +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} + +@layer components { + .row { + display: flex; + flex-direction: row; + gap: 10px; + } + + .col { + display: flex; + flex-direction: column; + gap: 10px; + } + + .row-lg { + display: flex; + flex-direction: row; + gap: 15px; + } + + .col-lg { + display: flex; + flex-direction: column; + gap: 15px; + } +} + +body, +html, +#root { + width: 100%; + height: 100%; + overflow: hidden; +} + +main { + width: 100%; + height: 100%; + padding: 15px; + display: flex; + flex-direction: column; + gap: 15px; +} diff --git a/plugins/dribbble/src/main.tsx b/plugins/dribbble/src/main.tsx new file mode 100644 index 000000000..49bb6da58 --- /dev/null +++ b/plugins/dribbble/src/main.tsx @@ -0,0 +1,64 @@ +import "./globals.css" +import "framer-plugin/framer.css" + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import React, { Suspense } from "react" +import ReactDOM from "react-dom/client" +import { App } from "./App.tsx" +import { framer } from "framer-plugin" +import { ErrorBoundary } from "react-error-boundary" +import { CenteredSpinner } from "./components/CenteredSpinner.tsx" +import { ErrorBoundaryFallback } from "./components/ErrorBoundaryFallback.tsx" +import { getPluginContext, shouldSyncImmediately, syncShots } from "./sync.ts" +import { assert } from "./utils.ts" + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 1000 * 60 * 5, + refetchOnWindowFocus: false, + throwOnError: true, + }, + }, +}) + +function renderPlugin(app: React.ReactNode) { + const root = document.getElementById("root") + if (!root) throw new Error("Root element not found") + + ReactDOM.createRoot(root).render( + + + + }>{app} + + + + ) +} + +async function runPlugin() { + const mode = framer.mode + + try { + const pluginContext = await getPluginContext() + + if (mode === "syncManagedCollection" && shouldSyncImmediately(pluginContext)) { + assert(pluginContext.slugFieldId) + return syncShots({ + fields: pluginContext.collectionFields, + slugFieldId: pluginContext.slugFieldId, + includedFieldIds: pluginContext.includedFieldIds, + }).then(() => framer.closePlugin()) + } + renderPlugin() + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + framer.closePlugin("An unexpected error ocurred: " + message, { + variant: "error", + }) + } +} + +runPlugin() diff --git a/plugins/dribbble/src/pages/Authenticate.tsx b/plugins/dribbble/src/pages/Authenticate.tsx new file mode 100644 index 000000000..16f835595 --- /dev/null +++ b/plugins/dribbble/src/pages/Authenticate.tsx @@ -0,0 +1,65 @@ +import { useRef, useState } from "react" +import auth from "../auth" +import { Button } from "../components/Button" +import { getPluginContext, PluginContext } from "../sync" +import { framer } from "framer-plugin" + +interface Props { + onAuthenticated: (context: PluginContext) => void +} + +export default function Authenticate({ onAuthenticated }: Props) { + const [isLoading, setIsLoading] = useState(false) + const pollInterval = useRef>() + + const pollForTokens = (readKey: string) => { + if (pollInterval.current) { + clearInterval(pollInterval.current) + } + + return new Promise( + resolve => + (pollInterval.current = setInterval( + () => + auth.fetchTokens(readKey).then(tokens => { + clearInterval(pollInterval.current) + resolve(tokens) + }), + 1500 + )) + ) + } + + const login = async () => { + setIsLoading(true) + + try { + const authorization = await auth.authorize() + window.open(authorization.url) + await pollForTokens(authorization.readKey) + + onAuthenticated(await getPluginContext()) + } catch (e) { + framer.notify(e instanceof Error ? e.message : JSON.stringify(e), { variant: "error" }) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+ Dribbble Logo +
+
Connect to Dribbble
+

+ Sign in to Dribbble to sync shots and import your content. +

+
+
+ +
+ ) +} diff --git a/plugins/dribbble/src/pages/MapFields.tsx b/plugins/dribbble/src/pages/MapFields.tsx new file mode 100644 index 000000000..5f82987d4 --- /dev/null +++ b/plugins/dribbble/src/pages/MapFields.tsx @@ -0,0 +1,115 @@ +import { useState } from "react" +import { Button } from "../components/Button" +import { SHOT_FIELDS } from "../constants" +import { assert, isDefined } from "../utils" +import { getPossibleSlugFields, useLoggingToggle } from "../cms" +import { FieldMapper } from "../components/FieldMapper" +import { PluginContext, SyncShotsMutationOptions } from "../sync" +import { ManagedCollectionField } from "framer-plugin" + +interface Props { + context: PluginContext + onSubmit: (opts: SyncShotsMutationOptions) => void + isLoading: boolean +} + +const getInitialSlugFieldId = (context: PluginContext, slugFields: ManagedCollectionField[]): string | null => { + if (context.type === "update" && context.slugFieldId) return context.slugFieldId + + return slugFields.length > 0 ? slugFields[0].id : null +} + +export default function MapFields({ context, onSubmit, isLoading }: Props) { + useLoggingToggle() + + const [includedFieldIds, setIncludedFieldIds] = useState>( + () => new Set(context.type === "update" ? context.includedFieldIds : SHOT_FIELDS.map(field => field.id)) + ) + + const slugFields = getPossibleSlugFields(SHOT_FIELDS).filter(field => includedFieldIds.has(field.id)) + const [slugFieldId, setSlugFieldId] = useState(() => getInitialSlugFieldId(context, slugFields)) + + const [collectionFieldConfig] = useState( + SHOT_FIELDS.map(field => ({ + field, + originalFieldName: field.name, + })) + ) + const [fieldNameOverrides, setFieldNameOverrides] = useState>(() => + context.type === "update" + ? Object.fromEntries(context.collectionFields.map(field => [field.id, field.name])) + : {} + ) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + assert(slugFieldId) + + const allFields = collectionFieldConfig + .filter(fieldConfig => fieldConfig.field && includedFieldIds.has(fieldConfig.field.id)) + .map(fieldConfig => fieldConfig.field) + .filter(isDefined) + .map(field => { + // Create copy to prevent showing overriden name temporarily in the UI + const fieldCopy = { ...field } + if (fieldNameOverrides[field.id]) { + fieldCopy.name = fieldNameOverrides[field.id] + } + + return field + }) + + onSubmit({ includedFieldIds: Array.from(includedFieldIds), fields: allFields, slugFieldId }) + } + + return ( +
+
+ + +
+ includedFieldIds.has(fieldId)} + onFieldToggle={fieldId => { + setIncludedFieldIds(current => { + const nextSet = new Set(current) + if (nextSet.has(fieldId)) { + nextSet.delete(fieldId) + } else { + nextSet.add(fieldId) + } + return nextSet + }) + }} + onFieldNameChange={(fieldId, value) => { + setFieldNameOverrides(current => ({ + ...current, + [fieldId]: value, + })) + }} + /> +
+ +
+ + ) +} diff --git a/plugins/dribbble/src/sync.ts b/plugins/dribbble/src/sync.ts new file mode 100644 index 000000000..88c78c5c5 --- /dev/null +++ b/plugins/dribbble/src/sync.ts @@ -0,0 +1,227 @@ +import { framer, ManagedCollection, ManagedCollectionField } from "framer-plugin" +import { useMutation } from "@tanstack/react-query" +import { fetchAllShots, Shot } from "./api" +import { isDefined } from "./utils" +import { SHOT_FIELDS } from "./constants" +import { + computeFieldSets, + createFieldSetHash, + FieldsById, + logSyncResult, + MAX_CMS_ITEMS, + slugify, + SyncResult, + SyncStatus, +} from "./cms" +import auth from "./auth" + +const PLUGIN_INCLUDED_FIELDS_HASH = "dribbblePluginShotIncludedFieldsHash" +const PLUGIN_SLUG_FIELD_ID_KEY = "dribbblePluginSlugFieldId" + +export interface SyncShotsMutationOptions { + fields: ManagedCollectionField[] + includedFieldIds: string[] + slugFieldId: string +} + +export interface ProcessShotsParams { + fields: ManagedCollectionField[] + fieldsById: FieldsById + slugFieldId: string + unsyncedItemIds: Set +} + +export interface ProcessShotParams extends ProcessShotsParams { + shot: Shot + status: SyncStatus +} + +export interface PluginContextNew { + type: "new" + collection: ManagedCollection + isAuthenticated: boolean +} + +export interface PluginContextUpdate { + type: "update" + collection: ManagedCollection + collectionFields: ManagedCollectionField[] + includedFieldIds: string[] + slugFieldId: string + isAuthenticated: boolean +} + +export type PluginContext = PluginContextNew | PluginContextUpdate + +export function shouldSyncImmediately(pluginContext: PluginContext): pluginContext is PluginContextUpdate { + if (pluginContext.type !== "update") return false + + return true +} + +export async function getPluginContext(): Promise { + const collection = await framer.getManagedCollection() + const collectionFields = await collection.getFields() + + const isAuthenticated = auth.isAuthenticated() + const allPossibleFieldIds = SHOT_FIELDS.map(field => field.id) + + const [slugFieldId, rawIncludedFieldHash] = await Promise.all([ + collection.getPluginData(PLUGIN_SLUG_FIELD_ID_KEY), + collection.getPluginData(PLUGIN_INCLUDED_FIELDS_HASH), + ]) + + if (!rawIncludedFieldHash || !slugFieldId) { + return { + type: "new", + collection, + isAuthenticated, + } + } + + const { includedFieldIds } = computeFieldSets({ + currentFields: collectionFields, + allPossibleFieldIds, + storedHash: rawIncludedFieldHash, + }) + + return { + type: "update", + collection, + collectionFields, + includedFieldIds, + isAuthenticated, + slugFieldId, + } +} + +// eslint-disable-next-line +function isShotPropertyValueMissing(value: any): boolean { + // Usual suspects + if (value === null || value === undefined || value === "") { + return true + } + + // Empty array + if (Array.isArray(value)) { + return value.length === 0 + } + + // Empty object + if (typeof value === "object") { + return Object.keys(value).length === 0 + } + + return false +} + +function processShot({ shot, slugFieldId, fieldsById, unsyncedItemIds, status }: ProcessShotParams) { + let slugValue: string | null = null + const fieldData: Record = {} + const fieldId = String(shot.id) + unsyncedItemIds.delete(fieldId) + + for (const [propertyName, field] of fieldsById) { + // Allow for nested dot notation to access fields e.g. images.hidpi + let value: unknown + if (propertyName.includes(".")) { + value = propertyName.split(".").reduce((obj: unknown, key) => { + if (obj && typeof obj === "object") { + return (obj as Record)[key] + } + return undefined + }, shot) + } else { + value = shot[propertyName as keyof Shot] + } + + if (field.id === slugFieldId && typeof value === "string") { + slugValue = slugify(value) + } + + if (isShotPropertyValueMissing(value)) { + status.warnings.push({ + fieldName: propertyName, + message: `Value is missing for field ${field.name}`, + }) + } + + fieldData[propertyName] = value + } + + if (!slugValue) { + status.warnings.push({ message: "Slug missing. Skipping item." }) + return null + } + + return { id: fieldId, slug: slugValue, fieldData } +} + +function processShots(shots: Shot[], processShotsParams: ProcessShotsParams) { + const seenItemIds = new Set() + const status: SyncStatus = { + info: [], + warnings: [], + errors: [], + } + + const collectionItems = shots.map(shot => processShot({ shot, status, ...processShotsParams })).filter(isDefined) + + return { + collectionItems, + status, + seenItemIds, + } +} + +export async function syncShots({ fields, slugFieldId, includedFieldIds }: SyncShotsMutationOptions) { + const collection = await framer.getManagedCollection() + await collection.setFields(fields) + + const fieldsById = new Map(fields.map(field => [field.id, field])) + const unsyncedItemIds = new Set(await collection.getItemIds()) + + const allShots = await fetchAllShots(MAX_CMS_ITEMS) + + const { collectionItems, status } = processShots(allShots, { + fields, + fieldsById, + unsyncedItemIds, + slugFieldId, + }) + + await collection.addItems(collectionItems) + + const itemsToDelete = Array.from(unsyncedItemIds) + await collection.removeItems(itemsToDelete) + + await Promise.all([ + await collection.setPluginData(PLUGIN_INCLUDED_FIELDS_HASH, createFieldSetHash(includedFieldIds)), + await collection.setPluginData(PLUGIN_SLUG_FIELD_ID_KEY, slugFieldId), + ]) + + const result: SyncResult = { + status: status.errors.length === 0 ? "success" : "completed_with_errors", + errors: status.errors, + info: status.info, + warnings: status.warnings, + } + + logSyncResult(result, collectionItems) + + return result +} + +export const useSyncShotsMutation = ({ + onSuccess, + onError, +}: { + onSuccess?: (result: SyncResult) => void + onError?: (e: Error) => void +}) => { + return useMutation({ + mutationFn: (args: SyncShotsMutationOptions) => syncShots(args), + onSuccess, + onError, + }) +} diff --git a/plugins/dribbble/src/utils.ts b/plugins/dribbble/src/utils.ts new file mode 100644 index 000000000..a1e224f70 --- /dev/null +++ b/plugins/dribbble/src/utils.ts @@ -0,0 +1,25 @@ +export function assert(condition: unknown, ...msg: unknown[]): asserts condition { + if (condition) return + + const e = Error("Assertion Error" + (msg.length > 0 ? ": " + msg.join(" ") : "")) + // Hack the stack so the assert call itself disappears. Works in jest and in chrome. + if (e.stack) { + try { + const lines = e.stack.split("\n") + if (lines[1]?.includes("assert")) { + lines.splice(1, 1) + e.stack = lines.join("\n") + } else if (lines[0]?.includes("assert")) { + lines.splice(0, 1) + e.stack = lines.join("\n") + } + } catch { + // nothing + } + } + throw e +} + +export function isDefined(value: T): value is NonNullable { + return value !== undefined && value !== null +} diff --git a/plugins/dribbble/src/vite-env.d.ts b/plugins/dribbble/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/plugins/dribbble/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/plugins/dribbble/tailwind.config.js b/plugins/dribbble/tailwind.config.js new file mode 100644 index 000000000..6bad2ded5 --- /dev/null +++ b/plugins/dribbble/tailwind.config.js @@ -0,0 +1,45 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class", "[data-framer-theme='dark']"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + backgroundColor: { + primary: "var(--framer-color-bg)", + secondary: "var(--framer-color-bg-secondary)", + tertiary: "var(--framer-color-bg-tertiary)", + tertiaryDimmedLight: "rgba(243, 243, 243, 0.75)", + tertiaryDimmedDark: "rgba(43, 43, 43, 0.75)", + divider: "var(--framer-color-divider)", + tint: "var(--framer-color-tint)", + tintDimmed: "var(--framer-color-tint-dimmed)", + tintDark: "var(--framer-color-tint-dark)", + blackDimmed: "rgba(0, 0, 0, 0.5)", + "dribbble-primary": "#EA4C89", + "framer-red": "#FF3366", + "framer-blue": "#0099FF", + }, + colors: { + primary: "var(--framer-color-text)", + secondary: "var(--framer-color-text-secondary)", + tertiary: "var(--framer-color-text-tertiary)", + inverted: "var(--framer-color-text-inverted)", + tint: "var(--framer-color-tint)", + "dribbble-primary": "#EA4C89", + "framer-red": "#FF3366", + }, + borderColor: { + divider: "var(--framer-color-divider)", + }, + fontSize: { + "2xs": "10px", + }, + padding: { + 15: "15px", + }, + gridTemplateColumns: { + fieldPicker: "1fr 8px 1fr", + }, + }, + }, +} diff --git a/plugins/dribbble/tsconfig.json b/plugins/dribbble/tsconfig.json new file mode 100644 index 000000000..325606c67 --- /dev/null +++ b/plugins/dribbble/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ES2022", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/plugins/dribbble/vite.config.ts b/plugins/dribbble/vite.config.ts new file mode 100644 index 000000000..7efb6ef85 --- /dev/null +++ b/plugins/dribbble/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react-swc" +import mkcert from "vite-plugin-mkcert" +import framer from "vite-plugin-framer" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), mkcert(), framer()], + build: { + target: "ES2022", + }, +})