From ad00e54df88d7e1c58021045f268f8c7e6aad8ae Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 1 Apr 2025 11:18:32 +0200 Subject: [PATCH 01/19] Migrate NotFound component to tailwind --- package-lock.json | 774 ++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/common/MainHeader.tsx | 2 +- src/common/NotFound.tsx | 9 +- src/index.tsx | 1 + src/tailwind.css | 3 + vite.config.ts | 3 +- 7 files changed, 787 insertions(+), 7 deletions(-) create mode 100644 src/tailwind.css diff --git a/package-lock.json b/package-lock.json index 4e7cee28..7eb8ac20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~3.5.0", "@stylistic/eslint-plugin": "^4.2.0", + "@tailwindcss/vite": "^4.0.17", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -62,6 +63,7 @@ "history": "^5.3.0", "jsdom": "^26.0.0", "sass": "^1.86.3", + "tailwindcss": "^4.0.17", "typescript": "^5.8.3", "typescript-eslint": "^8.29.0", "vite": "^6.2.5", @@ -3532,6 +3534,244 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/@tailwindcss/node": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.17.tgz", + "integrity": "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.17.tgz", + "integrity": "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-x64": "4.0.17", + "@tailwindcss/oxide-freebsd-x64": "4.0.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-x64-musl": "4.0.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz", + "integrity": "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz", + "integrity": "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz", + "integrity": "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz", + "integrity": "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz", + "integrity": "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz", + "integrity": "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz", + "integrity": "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz", + "integrity": "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz", + "integrity": "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz", + "integrity": "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz", + "integrity": "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.17.tgz", + "integrity": "sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.0.17", + "@tailwindcss/oxide": "4.0.17", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, "node_modules/@testing-library/dom": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", @@ -5400,6 +5640,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7339,6 +7593,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7533,6 +7797,255 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -9308,6 +9821,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwindcss": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.17.tgz", + "integrity": "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -12847,6 +13377,125 @@ } } }, + "@tailwindcss/node": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.17.tgz", + "integrity": "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==", + "dev": true, + "requires": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.17" + } + }, + "@tailwindcss/oxide": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.17.tgz", + "integrity": "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==", + "dev": true, + "requires": { + "@tailwindcss/oxide-android-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-arm64": "4.0.17", + "@tailwindcss/oxide-darwin-x64": "4.0.17", + "@tailwindcss/oxide-freebsd-x64": "4.0.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.17", + "@tailwindcss/oxide-linux-x64-musl": "4.0.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.17" + } + }, + "@tailwindcss/oxide-android-arm64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz", + "integrity": "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz", + "integrity": "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-darwin-x64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz", + "integrity": "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz", + "integrity": "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz", + "integrity": "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz", + "integrity": "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz", + "integrity": "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz", + "integrity": "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz", + "integrity": "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz", + "integrity": "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz", + "integrity": "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==", + "dev": true, + "optional": true + }, + "@tailwindcss/vite": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.17.tgz", + "integrity": "sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww==", + "dev": true, + "requires": { + "@tailwindcss/node": "4.0.17", + "@tailwindcss/oxide": "4.0.17", + "lightningcss": "1.29.2", + "tailwindcss": "4.0.17" + } + }, "@testing-library/dom": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", @@ -14130,6 +14779,16 @@ "version": "8.0.0", "dev": true }, + "enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, "entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -15507,6 +16166,12 @@ } } }, + "jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true + }, "js-tokens": { "version": "4.0.0" }, @@ -15653,6 +16318,103 @@ "type-check": "~0.4.0" } }, + "lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "dev": true, + "requires": { + "detect-libc": "^2.0.3", + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + }, + "dependencies": { + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true + } + } + }, + "lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "dev": true, + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "dev": true, + "optional": true + }, + "lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "dev": true, + "optional": true + }, + "lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "dev": true, + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "dev": true, + "optional": true + }, "locate-path": { "version": "6.0.0", "dev": true, @@ -16861,6 +17623,18 @@ "version": "3.2.4", "dev": true }, + "tailwindcss": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.17.tgz", + "integrity": "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==", + "devOptional": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, "temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", diff --git a/package.json b/package.json index cd904707..639ab9a2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~3.5.0", "@stylistic/eslint-plugin": "^4.2.0", + "@tailwindcss/vite": "^4.0.17", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -75,6 +76,7 @@ "history": "^5.3.0", "jsdom": "^26.0.0", "sass": "^1.86.3", + "tailwindcss": "^4.0.17", "typescript": "^5.8.3", "typescript-eslint": "^8.29.0", "vite": "^6.2.5", diff --git a/src/common/MainHeader.tsx b/src/common/MainHeader.tsx index 529cf947..31e5a39e 100644 --- a/src/common/MainHeader.tsx +++ b/src/common/MainHeader.tsx @@ -30,7 +30,7 @@ const MainHeader: FCWithDeps = () => { return ( - Shlink + Shlink diff --git a/src/common/NotFound.tsx b/src/common/NotFound.tsx index 3572c80f..582e20d7 100644 --- a/src/common/NotFound.tsx +++ b/src/common/NotFound.tsx @@ -1,19 +1,18 @@ -import { SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import { Button, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC, PropsWithChildren } from 'react'; -import { Link } from 'react-router'; type NotFoundProps = PropsWithChildren<{ to?: string }>; export const NotFound: FC = ({ to = '/', children = 'Home' }) => ( -
- +
+

Oops! We could not find requested route.

Use your browser's back button to navigate to the page you have previously come from, or just press this button.


