Browse Source

feat(frontend): migrate dashboard layout and components to Tailwind CSS v4

master
PouyaKhajavi 2 days ago
parent
commit
9696e09aa6
  1. 650
      frontend/package-lock.json
  2. 4
      frontend/package.json
  3. 366
      frontend/src/App.jsx
  4. 981
      frontend/src/index.css
  5. 6
      frontend/vite.config.js

650
frontend/package-lock.json

@ -8,10 +8,12 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
"axios": "^1.19.0",
"lucide-react": "^1.30.0",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@types/react": "^19.2.17",
@ -21,11 +23,55 @@
"vite": "^8.2.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@oxc-project/types": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
"integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
@ -361,7 +407,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -378,7 +423,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -395,7 +439,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -412,7 +455,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -429,7 +471,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -446,7 +487,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -463,7 +503,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -480,7 +519,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -497,7 +535,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -514,7 +551,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -531,7 +567,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -548,7 +583,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -565,7 +599,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -582,7 +615,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -596,9 +628,514 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
"integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.24.1",
"jiti": "^2.7.0",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.3"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"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-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
"integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.3",
"@tailwindcss/oxide-darwin-arm64": "4.3.3",
"@tailwindcss/oxide-darwin-x64": "4.3.3",
"@tailwindcss/oxide-freebsd-x64": "4.3.3",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
"@tailwindcss/oxide-linux-x64-musl": "4.3.3",
"@tailwindcss/oxide-wasm32-wasi": "4.3.3",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
"integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
"integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
"integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
"integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
"integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
"integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
"integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
"integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
"integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
"integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.11.1",
"@emnapi/runtime": "^1.11.1",
"@emnapi/wasi-threads": "^1.2.2",
"@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.2",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
"integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
"integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
"integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.3.3",
"@tailwindcss/oxide": "4.3.3",
"tailwindcss": "4.3.3"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@types/react": {
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
@ -737,7 +1274,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@ -757,6 +1293,19 @@
"node": ">= 0.4"
}
},
"node_modules/enhanced-resolve": {
"version": "5.24.5",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
"integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -806,7 +1355,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@ -860,7 +1408,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -929,6 +1476,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -981,11 +1534,19 @@
"node": ">= 6"
}
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@ -1018,7 +1579,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1039,7 +1599,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1060,7 +1619,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1081,7 +1639,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1102,7 +1659,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1123,7 +1679,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1144,7 +1699,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1165,7 +1719,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1186,7 +1739,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1207,7 +1759,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1228,7 +1779,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@ -1251,6 +1801,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1291,7 +1850,6 @@
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
"type": "github",
@ -1359,14 +1917,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@ -1379,7 +1935,6 @@
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -1438,7 +1993,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz",
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.143.0",
@ -1477,17 +2031,34 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tailwindcss": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@ -1504,7 +2075,6 @@
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",

4
frontend/package.json

@ -10,10 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
"axios": "^1.19.0",
"lucide-react": "^1.30.0",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@types/react": "^19.2.17",

366
frontend/src/App.jsx