- {children} +
); diff --git a/src/index.tsx b/src/index.tsx index 4e78ef35..0040ad3c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import pack from '../package.json'; import { container } from './container'; import { setUpStore } from './container/store'; import { register as registerServiceWorker } from './serviceWorkerRegistration'; +import './tailwind.css'; import './index.scss'; const store = setUpStore(container); diff --git a/src/tailwind.css b/src/tailwind.css new file mode 100644 index 00000000..21d51c4a --- /dev/null +++ b/src/tailwind.css @@ -0,0 +1,3 @@ +@import 'tailwindcss' prefix(tw) important; +@source '../node_modules/@shlinkio/shlink-frontend-kit'; +@import '@shlinkio/shlink-frontend-kit/tailwind.preset.css'; diff --git a/vite.config.ts b/vite.config.ts index 7e0fe317..8c87cefc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; import { VitePWA } from 'vite-plugin-pwa'; @@ -11,7 +12,7 @@ const homepage = pack.homepage?.trim(); /* eslint-disable-next-line no-restricted-exports */ export default defineConfig({ - plugins: [react(), VitePWA({ + plugins: [react(), tailwindcss(), VitePWA({ mode: process.env.NODE_ENV === 'development' ? 'development' : 'production', strategies: 'injectManifest', srcDir: './src', From c462bc30e19445716917b8423fc9378e25248a08 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 1 Apr 2025 11:30:15 +0200 Subject: [PATCH 02/19] Migrate ErrorHandler component to tailwind --- src/common/ErrorHandler.tsx | 17 +++++++---------- src/common/ErrorLayout.tsx | 15 +++++++++++++++ src/common/NotFound.tsx | 22 ++++++++++------------ 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 src/common/ErrorLayout.tsx diff --git a/src/common/ErrorHandler.tsx b/src/common/ErrorHandler.tsx index a18de8c1..a72589b0 100644 --- a/src/common/ErrorHandler.tsx +++ b/src/common/ErrorHandler.tsx @@ -1,7 +1,7 @@ -import { SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import { Button } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { PropsWithChildren, ReactNode } from 'react'; import { Component } from 'react'; -import { Button } from 'reactstrap'; +import { ErrorLayout } from './ErrorLayout'; type ErrorHandlerProps = PropsWithChildren<{ location?: typeof window.location; @@ -33,14 +33,11 @@ export class ErrorHandler extends Component - -

Oops! This is awkward :S

-

It seems that something went wrong. Try refreshing the page or just click this button.

-
- -
-
+ +

It seems that something went wrong. Try refreshing the page or just click this button.

+
+ +
); } diff --git a/src/common/ErrorLayout.tsx b/src/common/ErrorLayout.tsx new file mode 100644 index 00000000..ba3679a4 --- /dev/null +++ b/src/common/ErrorLayout.tsx @@ -0,0 +1,15 @@ +import { SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; +import type { FC, PropsWithChildren } from 'react'; + +export type ErrorLayoutProps = PropsWithChildren<{ + title: string; +}>; + +export const ErrorLayout: FC = ({ children, title }) => ( +
+ +

{title}

+ {children} +
+
+); diff --git a/src/common/NotFound.tsx b/src/common/NotFound.tsx index 582e20d7..a19973f5 100644 --- a/src/common/NotFound.tsx +++ b/src/common/NotFound.tsx @@ -1,18 +1,16 @@ -import { Button, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { Button } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC, PropsWithChildren } from 'react'; +import { ErrorLayout } from './ErrorLayout'; type NotFoundProps = PropsWithChildren<{ to?: string }>; export const NotFound: FC = ({ to = '/', children = 'Home' }) => ( -
- -

Oops! We could not find requested route.

-

- Use your browser's back button to navigate to the page you have previously come from, or just press this - button. -

-
- -
-
+ +

+ Use your browser's back button to navigate to the page you have previously come from, or just press this + button. +

+
+ +
); From 06fac716d14cdac475b79d00bbd2c5f27b4de109 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 1 Apr 2025 11:48:35 +0200 Subject: [PATCH 03/19] Migrate server-related components to tailwind --- src/servers/CreateServer.tsx | 20 +++++++++++--------- src/servers/EditServer.tsx | 8 ++++---- src/servers/ManageServers.tsx | 12 +++++------- src/servers/helpers/ImportServersBtn.tsx | 5 +++-- src/servers/helpers/ServerError.tsx | 4 ++-- src/servers/helpers/ServerForm.tsx | 18 ++++++++++++------ src/servers/helpers/withSelectedServer.tsx | 2 +- 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index b8c718fa..fc83e75e 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,5 +1,7 @@ import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; -import { Result, useToggle } from '@shlinkio/shlink-frontend-kit'; +import { useToggle } from '@shlinkio/shlink-frontend-kit'; +import type { ResultProps } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { Result } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router'; @@ -26,11 +28,11 @@ type CreateServerDeps = { useTimeoutToggle: TimeoutToggle; }; -const ImportResult = ({ type }: { type: 'error' | 'success' }) => ( +const ImportResult = ({ variant }: Pick) => (
- - {type === 'success' && 'Servers properly imported. You can now select one from the list :)'} - {type === 'error' && 'The servers could not be imported. Make sure the format is correct.'} + + {variant === 'success' && 'Servers properly imported. You can now select one from the list :)'} + {variant === 'error' && 'The servers could not be imported. Make sure the format is correct.'}
); @@ -68,16 +70,16 @@ const CreateServer: FCWithDeps = ({ servers return ( - Add new server} onSubmit={onSubmit}> + {!hasServers && ( )} {hasServers && } - + - {serversImported && } - {errorImporting && } + {serversImported && } + {errorImporting && } = withSelectedServ return ( Edit "{selectedServer.name}"} + title={<>Edit "{selectedServer.name}"} initialValues={selectedServer} onSubmit={handleSubmit} > - - + + ); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 25fad262..3cf1fb61 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -1,11 +1,9 @@ import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; -import { Result, SearchField, SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import { Button, Result, SearchInput, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useMemo, useState } from 'react'; -import { Link } from 'react-router'; -import { Button } from 'reactstrap'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; @@ -46,18 +44,18 @@ const ManageServers: FCWithDeps = ({ serv return ( - +
Import servers {filteredServers.length > 0 && ( - )}
-
@@ -83,7 +81,7 @@ const ManageServers: FCWithDeps = ({ serv {errorImporting && (
- The servers could not be imported. Make sure the format is correct. + The servers could not be imported. Make sure the format is correct.
)}
diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 5010ce59..30fd5717 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -1,9 +1,10 @@ import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit'; +import { Button } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { ChangeEvent, PropsWithChildren } from 'react'; import { useCallback, useRef, useState } from 'react'; -import { Button, UncontrolledTooltip } from 'reactstrap'; +import { UncontrolledTooltip } from 'reactstrap'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; import type { ServerData, ServersMap, ServerWithId } from '../data'; @@ -83,7 +84,7 @@ const ImportServersBtn: FCWithDeps - diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index b7504285..16b894d3 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -1,4 +1,4 @@ -import { Message } from '@shlinkio/shlink-frontend-kit'; +import { Message } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { Link } from 'react-router'; import { NoMenuLayout } from '../../common/NoMenuLayout'; @@ -25,7 +25,7 @@ const ServerError: FCWithDeps = ({ servers, s return (
- + {!isServerWithId(selectedServer) && 'Could not find this Shlink server.'} {isServerWithId(selectedServer) && ( <> diff --git a/src/servers/helpers/ServerForm.tsx b/src/servers/helpers/ServerForm.tsx index a1b8b53f..00ea2f10 100644 --- a/src/servers/helpers/ServerForm.tsx +++ b/src/servers/helpers/ServerForm.tsx @@ -1,4 +1,4 @@ -import { InputFormGroup, SimpleCard } from '@shlinkio/shlink-frontend-kit'; +import { LabelledInput, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC, PropsWithChildren, ReactNode } from 'react'; import { useState } from 'react'; import { handleEventPreventingDefault } from '../../utils/utils'; @@ -18,13 +18,19 @@ export const ServerForm: FC = ({ onSubmit, initialValues, child return (
- - Name - URL - API key + + setName(e.target.value)} required /> + setUrl(e.target.value)} required /> + setApiKey(e.target.value)} + required + /> -
{children}
+
{children}
); }; diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index e1b41ba1..b1b86b66 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -1,4 +1,4 @@ -import { Message } from '@shlinkio/shlink-frontend-kit'; +import { Message } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useEffect } from 'react'; import { useParams } from 'react-router'; From c9ada8f41d0099968484b2edb5167cac778f6039 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Apr 2025 09:01:33 +0200 Subject: [PATCH 04/19] Migrate servers table to a tailwind-based Table component --- src/common/NoMenuLayout.tsx | 2 +- src/servers/ManageServers.tsx | 49 +++++++++++++++----------------- src/servers/ManageServersRow.tsx | 19 +++++++------ 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/common/NoMenuLayout.tsx b/src/common/NoMenuLayout.tsx index 2a2e171d..bcc87837 100644 --- a/src/common/NoMenuLayout.tsx +++ b/src/common/NoMenuLayout.tsx @@ -7,5 +7,5 @@ export type NoMenuLayoutProps = PropsWithChildren & { }; export const NoMenuLayout: FC = ({ children, className }) => ( -
{children}
+
{children}
); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 3cf1fb61..20f02710 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -1,7 +1,7 @@ import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; -import { Button, Result, SearchInput, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useMemo, useState } from 'react'; import { NoMenuLayout } from '../common/NoMenuLayout'; @@ -43,40 +43,37 @@ const ManageServers: FCWithDeps = ({ serv const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); return ( - + -
-
- Import servers +
+
+ Import servers {filteredServers.length > 0 && ( - )}
-
- - - - - {hasAutoConnect && } - - - - - - - {!filteredServers.length && } - {filteredServers.map((server) => ( - - ))} - -
Auto-connectNameBase URLOptions
No servers found.
+ + + {hasAutoConnect && Auto-connect} + Name + Base URL + Options + + )}> + {!filteredServers.length && No servers found.} + {filteredServers.map((server) => ( + + ))} +
{errorImporting && ( diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx index a6d20c91..91fd5067 100644 --- a/src/servers/ManageServersRow.tsx +++ b/src/servers/ManageServersRow.tsx @@ -1,5 +1,6 @@ import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Table } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { Link } from 'react-router'; import { UncontrolledTooltip } from 'reactstrap'; @@ -21,9 +22,9 @@ const ManageServersRow: FCWithDeps const { ManageServersRowDropdown } = useDependencies(ManageServersRow); return ( - + {hasAutoConnect && ( - + {server.autoConnect && ( <> @@ -32,16 +33,16 @@ const ManageServersRow: FCWithDeps )} - + )} - + {server.name} - - {server.url} - + + {server.url} + - - + + ); }; From a63c214d8d3074b329882e2fdd446b0c79de4805 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Apr 2025 09:20:04 +0200 Subject: [PATCH 05/19] Use tailwind-based components in AppUpdateBanner --- src/app/App.tsx | 2 +- src/common/AppUpdateBanner.scss | 17 --------------- src/common/AppUpdateBanner.tsx | 38 +++++++++++++++++++++------------ 3 files changed, 25 insertions(+), 32 deletions(-) delete mode 100644 src/common/AppUpdateBanner.scss diff --git a/src/app/App.tsx b/src/app/App.tsx index 18ce3fc0..11e99895 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -87,7 +87,7 @@ const App: FCWithDeps = (
- +
); }; diff --git a/src/common/AppUpdateBanner.scss b/src/common/AppUpdateBanner.scss deleted file mode 100644 index 5f84ca85..00000000 --- a/src/common/AppUpdateBanner.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; -@use '../utils/mixins/horizontal-align'; - -.app-update-banner.app-update-banner { - @include horizontal-align.horizontal-align(); - - position: fixed; - top: base.$headerHeight - 25px; - padding: 0 4rem 0 0; - z-index: 1040; - margin: 0; - color: var(--text-color); - text-align: center; - width: 700px; - max-width: calc(100% - 30px); - box-shadow: 0 0 1rem var(--brand-color); -} diff --git a/src/common/AppUpdateBanner.tsx b/src/common/AppUpdateBanner.tsx index c3c6db62..05c01abb 100644 --- a/src/common/AppUpdateBanner.tsx +++ b/src/common/AppUpdateBanner.tsx @@ -1,34 +1,44 @@ import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { SimpleCard, useToggle } from '@shlinkio/shlink-frontend-kit'; -import type { MouseEventHandler } from 'react'; -import { forwardRef, useCallback } from 'react'; -import { Alert, Button } from 'reactstrap'; -import './AppUpdateBanner.scss'; +import { useToggle } from '@shlinkio/shlink-frontend-kit'; +import { Button, Card, CloseButton } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { clsx } from 'clsx'; +import type { FC } from 'react'; +import { useCallback } from 'react'; interface AppUpdateBannerProps { isOpen: boolean; - toggle: MouseEventHandler; + onClose: () => void; forceUpdate: () => void; } -export const AppUpdateBanner = forwardRef(({ isOpen, toggle, forceUpdate }, ref) => { +export const AppUpdateBanner: FC = ({ isOpen, onClose, forceUpdate }) => { const [isUpdating,, setUpdating] = useToggle(); const update = useCallback(() => { setUpdating(); forceUpdate(); }, [forceUpdate, setUpdating]); + if (!isOpen) { + return null; + } + return ( - -

This app has just been updated!

-

+ + + This app has just been updated! + + + Restart it to enjoy the new features. - -

-
+ + ); -}); +}; From 7c31b210bdfbc4a9ca7fae280865ab4ff4f1437b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Apr 2025 09:33:28 +0200 Subject: [PATCH 06/19] Fix tests after initial tailwind components migration --- src/common/AppUpdateBanner.tsx | 13 ++-- test/common/AppUpdateBanner.test.tsx | 8 +-- test/common/NotFound.test.tsx | 1 - test/servers/ManageServersRow.test.tsx | 15 ++-- .../ManageServersRow.test.tsx.snap | 70 ++++++++++++------- test/servers/helpers/ServerForm.test.tsx | 6 +- 6 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/common/AppUpdateBanner.tsx b/src/common/AppUpdateBanner.tsx index 05c01abb..8562271c 100644 --- a/src/common/AppUpdateBanner.tsx +++ b/src/common/AppUpdateBanner.tsx @@ -24,12 +24,15 @@ export const AppUpdateBanner: FC = ({ isOpen, onClose, for } return ( - + - This app has just been updated! +
This app has just been updated!
diff --git a/test/common/AppUpdateBanner.test.tsx b/test/common/AppUpdateBanner.test.tsx index e98ad2cb..3587d237 100644 --- a/test/common/AppUpdateBanner.test.tsx +++ b/test/common/AppUpdateBanner.test.tsx @@ -4,11 +4,11 @@ import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - const toggle = vi.fn(); + const onClose = vi.fn(); const forceUpdate = vi.fn(); const setUp = async () => { const result = await act( - () => renderWithEvents(), + () => renderWithEvents(), ); await waitFor(() => screen.getByRole('alert')); @@ -28,9 +28,9 @@ describe('', () => { it('invokes toggle when alert is closed', async () => { const { user } = await setUp(); - expect(toggle).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); await user.click(screen.getByLabelText('Close')); - expect(toggle).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); }); it('triggers the update when clicking the button', async () => { diff --git a/test/common/NotFound.test.tsx b/test/common/NotFound.test.tsx index 1e13e3c1..61d71f37 100644 --- a/test/common/NotFound.test.tsx +++ b/test/common/NotFound.test.tsx @@ -30,6 +30,5 @@ describe('', () => { expect(link).toHaveAttribute('href', expectedLink); expect(link).toHaveTextContent(expectedText); - expect(link).toHaveAttribute('class', 'btn btn-outline-primary btn-lg'); }); }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 4a3ec3c4..fb3b4e9d 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,3 +1,4 @@ +import { Table } from '@shlinkio/shlink-frontend-kit/tailwind'; import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; @@ -17,11 +18,9 @@ describe('', () => { }; const setUp = (hasAutoConnect = false, autoConnect = false) => render( - - - - -
+ }> + +
, ); @@ -32,11 +31,7 @@ describe('', () => { [false, 3], ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { setUp(hasAutoConnect); - - const td = screen.getAllByRole('cell'); - const th = screen.getAllByRole('columnheader'); - - expect(td.length + th.length).toEqual(expectedCols); + expect(screen.getAllByRole('cell')).toHaveLength(expectedCols); }); it('renders a dropdown', () => { diff --git a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap index ae446804..0ecf0a1a 100644 --- a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap @@ -2,14 +2,25 @@ exports[` > renders auto-connect icon only if server is autoConnect 1`] = `
- - +
+ + + + - +
> renders auto-connect icon only if server is auto /> > renders auto-connect icon only if server is auto > My server - https://example.com ManageServersRowDropdown @@ -60,18 +71,29 @@ exports[` > renders auto-connect icon only if server is auto exports[` > renders auto-connect icon only if server is autoConnect 2`] = `
- - +
+ + + + +
- > renders auto-connect icon only if server is auto > My server - https://example.com ManageServersRowDropdown diff --git a/test/servers/helpers/ServerForm.test.tsx b/test/servers/helpers/ServerForm.test.tsx index 63207f6c..2d6f6379 100644 --- a/test/servers/helpers/ServerForm.test.tsx +++ b/test/servers/helpers/ServerForm.test.tsx @@ -8,10 +8,12 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders components', () => { + it('renders inputs', () => { setUp(); - expect(screen.getAllByRole('textbox')).toHaveLength(3); + expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); + expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); + expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); expect(screen.getByText('Something')).toBeInTheDocument(); }); From b19162ce9198ab0e6ac73951edcb463c6ef794f5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Apr 2025 10:27:30 +0200 Subject: [PATCH 07/19] Migrate Home component to tailwind --- src/common/Home.scss | 15 --------------- src/common/Home.tsx | 43 ++++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 src/common/Home.scss diff --git a/src/common/Home.scss b/src/common/Home.scss deleted file mode 100644 index 04c8c21e..00000000 --- a/src/common/Home.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; - -.home__title { - font-size: 1.75rem; - - @media (min-width: base.$mdMin) { - font-size: 2.2rem; - } -} - -.home__servers-container { - @media (min-width: base.$mdMin) { - border-left: 1px solid var(--border-color); - } -} diff --git a/src/common/Home.tsx b/src/common/Home.tsx index 7ebfa583..23e3e819 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -1,18 +1,17 @@ import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, Card } from '@shlinkio/shlink-frontend-kit/tailwind'; import { clsx } from 'clsx'; import { useEffect } from 'react'; import { ExternalLink } from 'react-external-link'; -import { Link, useNavigate } from 'react-router'; -import { Card } from 'reactstrap'; +import { useNavigate } from 'react-router'; import type { ServersMap } from '../servers/data'; import { ServersListGroup } from '../servers/ServersListGroup'; import { ShlinkLogo } from './img/ShlinkLogo'; -import './Home.scss'; -interface HomeProps { +export type HomeProps = { servers: ServersMap; -} +}; export const Home = ({ servers }: HomeProps) => { const navigate = useNavigate(); @@ -28,35 +27,37 @@ export const Home = ({ servers }: HomeProps) => { }, [serversList, navigate]); return ( -
- -
-
-
+
+ +
+
+
-
+

Welcome!

{!hasServers && ( -
-

This application will help you manage your Shlink servers.

-

- - Add a server - +

+

This application will help you manage your Shlink servers.

+

+

-

+

- Learn more about Shlink + Learn more about Shlink From 15ef29ecea5b6276a81164ec087a377c703aec41 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Apr 2025 10:34:47 +0200 Subject: [PATCH 08/19] Migrated NoMenuLayout to tailwind --- src/common/NoMenuLayout.scss | 9 --------- src/common/NoMenuLayout.tsx | 5 +++-- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 src/common/NoMenuLayout.scss diff --git a/src/common/NoMenuLayout.scss b/src/common/NoMenuLayout.scss deleted file mode 100644 index a97be69e..00000000 --- a/src/common/NoMenuLayout.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; - -.no-menu-wrapper { - padding: 15px 0 0; - - @media (min-width: base.$mdMin) { - padding: 30px 20px 20px; - } -} diff --git a/src/common/NoMenuLayout.tsx b/src/common/NoMenuLayout.tsx index bcc87837..1b2a4ace 100644 --- a/src/common/NoMenuLayout.tsx +++ b/src/common/NoMenuLayout.tsx @@ -1,11 +1,12 @@ import { clsx } from 'clsx'; import type { FC, PropsWithChildren } from 'react'; -import './NoMenuLayout.scss'; export type NoMenuLayoutProps = PropsWithChildren & { className?: string; }; export const NoMenuLayout: FC = ({ children, className }) => ( -

{children}
+
+ {children} +
); From 01ca3693880a69e09cb3a3242b26c8f04d73405d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Apr 2025 07:57:58 +0200 Subject: [PATCH 09/19] Migrate DeleteServerModal to tailwind components --- config/test/setupTests.ts | 9 +++ src/servers/CreateServer.tsx | 9 ++- src/servers/DeleteServerButton.tsx | 11 ++- src/servers/DeleteServerModal.tsx | 72 ++++++++----------- src/servers/ManageServersRowDropdown.tsx | 4 +- test/__helpers__/TestModalWrapper.tsx | 16 +++-- test/servers/DeleteServerButton.test.tsx | 41 +++++++---- test/servers/DeleteServerModal.test.tsx | 50 +++++-------- .../servers/ManageServersRowDropdown.test.tsx | 4 +- .../ManageServersRowDropdown.test.tsx.snap | 4 +- vite.config.ts | 4 ++ 11 files changed, 117 insertions(+), 107 deletions(-) diff --git a/config/test/setupTests.ts b/config/test/setupTests.ts index 893d13ae..abe354c9 100644 --- a/config/test/setupTests.ts +++ b/config/test/setupTests.ts @@ -22,3 +22,12 @@ afterEach(() => { HTMLCanvasElement.prototype.getContext = (() => {}) as any; (global as any).scrollTo = () => {}; (global as any).matchMedia = () => ({ matches: false }); + +HTMLDialogElement.prototype.showModal = function() { + this.setAttribute('open', ''); +}; +HTMLDialogElement.prototype.close = function() { + this.removeAttribute('open'); + this.dispatchEvent(new CloseEvent('close')); + this.dispatchEvent(new CloseEvent('cancel')); +}; diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index fc83e75e..7c038810 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,11 +1,10 @@ import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; import { useToggle } from '@shlinkio/shlink-frontend-kit'; import type { ResultProps } from '@shlinkio/shlink-frontend-kit/tailwind'; -import { Result } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { Button, Result } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router'; -import { Button } from 'reactstrap'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; @@ -29,7 +28,7 @@ type CreateServerDeps = { }; const ImportResult = ({ variant }: Pick) => ( -
+
{variant === 'success' && 'Servers properly imported. You can now select one from the list :)'} {variant === 'error' && 'The servers could not be imported. Make sure the format is correct.'} @@ -74,8 +73,8 @@ const CreateServer: FCWithDeps = ({ servers {!hasServers && ( )} - {hasServers && } - + {hasServers && } + {serversImported && } diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index 7e514836..78cb7004 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -3,6 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useToggle } from '@shlinkio/shlink-frontend-kit'; import { clsx } from 'clsx'; import type { FC, PropsWithChildren } from 'react'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import type { ServerWithId } from './data'; @@ -23,6 +25,13 @@ const DeleteServerButton: FCWithDeps { const { DeleteServerModal } = useDependencies(DeleteServerButton); const [isModalOpen, , showModal, hideModal] = useToggle(); + const navigate = useNavigate(); + const onClose = useCallback((confirmed: boolean) => { + hideModal(); + if (confirmed) { + navigate('/'); + } + }, [hideModal, navigate]); return ( <> @@ -31,7 +40,7 @@ const DeleteServerButton: FCWithDeps{children ?? 'Remove this server'} - + ); }; diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index 23ca528f..e4561f22 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -1,56 +1,40 @@ +import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; -import { useRef } from 'react'; -import { useNavigate } from 'react-router'; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { useCallback } from 'react'; import type { ServerWithId } from './data'; -export interface DeleteServerModalProps { +export type DeleteServerModalProps = { server: ServerWithId; - toggle: () => void; - isOpen: boolean; - redirectHome?: boolean; -} + onClose: (confirmed: boolean) => void; + open: boolean; +}; -interface DeleteServerModalConnectProps extends DeleteServerModalProps { +type DeleteServerModalConnectProps = DeleteServerModalProps & { deleteServer: (server: ServerWithId) => void; -} - -export const DeleteServerModal: FC = ( - { server, toggle, isOpen, deleteServer, redirectHome = true }, -) => { - const navigate = useNavigate(); - const doDelete = useRef(false); - const toggleAndDelete = () => { - doDelete.current = true; - toggle(); - }; - const onClosed = () => { - if (!doDelete.current) { - return; - } +}; +export const DeleteServerModal: FC = ({ server, onClose, open, deleteServer }) => { + const onConfirm = useCallback(() => { deleteServer(server); - if (redirectHome) { - navigate('/'); - } - }; + onClose(true); + }, [deleteServer, onClose, server]); return ( - - Remove server - -

Are you sure you want to remove {server ? server.name : ''}?

-

- - No data will be deleted, only the access to this server will be removed from this device. - You can create it again at any moment. - -

-
- - - - -
+ onClose(false)} + onConfirm={onConfirm} + confirmText="Delete" + > +

Are you sure you want to remove {server ? server.name : ''}?

+

+ + No data will be deleted, only the access to this server will be removed from this device. + You can create it again at any moment. + +

+
); }; diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 4fde175a..60c46ab1 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -48,11 +48,11 @@ const ManageServersRowDropdown: FCWithDeps {isAutoConnect ? 'Do not a' : 'A'}uto-connect - + Remove server - + ); }; diff --git a/test/__helpers__/TestModalWrapper.tsx b/test/__helpers__/TestModalWrapper.tsx index 18959df7..28ade53a 100644 --- a/test/__helpers__/TestModalWrapper.tsx +++ b/test/__helpers__/TestModalWrapper.tsx @@ -1,14 +1,16 @@ -import { useToggle } from '@shlinkio/shlink-frontend-kit'; import type { FC, ReactElement } from 'react'; +import { useCallback, useState } from 'react'; -interface RenderModalArgs { - isOpen: boolean; - toggle: () => void; -} +export type RenderModalArgs = { + open: boolean; + onClose: () => void; +}; export const TestModalWrapper: FC<{ renderModal: (args: RenderModalArgs) => ReactElement }> = ( { renderModal }, ) => { - const [isOpen, toggle] = useToggle(true); - return renderModal({ isOpen, toggle }); + const [open, setOpen] = useState(true); + const onClose = useCallback(() => setOpen(false), []); + + return renderModal({ open, onClose }); }; diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 75693268..9c7eee7b 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,18 +1,28 @@ -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { createMemoryHistory } from 'history'; import type { ReactNode } from 'react'; +import { Router } from 'react-router'; import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton'; import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal'; +import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const DeleteServerButton = DeleteServerButtonFactory(fromPartial({ - DeleteServerModal: ({ isOpen }: DeleteServerModalProps) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}, + DeleteServerModal: (props: DeleteServerModalProps) => , })); - const setUp = (children?: ReactNode) => renderWithEvents( - {children}, - ); + const setUp = (children?: ReactNode) => { + const history = createMemoryHistory({ initialEntries: ['/foo'] }); + const result = renderWithEvents( + + {children} + , + ); + + return { history, ...result }; + }; it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); @@ -28,14 +38,21 @@ describe('', () => { }); it('displays modal when button is clicked', async () => { - const { user, container } = setUp(); + const { user } = setUp(); - expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Closed/); - expect(screen.getByText(/DeleteServerModal/)).not.toHaveTextContent(/Open/); - if (container.firstElementChild) { - await user.click(container.firstElementChild); - } + expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); + await user.click(screen.getByText('Remove this server')); + expect(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); + }); - await waitFor(() => expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Open/)); + it('navigates to home when deletion is confirmed', async () => { + const { user, history } = setUp(); + + // Open modal + await user.click(screen.getByText('Remove this server')); + + expect(history.location.pathname).toEqual('/foo'); + await user.click(screen.getByRole('button', { name: 'Delete' })); + expect(history.location.pathname).toEqual('/'); }); }); diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index dfb8a2c6..dfc24a1b 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,7 +1,5 @@ -import { act, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { createMemoryHistory } from 'history'; -import { Router } from 'react-router'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -10,36 +8,29 @@ import { TestModalWrapper } from '../__helpers__/TestModalWrapper'; describe('', () => { const deleteServerMock = vi.fn(); const serverName = 'the_server_name'; - const setUp = async () => { - const history = createMemoryHistory({ initialEntries: ['/foo'] }); - const result = await act(() => renderWithEvents( - - ( - - )} + const setUp = () => renderWithEvents( + ( + - , - )); - - return { history, ...result }; - }; + )} + />, + ); it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders a modal window', async () => { - await setUp(); + it('renders a modal window', () => { + setUp(); expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); }); - it('displays the name of the server as part of the content', async () => { - await setUp(); + it('displays the name of the server as part of the content', () => { + setUp(); expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); expect(screen.getByText(serverName)).toBeInTheDocument(); @@ -47,25 +38,20 @@ describe('', () => { it.each([ [() => screen.getByRole('button', { name: 'Cancel' })], - [() => screen.getByLabelText('Close')], + [() => screen.getByLabelText('Close dialog')], ])('toggles when clicking cancel button', async (getButton) => { - const { user, history } = await setUp(); + const { user } = setUp(); - expect(history.location.pathname).toEqual('/foo'); await user.click(getButton()); - expect(deleteServerMock).not.toHaveBeenCalled(); - expect(history.location.pathname).toEqual('/foo'); // No navigation happens, keeping initial pathname }); it('deletes server when clicking accept button', async () => { - const { user, history } = await setUp(); + const { user } = setUp(); expect(deleteServerMock).not.toHaveBeenCalled(); - expect(history.location.pathname).toEqual('/foo'); await user.click(screen.getByRole('button', { name: 'Delete' })); await waitFor(() => expect(deleteServerMock).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(history.location.pathname).toEqual('/')); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index 7ac1930f..9db54e7b 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -9,8 +9,8 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({ - DeleteServerModal: ({ isOpen }: { isOpen: boolean }) => ( - DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'} + DeleteServerModal: ({ open }: { open: boolean }) => ( + DeleteServerModal {open ? '[OPEN]' : '[CLOSED]'} ), })); const setAutoConnect = vi.fn(); diff --git a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap index 967d1e10..d597f4bd 100644 --- a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap @@ -114,7 +114,7 @@ exports[` > renders expected size and icon 1`] = ` tabindex="-1" /> } @@ -81,10 +81,10 @@ const CreateServer: FCWithDeps = ({ servers {errorImporting && } serverData && saveNewServer(serverData)} + onClose={goBack} + onConfirm={() => serverData && saveNewServer(serverData)} /> ); diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 20f02710..d9ba7b17 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -48,7 +48,7 @@ const ManageServers: FCWithDeps = ({ serv
- Import servers + Import servers {filteredServers.length > 0 && ( - - - + +

{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}

+
    + {duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? ( + +
  • URL: {url}
  • +
  • API key: {apiKey}
  • +
    + ) :
  • {url} - {apiKey}
  • ))} +
+ + {hasMultipleServers ? 'Do you want to save duplicated servers' : 'Do you want to save this server'}? + +
); }; diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 503c7736..c0b1ed5e 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit'; import { Button } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { ChangeEvent, PropsWithChildren } from 'react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef , useState } from 'react'; import { UncontrolledTooltip } from 'reactstrap'; import type { FCWithDeps } from '../../container/utils'; import { componentFactory, useDependencies } from '../../container/utils'; @@ -14,7 +14,7 @@ import { dedupServers, ensureUniqueIds } from './index'; export type ImportServersBtnProps = PropsWithChildren<{ onImport?: () => void; - onImportError?: (error: Error) => void; + onError?: (error: Error) => void; tooltipPlacement?: 'top' | 'bottom'; className?: string; }>; @@ -32,8 +32,8 @@ const ImportServersBtn: FCWithDeps {}, - onImportError = () => {}, + onImport, + onError = () => {}, tooltipPlacement = 'bottom', className = '', }) => { @@ -41,41 +41,46 @@ const ImportServersBtn: FCWithDeps(); const [duplicatedServers, setDuplicatedServers] = useState([]); const [isModalOpen,, showModal, hideModal] = useToggle(); + const newServersCreatedRef = useRef(false); - const importedServersRef = useRef([]); - const newServersRef = useRef([]); - - const create = useCallback((serversData: ServerWithId[]) => { - createServers(serversData); - hideModal(); - onImport(); - }, [createServers, hideModal, onImport]); const onFile = useCallback( async ({ target }: ChangeEvent) => serversImporter.importServersFromFile(target.files?.[0]) .then((importedServers) => { const { duplicatedServers, newServers } = dedupServers(servers, importedServers); - importedServersRef.current = ensureUniqueIds(servers, importedServers); - newServersRef.current = ensureUniqueIds(servers, newServers); + // Immediately create new servers + newServersCreatedRef.current = newServers.length > 0; + createServers(ensureUniqueIds(servers, newServers)); - if (duplicatedServers.length === 0) { - create(importedServersRef.current); - } else { + // For duplicated servers, ask for confirmation + if (duplicatedServers.length > 0) { setDuplicatedServers(duplicatedServers); showModal(); + } else { + onImport?.(); } }) .then(() => { // Reset file input after processing file (target as { value: string | null }).value = null; }) - .catch(onImportError), - [create, onImportError, servers, serversImporter, showModal], + .catch(onError), + [createServers, onError, onImport, servers, serversImporter, showModal], ); - const createAllServers = useCallback(() => create(importedServersRef.current), [create]); - const createNonDuplicatedServers = useCallback(() => create(newServersRef.current), [create]); + const createDuplicatedServers = useCallback(() => { + createServers(ensureUniqueIds(servers, duplicatedServers)); + hideModal(); + onImport?.(); + }, [createServers, duplicatedServers, hideModal, onImport, servers]); + const discardDuplicatedServers = useCallback(() => { + hideModal(); + // If duplicated servers were discarded but some non-duplicated servers were created, call onImport + if (newServersCreatedRef.current) { + onImport?.(); + } + }, [hideModal, onImport]); return ( <> @@ -97,10 +102,10 @@ const ImportServersBtn: FCWithDeps ); diff --git a/test/servers/helpers/DuplicatedServersModal.test.tsx b/test/servers/helpers/DuplicatedServersModal.test.tsx index 99a99846..e87df81a 100644 --- a/test/servers/helpers/DuplicatedServersModal.test.tsx +++ b/test/servers/helpers/DuplicatedServersModal.test.tsx @@ -6,10 +6,10 @@ import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - const onDiscard = vi.fn(); - const onSave = vi.fn(); + const onClose = vi.fn(); + const onConfirm = vi.fn(); const setUp = (duplicatedServers: ServerData[] = []) => act(() => renderWithEvents( - , + , )); const mockServer = (data: Partial = {}) => fromPartial(data); @@ -32,8 +32,9 @@ describe('', () => { { header: 'Duplicated server', firstParagraph: 'There is already a server with:', - lastParagraph: 'Do you want to save this server anyway?', + lastParagraph: 'Do you want to save this server?', discardBtn: 'Discard', + confirmButton: 'Save duplicate', }, ], [ @@ -41,8 +42,9 @@ describe('', () => { { header: 'Duplicated servers', firstParagraph: 'The next servers already exist:', - lastParagraph: 'Do you want to ignore duplicated servers?', + lastParagraph: 'Do you want to save duplicated servers?', discardBtn: 'Ignore duplicates', + confirmButton: 'Save duplicates', }, ], ])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => { @@ -52,6 +54,7 @@ describe('', () => { expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); }); it.each([ @@ -80,19 +83,19 @@ describe('', () => { } }); - it('invokes onDiscard when appropriate button is clicked', async () => { + it('invokes onClose when appropriate button is clicked', async () => { const { user } = await setUp(); - expect(onDiscard).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); await user.click(screen.getByRole('button', { name: 'Discard' })); - expect(onDiscard).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); }); - it('invokes onSave when appropriate button is clicked', async () => { + it('invokes onConfirm when appropriate button is clicked', async () => { const { user } = await setUp(); - expect(onSave).not.toHaveBeenCalled(); - await user.click(screen.getByRole('button', { name: 'Save anyway' })); - expect(onSave).toHaveBeenCalled(); + expect(onConfirm).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Save duplicate' })); + expect(onConfirm).toHaveBeenCalled(); }); }); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 62a1888c..33c221da 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -65,9 +65,9 @@ describe('', () => { }); it.each([ - { btnName: 'Save anyway',savesDuplicatedServers: true }, + { btnName: 'Save duplicate', savesDuplicatedServers: true }, { btnName: 'Discard', savesDuplicatedServers: false }, - ])('creates expected servers depending on selected option in modal', async ({ btnName, savesDuplicatedServers }) => { + ])('creates duplicated servers depending on selected option in modal', async ({ btnName, savesDuplicatedServers }) => { const existingServerData: ServerData = { name: 'existingServer', url: 'http://s.test/existingUrl', @@ -84,14 +84,20 @@ describe('', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await user.upload(screen.getByTestId('csv-file-input'), csvFile); + + // Once the file is uploaded, non-duplicated servers are immediately created + expect(createServersMock).toHaveBeenCalledExactlyOnceWith([expect.objectContaining(newServer)]); + expect(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: btnName })); - expect(createServersMock).toHaveBeenCalledWith( - savesDuplicatedServers - ? [expect.objectContaining(existingServerData), expect.objectContaining(newServer)] - : [expect.objectContaining(newServer)], - ); - expect(onImportMock).toHaveBeenCalledTimes(1); + // If duplicated servers are saved, there's one extra call + if (savesDuplicatedServers) { + expect(createServersMock).toHaveBeenLastCalledWith([expect.objectContaining(existingServerData)]); + } + + // On import is called only once, no matter what + expect(onImportMock).toHaveBeenCalledOnce(); + expect(createServersMock).toHaveBeenCalledTimes(savesDuplicatedServers ? 2 : 1); }); }); From c29b077e937805d594e589809790ddbdb236ba2c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Apr 2025 11:23:35 +0200 Subject: [PATCH 15/19] Fix some buttons acting like submits --- src/servers/CreateServer.tsx | 2 +- src/servers/EditServer.tsx | 2 +- src/servers/helpers/ImportServersBtn.tsx | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index c8157c26..ef16e410 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -73,7 +73,7 @@ const CreateServer: FCWithDeps = ({ servers {!hasServers && ( )} - {hasServers && } + {hasServers && } diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index 86fcbb13..4877b167 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -44,7 +44,7 @@ const EditServer: FCWithDeps = withSelectedServ initialValues={selectedServer} onSubmit={handleSubmit} > - + diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index c0b1ed5e..94ae7da0 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -84,7 +84,13 @@ const ImportServersBtn: FCWithDeps - From 0ec867b1858774370ff170f9d30838141e18b338 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Apr 2025 11:37:44 +0200 Subject: [PATCH 16/19] Migrate App stylesheets to tailwind --- src/app/App.scss | 26 -------------------------- src/app/App.tsx | 14 +++++++++----- src/tailwind.css | 3 +++ 3 files changed, 12 insertions(+), 31 deletions(-) delete mode 100644 src/app/App.scss diff --git a/src/app/App.scss b/src/app/App.scss deleted file mode 100644 index 11f0bc7a..00000000 --- a/src/app/App.scss +++ /dev/null @@ -1,26 +0,0 @@ -@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; - -.app-container { - height: 100%; -} - -.app { - padding-top: base.$headerHeight; - height: 100%; -} - -.shlink-wrapper { - min-height: 100%; - padding-bottom: base.$footer-height + base.$footer-margin; - margin-bottom: -(base.$footer-height + base.$footer-margin); -} - -.shlink-footer { - height: base.$footer-height; - margin-top: base.$footer-margin; - padding: 0; - - @media (min-width: base.$mdMin) { - padding: 0 15px; - } -} diff --git a/src/app/App.tsx b/src/app/App.tsx index 11e99895..ebf5f964 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -10,7 +10,6 @@ import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import type { ServersMap } from '../servers/data'; import { forceUpdate } from '../utils/helpers/sw'; -import './App.scss'; type AppProps = { fetchServers: () => void; @@ -62,11 +61,16 @@ const App: FCWithDeps = ( }, [settings.ui?.theme]); return ( -
+
-
-
+
+
} /> @@ -82,7 +86,7 @@ const App: FCWithDeps = (
-
+
diff --git a/src/tailwind.css b/src/tailwind.css index 78ee9f46..2741f627 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -18,6 +18,9 @@ @layer base { :root { --header-height: 56px; + --footer-height: 2.3rem; + --footer-margin: .8rem; + /* Width of ShlinkWebComponent's side menu when not collapsed */ --aside-menu-width: 260px; } From bd034c11b69bcf0d74684705f13cd39f65685a48 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Apr 2025 11:41:50 +0200 Subject: [PATCH 17/19] Fix App test --- src/app/App.tsx | 1 + src/common/AppUpdateBanner.tsx | 2 +- test/app/App.test.tsx | 18 +++++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index ebf5f964..718124db 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -66,6 +66,7 @@ const App: FCWithDeps = (
= ({ isOpen, onClose, for Restart it to enjoy the new features. diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 19d692e2..bb1a49cf 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -54,13 +54,17 @@ describe('', () => { }); it.each([ - ['/foo', 'shlink-wrapper'], - ['/bar', 'shlink-wrapper'], - ['/', 'shlink-wrapper d-flex align-items-center pt-3'], - ])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, expectedClasses) => { - const { container } = await setUp(pathname); - const shlinkWrapper = container.querySelector('.shlink-wrapper'); + ['/foo', false], + ['/bar', false], + ['/', true], + ])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => { + await setUp(pathname); + const shlinkWrapper = screen.getByTestId('shlink-wrapper'); - expect(shlinkWrapper).toHaveAttribute('class', expectedClasses); + if (isFlex) { + expect(shlinkWrapper).toHaveClass('tw:flex'); + } else { + expect(shlinkWrapper).not.toHaveClass('tw:flex'); + } }); }); From d188d67c5ae24e363099695784665a4b1e3cb36b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Apr 2025 11:48:24 +0200 Subject: [PATCH 18/19] Replace some bootstrap utility classes with tailwind ones --- src/common/ShlinkVersions.tsx | 4 +- src/common/ShlinkVersionsContainer.tsx | 2 +- src/servers/DeleteServerButton.tsx | 15 ++---- src/servers/helpers/ServerError.tsx | 2 +- test/servers/DeleteServerButton.test.tsx | 5 +- .../DeleteServerButton.test.tsx.snap | 52 +++---------------- 6 files changed, 15 insertions(+), 65 deletions(-) diff --git a/src/common/ShlinkVersions.tsx b/src/common/ShlinkVersions.tsx index 38a3a4aa..d19f9291 100644 --- a/src/common/ShlinkVersions.tsx +++ b/src/common/ShlinkVersions.tsx @@ -12,7 +12,7 @@ export interface ShlinkVersionsProps { } const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-client'; version: string }) => ( - + {version} ); @@ -21,7 +21,7 @@ export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIE const normalizedClientVersion = normalizeVersion(clientVersion); return ( - + {isReachableServer(selectedServer) && ( <>Server: - )} diff --git a/src/common/ShlinkVersionsContainer.tsx b/src/common/ShlinkVersionsContainer.tsx index 18e1a680..03f60878 100644 --- a/src/common/ShlinkVersionsContainer.tsx +++ b/src/common/ShlinkVersionsContainer.tsx @@ -9,7 +9,7 @@ export type ShlinkVersionsContainerProps = { export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => (
diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index 78cb7004..e362d6fb 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -1,7 +1,4 @@ -import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useToggle } from '@shlinkio/shlink-frontend-kit'; -import { clsx } from 'clsx'; import type { FC, PropsWithChildren } from 'react'; import { useCallback } from 'react'; import { useNavigate } from 'react-router'; @@ -12,17 +9,13 @@ import type { DeleteServerModalProps } from './DeleteServerModal'; export type DeleteServerButtonProps = PropsWithChildren<{ server: ServerWithId; - className?: string; - textClassName?: string; }>; type DeleteServerButtonDeps = { DeleteServerModal: FC; }; -const DeleteServerButton: FCWithDeps = ( - { server, className, children, textClassName }, -) => { +const DeleteServerButton: FCWithDeps = ({ server, children }) => { const { DeleteServerModal } = useDependencies(DeleteServerButton); const [isModalOpen, , showModal, hideModal] = useToggle(); const navigate = useNavigate(); @@ -35,11 +28,9 @@ const DeleteServerButton: FCWithDeps - - ); diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index c95b39a3..a1bdd87b 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -45,7 +45,7 @@ const ServerError: FCWithDeps = ({ servers, s {isServerWithId(selectedServer) && (

Alternatively, if you think you may have misconfigured this server, you - can remove + can remove it or  edit it.

diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 9c7eee7b..fce035cb 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -13,11 +13,11 @@ describe('', () => { const DeleteServerButton = DeleteServerButtonFactory(fromPartial({ DeleteServerModal: (props: DeleteServerModalProps) => , })); - const setUp = (children?: ReactNode) => { + const setUp = (children: ReactNode = 'Remove this server') => { const history = createMemoryHistory({ initialEntries: ['/foo'] }); const result = renderWithEvents( - {children} + {children} , ); @@ -30,7 +30,6 @@ describe('', () => { ['Foo bar'], ['baz'], ['something'], - [undefined], ])('renders expected content', (children) => { const { container } = setUp(children); expect(container.firstChild).toBeTruthy(); diff --git a/test/servers/__snapshots__/DeleteServerButton.test.tsx.snap b/test/servers/__snapshots__/DeleteServerButton.test.tsx.snap index 7d21e6cd..c012b222 100644 --- a/test/servers/__snapshots__/DeleteServerButton.test.tsx.snap +++ b/test/servers/__snapshots__/DeleteServerButton.test.tsx.snap @@ -2,67 +2,27 @@ exports[` > renders expected content 1`] = ` `; exports[` > renders expected content 2`] = ` `; exports[` > renders expected content 3`] = ` -`; - -exports[` > renders expected content 4`] = ` - `; From 5e0db07ef3868e974cbec611ee1a11f874554e28 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Apr 2025 12:14:27 +0200 Subject: [PATCH 19/19] Replace all remaining bootstrap utility classes with tailwind classes --- src/common/Home.tsx | 38 +++++++++---------- src/servers/ManageServers.tsx | 8 +++- src/servers/ManageServersRow.tsx | 2 +- src/servers/ServersDropdown.tsx | 8 ++-- src/servers/ServersListGroup.tsx | 9 ++--- src/servers/helpers/ImportServersBtn.tsx | 2 +- src/servers/helpers/ServerForm.tsx | 2 +- test/servers/ServersListGroup.test.tsx | 18 ++------- .../ManageServersRow.test.tsx.snap | 2 +- 9 files changed, 39 insertions(+), 50 deletions(-) diff --git a/src/common/Home.tsx b/src/common/Home.tsx index 4258ac65..9c48fdd0 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -45,26 +45,24 @@ export const Home = ({ servers }: HomeProps) => { > Welcome! - - {!hasServers && ( -
-

This application will help you manage your Shlink servers.

-

- -

-

- - - Learn more about Shlink - - - -

-
- )} -
+ {hasServers ? : ( +
+

This application will help you manage your Shlink servers.

+

+ +

+

+ + + Learn more about Shlink + + + +

+
+ )}
diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index d9ba7b17..04966585 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -63,13 +63,17 @@ const ManageServers: FCWithDeps = ({ serv - {hasAutoConnect && Auto-connect} + {hasAutoConnect && ( + Auto-connect + )} NameBase URLOptions )}> - {!filteredServers.length && No servers found.} + {!filteredServers.length && ( + No servers found. + )} {filteredServers.map((server) => ( ))} diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx index 91fd5067..b90a777c 100644 --- a/src/servers/ManageServersRow.tsx +++ b/src/servers/ManageServersRow.tsx @@ -27,7 +27,7 @@ const ManageServersRow: FCWithDeps {server.autoConnect && ( <> - + Auto-connect to this server diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index fbfe08c9..948cf1dd 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -17,7 +17,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp if (serversList.length === 0) { return ( - Add a server + Add a server ); } @@ -31,7 +31,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp ))} - Manage servers + Manage servers ); @@ -40,9 +40,9 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp return ( - Servers + Servers - {renderServers()} + {renderServers()} ); }; diff --git a/src/servers/ServersListGroup.tsx b/src/servers/ServersListGroup.tsx index 0d8d1af8..eed7824f 100644 --- a/src/servers/ServersListGroup.tsx +++ b/src/servers/ServersListGroup.tsx @@ -1,14 +1,14 @@ import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { clsx } from 'clsx'; -import type { FC, PropsWithChildren } from 'react'; +import type { FC } from 'react'; import { Link } from 'react-router'; import type { ServerWithId } from './data'; -type ServersListGroupProps = PropsWithChildren<{ +type ServersListGroupProps = { servers: ServerWithId[]; borderless?: boolean; -}>; +}; const ServerListItem = ({ id, name }: { id: string; name: string }) => ( ( ); -export const ServersListGroup: FC = ({ servers, children, borderless }) => ( +export const ServersListGroup: FC = ({ servers, borderless }) => ( <> - {children &&
{children}
} {servers.length > 0 && (
= ({ onSubmit, initialValues, child const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey })); return ( -
+ setName(e.target.value)} required /> setUrl(e.target.value)} required /> diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index cc2479c6..274d6f90 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -10,30 +10,18 @@ describe('', () => { fromPartial({ name: 'foo', id: '123' }), fromPartial({ name: 'bar', id: '456' }), ]; - const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; borderless?: boolean } = {}) => { - const { servers = [], withChildren = true, borderless } = params; + const setUp = (params: { servers?: ServerWithId[]; borderless?: boolean } = {}) => { + const { servers = [], borderless } = params; return render( - - {withChildren ? 'The list of servers' : undefined} - + , ); }; it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders title', () => { - setUp({}); - expect(screen.getByTestId('title')).toHaveTextContent('The list of servers'); - }); - - it('does not render title when children is not provided', () => { - setUp({ withChildren: false }); - expect(screen.queryByTestId('title')).not.toBeInTheDocument(); - }); - it.each([ [servers], [[]], diff --git a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap index 0ecf0a1a..855d1bb5 100644 --- a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap @@ -24,7 +24,7 @@ exports[` > renders auto-connect icon only if server is auto >