@ -12,9 +12,7 @@ import {
Play,
X,
AlertTriangle,
RefreshCw,
ExternalLink,
Info
ExternalLink
} from 'lucide-react';
function App() {
@ -137,8 +135,6 @@ function App() {
const fetchLogs = async () => {
setLoadingLogs(true);
try {
// In Django REST, runs are queried per-crawler. We fetch all crawlers,
// then request runs for each and merge/sort them.
const crawlersList = crawlers.length > 0 ? crawlers : (await axios.get('/api/crawlers/')).data;
const promises = crawlersList.map(async (c) => {
try {
@ -256,7 +252,7 @@ function App() {
const triggerCrawler = async (id) => {
try {
const res = await axios.post(`/api/crawlers/${id}/trigger/`);
await axios.post(`/api/crawlers/${id}/trigger/`);
showToast('درخواست اجرای کرالر با موفقیت ارسال شد', 'success');
if (activeTab === 'logs') {
fetchLogs();
@ -289,75 +285,68 @@ function App() {
};
return (
<div className="app-container">
<div className="app-container flex flex-col md:flex-row min-h-screen relative font-fa">
{/* Toast Notification */}
{toast && (
<div style={{
position: 'fixed',
top: '20px',
left: '20px',
backgroundColor: toast.type === 'success' ? 'var(--success)' : toast.type === 'danger' ? 'var(--danger)' : 'var(--accent)',
color: 'white',
padding: '10px 20px',
borderRadius: '8px',
zIndex: 9999,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
direction: 'rtl',
animation: 'fadeIn 0.2s ease'
}}>
<div
className="fixed top-5 left-5 px-5 py-3 rounded-lg z-50 shadow-2xl text-white direction-rtl animate-fade-in"
style={{
backgroundColor: toast.type === 'success' ? '#10b981' : toast.type === 'danger' ? '#ef4444' : '#a855f7',
}}
>
{toast.message}
</div>
)}
{/* Sidebar */}
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-logo">
<ShieldAlert className="brand-icon" />
<aside className="w-full md:w-72 bg-slate-900 border-l border-white/5 flex flex-col p-6 sticky top-0 md:h-screen z-10 shrink-0">
<div className="flex items-center gap-3 pb-4 mb-10 border-b border-white/5">
<div className="bg-gradient-to-br from-purple-500 to-blue-500 w-11 h-11 rounded-xl flex items-center justify-center shadow-[0_0_15px_rgba(168,85,247,0.3)] text-white">
<ShieldAlert className="w-6 h-6" />
</div>
<div class="brand-info">
<h1>دیدبان دیوار</h1>
<span>کرال هوشمند با AI</span>
<div className="flex flex-col">
<h1 className="text-lg font-bold text-white leading-tight">دیدبان دیوار</h1>
<span className="text-[10px] text-gray-500">کرال هوشمند با AI</span>
</div>
</div>
<nav class="sidebar-nav">
<nav className="flex flex-row md:flex-col overflow-x-auto md:overflow-x-visible gap-2 flex-grow pb-2 md:pb-0">
<button
className={`nav-item ${activeTab === 'crawlers' ? 'active' : ''}`}
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'crawlers' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`}
onClick={() => switchTab('crawlers')}
>
<Layers />
<Layers className="w-5 h-5" />
<span>مدیریت کرالرها</span>
</button>
<button
className={`nav-item ${activeTab === 'ads' ? 'active' : ''}`}
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'ads' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`}
onClick={() => switchTab('ads')}
>
<Newspaper />
<Newspaper className="w-5 h-5" />
<span>فید آگهی‌ها</span>
</button>
<button
className={`nav-item ${activeTab === 'logs' ? 'active' : ''}`}
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'logs' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`}
onClick={() => switchTab('logs')}
>
<History />
<History className="w-5 h-5" />
<span>تاریخچه اجرا</span>
</button>
</nav>
<div class="sidebar-footer">
<span class="version-label">نسخه ۱ (MVP)</span>
<div className="pt-4 border-t border-white/5 text-center hidden md:block">
<span className="text-[11px] text-gray-500 font-en">نسخه ۱ (MVP)</span>
</div>
</aside>
{/* Main Content */}
<div className="main-content">
<div className="flex-grow flex flex-col min-w-0">
{/* Header */}
<header className="app-header">
<header className="bg-slate-950/70 backdrop-blur-md border-b border-white/5 flex items-center justify-between px-6 md:px-8 py-5 sticky top-0 z-9">
<div className="header-title">
<h2>
<h2 className="text-lg font-bold text-white">
{activeTab === 'crawlers' && 'مدیریت کرالرها'}
{activeTab === 'ads' && 'فید آگهی‌های دیوار'}
{activeTab === 'logs' && 'تاریخچه اجرای کرالرها'}
@ -365,13 +354,13 @@ function App() {
</div>
<div className="header-actions">
<div className="system-health">
<div className="health-item">
<span className={`dot ${health.database === 'healthy' ? 'green' : 'red'}`}></span>
<div className="flex items-center gap-5 bg-white/2 px-4 py-2 rounded-full border border-white/5 text-xs text-gray-400">
<div className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full inline-block ${health.database === 'healthy' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-rose-500 shadow-[0_0_8px_#ef4444]'}`}></span>
<span>دیتابیس</span>
</div>
<div className="health-item">
<span className={`dot ${health.redis === 'healthy' ? 'green' : 'red'}`}></span>
<div className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full inline-block ${health.redis === 'healthy' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-rose-500 shadow-[0_0_8px_#ef4444]'}`}></span>
<span>ردیس</span>
</div>
</div>
@ -379,84 +368,85 @@ function App() {
</header>
{/* Viewport */}
<main className="view-viewport">
<main className="p-6 md:p-8 flex-grow">
{/* TAB 1: Crawlers View */}
{activeTab === 'crawlers' && (
<div className="tab-view active">
<div className="view-actions-bar">
<button className="btn btn-primary" onClick={() => openCrawlerModal()}>
<Plus />
<div className="animate-fade-in">
<div className="flex justify-between items-center mb-8">
<button className="inline-flex items-center justify-center gap-2 border border-transparent rounded-lg px-4 py-2.5 text-sm font-medium cursor-pointer transition-all duration-200 bg-purple-500 text-white shadow-[0_4px_14px_rgba(168,85,247,0.3)] hover:bg-purple-600 hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(168,85,247,0.4)]" onClick={() => openCrawlerModal()}>
<Plus className="w-5 h-5" />
<span>تعریف کرالر جدید</span>
</button>
</div>
{loadingCrawlers ? (
<div className="loading-state">
<div className="spinner"></div>
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div>
<p>در حال بارگذاری کرالرها...</p>
</div>
) : crawlers.length === 0 ? (
<div className="empty-state">
<AlertTriangle />
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" />
<p>هیچ کرالری تعریف نشده است. جهت پایش دیوار، یک کرالر جدید ایجاد کنید.</p>
</div>
) : (
<div className="crawlers-grid">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{crawlers.map(c => (
<div className="card" key={c.id}>
<div className="card-header">
<h4 className="card-title" title={c.title}>{c.title}</h4>
<div className="toggle-group">
<label className="switch-container">
<div className="bg-slate-900/40 backdrop-blur-md border border-white/5 rounded-2xl p-6 shadow-xl hover:bg-slate-900/60 hover:border-purple-500/40 transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl flex flex-col relative overflow-hidden" key={c.id}>
<div className="flex justify-between items-start mb-4 gap-4">
<h4 className="text-lg font-bold text-white overflow-hidden text-overflow-ellipsis whitespace-nowrap max-w-[200px]" title={c.title}>{c.title}</h4>
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={c.is_active}
onChange={() => toggleCrawlerActive(c)}
className="sr-only peer"
/>
<span className="switch-slider"></span>
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div>
</label>
</div>
</div>
<div className="card-body">
<div className="card-field">
<span className="card-field-label">لینک فیلتر دیوار</span>
<span className="card-field-value url" title={c.divar_url}>{c.divar_url}</span>
<div className="flex-grow flex flex-col gap-3 mb-5">
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-500">لینک فیلتر دیوار</span>
<span className="text-xs text-blue-400 font-en direction-ltr break-all overflow-hidden text-ellipsis whitespace-nowrap block" title={c.divar_url}>{c.divar_url}</span>
</div>
<div className="card-field">
<span className="card-field-label">پرامپت ارزیابی AI</span>
<span className="card-field-value prompt" title={c.detection_prompt}>{c.detection_prompt}</span>
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-500">پرامپت ارزیابی AI</span>
<span className="text-[13px] text-gray-300 line-clamp-2" title={c.detection_prompt}>{c.detection_prompt}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
<div className="card-field">
<span className="card-field-label">فاصله زمانی پایش</span>
<span className="card-field-value arabic-num-font">{c.interval_minutes} دقیقه</span>
<div className="grid grid-cols-2 gap-3.5">
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-500">فاصله زمانی پایش</span>
<span className="text-xs text-gray-300 font-en">{c.interval_minutes}m</span>
</div>
<div className="card-field">
<span className="card-field-label">بازه زمانی مجاز</span>
<span className="card-field-value arabic-num-font">{c.start_hour.substring(0,5)} الی {c.end_hour.substring(0,5)}</span>
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-500">بازه زمانی مجاز</span>
<span className="text-xs text-gray-300 font-en">{c.start_hour.substring(0,5)} - {c.end_hour.substring(0,5)}</span>
</div>
</div>
{c.telegram_channel_id && (
<div className="card-field">
<span className="card-field-label">کانال اعلانات تلگرام</span>
<span className="card-field-value font-mono">{c.telegram_channel_id}</span>
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-500">کانال اعلانات تلگرام</span>
<span className="text-xs text-purple-400 font-mono">{c.telegram_channel_id}</span>
</div>
)}
</div>
<div className="card-footer">
<button className="btn btn-secondary btn-icon-only" title="اجرای فوری" onClick={() => triggerCrawler(c.id)}>
<Play style={{ fill: 'currentColor' }} />
<div className="flex justify-between items-center border-t border-white/5 pt-4 mt-auto">
<button className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-white hover:bg-white/10" title="اجرای فوری" onClick={() => triggerCrawler(c.id)}>
<Play className="w-5 h-5 fill-current" />
</button>
<div className="card-actions">
<button className="btn btn-secondary btn-icon-only" title="ویرایش" onClick={() => openCrawlerModal(c)}>
<Edit2 />
<div className="flex items-center gap-2">
<button className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-white hover:bg-white/10" title="ویرایش" onClick={() => openCrawlerModal(c)}>
<Edit2 className="w-4 h-4" />
</button>
<button className="btn btn-danger btn-icon-only" title="حذف" onClick={() => deleteCrawler(c.id)}>
<Trash2 />
<button className="inline-flex items-center justify-center border border-transparent rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-rose-500 text-white shadow-[0_4px_14px_rgba(239,68,68,0.2)] hover:bg-rose-600 hover:-translate-y-0.5" title="حذف" onClick={() => deleteCrawler(c.id)}>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
@ -469,22 +459,27 @@ function App() {
{/* TAB 2: Ads View */}
{activeTab === 'ads' && (
<div className="tab-view active">
<div className="ads-filters-bar">
<div className="search-box">
<Search className="search-icon" />
<div className="animate-fade-in">
<div className="flex flex-col lg:flex-row justify-between gap-5 bg-slate-900/40 p-5 rounded-2xl border border-white/5 mb-8 items-center">
<div className="relative w-full lg:max-w-md">
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-gray-500" />
<input
type="text"
placeholder="جستجو در فید آگهی‌ها..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-black/20 border border-white/5 rounded-lg py-2.5 pr-10 pl-4 text-white text-sm outline-none transition-all duration-200 focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.15)]"
/>
</div>
<div className="filters-group">
<div className="filter-item">
<div className="flex items-center gap-6 flex-wrap w-full lg:w-auto justify-between lg:justify-end">
<div className="flex items-center gap-2 text-sm text-gray-300">
<label>فیلتر کرالر:</label>
<select value={filterTaskId} onChange={(e) => setFilterTaskId(e.target.value)}>
<select
value={filterTaskId}
onChange={(e) => setFilterTaskId(e.target.value)}
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500"
>
<option value="">همه کرالرها</option>
{crawlers.map(c => (
<option key={c.id} value={c.id}>{c.title}</option>
@ -492,69 +487,70 @@ function App() {
</select>
</div>
<div className="filter-item toggle-filter">
<label className="switch-container">
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={filterOnlyFlagged}
onChange={(e) => setFilterOnlyFlagged(e.target.checked)}
className="sr-only peer"
/>
<span class="switch-slider"></span>
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div>
</label>
<span className="toggle-label">فقط پرچم‌گذاری شده (AI)</span>
<span className="text-sm text-gray-300">فقط پرچم‌گذاری شده (AI)</span>
</div>
</div>
</div>
{loadingAds ? (
<div className="loading-state">
<div className="spinner"></div>
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div>
<p>در حال بارگذاری آگهی‌ها...</p>
</div>
) : getFilteredAds().length === 0 ? (
<div className="empty-state">
<AlertTriangle />
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" />
<p>هیچ آگهی با معیارهای فیلتر یافت نشد.</p>
</div>
) : (
<div className="ads-grid">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{getFilteredAds().map(adEval => (
<div className={`card ad-card ${adEval.is_flagged ? 'flagged' : ''}`} key={adEval.id}>
<div className="ad-meta-top">
<span className="ad-published-at">{formatDate(adEval.evaluated_at)}</span>
<div className={`bg-slate-900/40 backdrop-blur-md rounded-2xl p-6 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl flex flex-col relative overflow-hidden border-t-4 ${adEval.is_flagged ? 'border-purple-500 bg-gradient-to-b from-purple-500/3 to-slate-900/40 shadow-[0_8px_32px_rgba(168,85,247,0.05)]' : 'border-white/5'}`} key={adEval.id}>
<div className="flex justify-between items-center mb-2 w-full text-xs text-gray-500">
<span className="font-en">{formatDate(adEval.evaluated_at)}</span>
{adEval.ad.price && (
<span className="ad-price">{adEval.ad.price}</span>
<span className="text-[13px] font-bold text-purple-400">{adEval.ad.price}</span>
)}
</div>
<h4 className="card-title" style={{ maxWidth: '100%', marginBottom: '0.5rem' }}>
<h4 className="text-base font-bold text-white mb-2 leading-snug">
{adEval.ad.title}
</h4>
<p className="ad-desc">{adEval.ad.description}</p>
<p className="text-xs.5 text-gray-400 line-clamp-3 mb-4 leading-relaxed break-all">{adEval.ad.description}</p>
{adEval.is_flagged && (
<div className="ai-flag-box">
<div className="ai-flag-header">
<div className="bg-purple-500/8 border border-purple-500/20 rounded-xl p-4 text-sm mb-4">
<div className="flex items-center justify-between text-purple-400 font-bold mb-1.5">
<span>تحلیل هوش مصنوعی (پرچم‌گذاری شده)</span>
{adEval.confidence && (
<span className="ai-confidence-badge">اطمینان: {Math.round(adEval.confidence * 100)}٪</span>
<span className="text-[11px] bg-purple-500/25 px-1.5 py-0.5 rounded font-en">Confidence: {Math.round(adEval.confidence * 100)}%</span>
)}
</div>
<p className="ai-flag-reason">{adEval.reason}</p>
<p className="text-gray-300 text-xs.5 leading-relaxed">{adEval.reason}</p>
</div>
)}
<div className="ad-tag-list">
<span className="ad-tag">توکن: {adEval.ad.divar_token}</span>
<div className="flex flex-wrap gap-2 mb-4">
<span className="bg-white/2 border border-white/5 px-2 py-0.5 rounded text-[11px] text-gray-400 font-en">Token: {adEval.ad.divar_token}</span>
{adEval.ad.category && (
<span className="ad-tag">{adEval.ad.category}</span>
<span className="bg-white/2 border border-white/5 px-2 py-0.5 rounded text-[11px] text-gray-400">{adEval.ad.category}</span>
)}
</div>
<div className="card-footer" style={{ borderTop: '1px solid var(--border-color)', paddingTop: '1rem', marginTop: 'auto' }}>
<a href={adEval.ad.url} target="_blank" rel="noopener noreferrer" className="btn btn-secondary btn-icon-only" title="مشاهده در دیوار" style={{ color: 'var(--info)' }}>
<ExternalLink />
<div className="flex justify-between items-center border-t border-white/5 pt-4 mt-auto">
<a href={adEval.ad.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-blue-400 hover:bg-white/10" title="مشاهده در دیوار">
<ExternalLink className="w-5 h-5" />
</a>
</div>
</div>
@ -566,51 +562,51 @@ function App() {
{/* TAB 3: Logs View */}
{activeTab === 'logs' && (
<div className="tab-view active">
<div className="animate-fade-in">
{loadingLogs ? (
<div className="loading-state">
<div className="spinner"></div>
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div>
<p>در حال دریافت تاریخچه اجرا...</p>
</div>
) : logs.length === 0 ? (
<div className="empty-state">
<AlertTriangle />
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400">
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" />
<p>هیچ لاگ تاریخی ثبت نشده است.</p>
</div>
) : (
<div className="table-container">
<table className="data-table">
<thead>
<div className="bg-slate-900/40 backdrop-blur-md border border-white/5 rounded-2xl shadow-xl overflow-hidden w-full overflow-x-auto">
<table className="min-w-full text-right text-sm text-gray-300 border-collapse">
<thead className="bg-black/20 text-white font-bold">
<tr>
<th>کرالر</th>
<th>شروع اجرا</th>
<th>پایان اجرا</th>
<th>وضعیت</th>
<th>آگهی یافت شده</th>
<th>پردازش AI</th>
<th>پرچم‌گذاری شده</th>
<th>لاگ خطا</th>
<th className="px-5 py-4 border-b border-white/5">کرالر</th>
<th className="px-5 py-4 border-b border-white/5">شروع اجرا</th>
<th className="px-5 py-4 border-b border-white/5">پایان اجرا</th>
<th className="px-5 py-4 border-b border-white/5">وضعیت</th>
<th className="px-5 py-4 border-b border-white/5">آگهی یافت شده</th>
<th className="px-5 py-4 border-b border-white/5">پردازش AI</th>
<th className="px-5 py-4 border-b border-white/5">پرچم‌گذاری شده</th>
<th className="px-5 py-4 border-b border-white/5">لاگ خطا</th>
</tr>
</thead>
<tbody>
{logs.map(run => (
<tr key={run.id}>
<td style={{ fontWeight: 600 }}>{run.crawler_title}</td>
<td className="arabic-num-font">{formatDate(run.started_at)}</td>
<td className="arabic-num-font">{formatDate(run.finished_at)}</td>
<td>
<span className={`badge-status ${run.status.toLowerCase()}`}>
<tr key={run.id} className="hover:bg-white/1">
<td className="px-5 py-4 border-b border-white/5 font-semibold">{run.crawler_title}</td>
<td className="px-5 py-4 border-b border-white/5 font-en text-xs">{formatDate(run.started_at)}</td>
<td className="px-5 py-4 border-b border-white/5 font-en text-xs">{formatDate(run.finished_at)}</td>
<td className="px-5 py-4 border-b border-white/5">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium inline-flex items-center gap-1 border ${run.status === 'SUCCESS' ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30' : run.status === 'FAILED' ? 'bg-rose-500/10 text-rose-400 border-rose-500/30' : 'bg-amber-500/10 text-amber-400 border-amber-500/30'}`}>
{run.status === 'RUNNING' && 'در حال اجرا'}
{run.status === 'SUCCESS' && 'موفق'}
{run.status === 'FAILED' && 'ناموفق'}
</span>
</td>
<td className="arabic-num-font">{run.ads_fetched_count}</td>
<td className="arabic-num-font">{run.ads_evaluated_count}</td>
<td className="arabic-num-font" style={{ color: run.ads_flagged_count > 0 ? 'var(--accent)' : 'inherit', fontWeight: run.ads_flagged_count > 0 ? 700 : 'normal' }}>
<td className="px-5 py-4 border-b border-white/5 font-en">{run.ads_fetched_count}</td>
<td className="px-5 py-4 border-b border-white/5 font-en">{run.ads_evaluated_count}</td>
<td className={`px-5 py-4 border-b border-white/5 font-en ${run.ads_flagged_count > 0 ? 'text-purple-400 font-bold' : ''}`}>
{run.ads_flagged_count}
</td>
<td style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={run.error_log}>
<td className="px-5 py-4 border-b border-white/5 max-w-[180px] overflow-hidden text-ellipsis whitespace-nowrap" title={run.error_log}>
{run.error_log || '-'}
</td>
</tr>
@ -626,67 +622,67 @@ function App() {
</div>
{/* Form Dialog Modal */}
<div className={`modal-overlay ${modalOpen ? 'open' : ''}`}>
<div className="modal-card">
<div className="modal-header">
<h3>{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}</h3>
<button className="close-btn" onClick={closeCrawlerModal}>
<X />
<div className={`fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 transition-opacity duration-200 ${modalOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}>
<div className={`bg-slate-900 border border-white/5 rounded-2xl w-[600px] max-w-[95%] max-h-[90vh] shadow-[0_20px_50px_rgba(0,0,0,0.6)] flex flex-col transition-transform duration-200 ${modalOpen ? 'scale-100' : 'scale-95'}`}>
<div className="flex items-center justify-between p-5 border-b border-white/5">
<h3 className="text-base font-bold text-white">{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}</h3>
<button className="bg-transparent border-none text-gray-400 cursor-pointer flex items-center hover:text-white" onClick={closeCrawlerModal}>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleFormSubmit}>
<div className="modal-body">
<form onSubmit={handleFormSubmit} className="flex flex-col min-h-0">
<div className="p-6 overflow-y-auto flex flex-col gap-5">
{formError && (
<div style={{
backgroundColor: 'rgba(239, 68, 68, 0.1)',
border: '1px solid var(--danger)',
padding: '10px',
borderRadius: '6px',
fontSize: '0.85rem',
color: '#fca5a5'
}}>
<div className="bg-rose-500/10 border border-rose-500/30 p-3 rounded-lg text-xs text-rose-300">
{formError}
</div>
)}
<div className="form-group">
<label>عنوان کرالر</label>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">عنوان کرالر</label>
<input
type="text"
value={formTitle}
onChange={(e) => setFormTitle(e.target.value)}
placeholder="مثال: رهن کامل فوری نیاوران"
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]"
required
/>
</div>
<div className="form-group">
<label>لینک فیلتر دیوار (باید با https://divar.ir/s/ شروع شود)</label>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">لینک فیلتر دیوار (باید با https://divar.ir/s/ شروع شود)</label>
<input
type="url"
value={formUrl}
onChange={(e) => setFormUrl(e.target.value)}
placeholder="https://divar.ir/s/tehran/rent-apartment/niavaran?..."
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]"
required
/>
</div>
<div className="form-group">
<label>پرامپت ارزیابی هوش مصنوعی (AI)</label>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">پرامپت ارزیابی هوش مصنوعی (AI)</label>
<textarea
value={formPrompt}
onChange={(e) => setFormPrompt(e.target.value)}
placeholder="بررسی کن آیا آگهی پول لازم و رهن کامل است یا خیر..."
rows={3}
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)] resize-vertical"
required
/>
</div>
<div className="form-row">
<div className="form-group">
<label>بازه زمانی پایش</label>
<select value={formInterval} onChange={(e) => setFormInterval(e.target.value)}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">بازه زمانی پایش</label>
<select
value={formInterval}
onChange={(e) => setFormInterval(e.target.value)}
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500"
>
<option value="5">۵ دقیقه</option>
<option value="15">۱۵ دقیقه</option>
<option value="30">۳۰ دقیقه</option>
@ -699,56 +695,60 @@ function App() {
</select>
</div>
<div className="form-group">
<label>آیدی کانال تلگرام (اختیاری)</label>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">آیدی کانال تلگرام (اختیاری)</label>
<input
type="text"
value={formTelegram}
onChange={(e) => setFormTelegram(e.target.value)}
placeholder="مثال: @my_alerts"
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]"
/>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label>ساعت شروع پنجره مجاز</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">ساعت شروع پنجره مجاز</label>
<input
type="time"
value={formStartHour}
onChange={(e) => setFormStartHour(e.target.value)}
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500"
required
/>
</div>
<div className="form-group">
<label>ساعت پایان پنجره مجاز</label>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-gray-400 font-medium">ساعت پایان پنجره مجاز</label>
<input
type="time"
value={formEndHour}
onChange={(e) => setFormEndHour(e.target.value)}
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500"
required
/>
</div>
</div>
<div className="form-group toggle-group">
<span className="toggle-label" style={{ fontSize: '0.85rem' }}>کرالر فعال باشد</span>
<label className="switch-container">
<div className="flex flex-row justify-between items-center bg-white/1 p-3.5 rounded-lg border border-white/5">
<span className="text-xs text-gray-300 font-medium">کرالر فعال باشد</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={formIsActive}
onChange={(e) => setFormIsActive(e.target.checked)}
className="sr-only peer"
/>
<span class="switch-slider"></span>
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div>
</label>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={closeCrawlerModal}>انصراف</button>
<button type="submit" className="btn btn-primary">ذخیره</button>
<div className="flex justify-end gap-2 p-5 border-t border-white/5">
<button type="button" className="inline-flex items-center justify-center border border-white/5 rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 text-white hover:bg-white/10" onClick={closeCrawlerModal}>انصراف</button>
<button type="submit" className="inline-flex items-center justify-center border border-transparent rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-purple-500 text-white shadow-[0_4px_14px_rgba(168,85,247,0.3)] hover:bg-purple-600 hover:-translate-y-0.5">ذخیره</button>
</div>
</form>
</div>

981
frontend/src/index.css
File diff suppressed because it is too large
View File

6
frontend/vite.config.js

@ -1,9 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [
react(),
tailwindcss(),
],
server: {
proxy: {
'/api': {

Loading…
Cancel
Save