diff --git a/package-lock.json b/package-lock.json index e674ad2..bc68514 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@fortawesome/fontawesome-free": "^7.1.0", "axios": "^1.13.2", + "clsx": "^2.1.1", + "framer-motion": "^12.27.0", "i18next": "^25.7.4", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -23,16 +25,33 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.18", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", "vite": "^5.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1362,6 +1381,277 @@ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1543,6 +1833,43 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -1668,6 +1995,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1780,6 +2116,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "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" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1804,6 +2150,20 @@ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "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", @@ -2185,6 +2545,47 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.27.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.27.0.tgz", + "integrity": "sha512-gJtqOKEDJH/jrn0PpsWp64gdOjBvGX8hY6TWstxjDot/85daIEtJHl1UsiwHSXiYmJF2QXUoXP6/3gGw5xY2YA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.27.0", + "motion-utils": "^12.24.10", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2307,6 +2708,13 @@ "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==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2482,6 +2890,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -2587,6 +3005,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "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-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "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/livekit-client": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.16.1.tgz", @@ -2649,6 +3328,16 @@ "yallist": "^3.0.2" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/maplibre-gl": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.15.0.tgz", @@ -2733,6 +3422,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.27.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.0.tgz", + "integrity": "sha512-oDjl0WoAsWIWKl3GCDxmh7GITrNjmLX+w5+jwk4+pzLu3VnFvsOv2E6+xCXeH72O65xlXsr84/otiOYQKW/nQA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.24.10" + } + }, + "node_modules/motion-utils": { + "version": "12.24.10", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz", + "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2906,6 +3610,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2915,6 +3620,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/potpack": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", @@ -3212,6 +3924,27 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", diff --git a/package.json b/package.json index 279aac2..1af8938 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dependencies": { "@fortawesome/fontawesome-free": "^7.1.0", "axios": "^1.13.2", + "clsx": "^2.1.1", + "framer-motion": "^12.27.0", "i18next": "^25.7.4", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -25,13 +27,17 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.18", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", "vite": "^5.0.0" }, "overrides": { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..a7f73a2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/src/components/Posts/PostCardNew.jsx b/src/components/Posts/PostCardNew.jsx new file mode 100644 index 0000000..05c70ad --- /dev/null +++ b/src/components/Posts/PostCardNew.jsx @@ -0,0 +1,892 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import { loadAuth } from "../../auth/authStorage"; +import { + fetchPostLikeState, + recordPostShare, + togglePostLike, + updatePostVisibility, + fetchProfile, + editPost, + deletePost, +} from "../../api/client"; +import { useAuth } from "../../auth/AuthContext"; +import { trackEvent } from "../../utils/analytics"; +import { BlurImage, GalleryImage } from "../ui/BlurImage"; + +// ───────────────────────────────────────────────────────────────────────────── +// Utilities +// ───────────────────────────────────────────────────────────────────────────── + +function isAuthed() { + try { + const auth = loadAuth(); + return !!(auth && auth.access_token); + } catch { + return false; + } +} + +function requestAuth(message) { + try { + window.dispatchEvent( + new CustomEvent("sociowire:auth-required", { + detail: { message, mode: "login" }, + }) + ); + } catch {} + trackEvent("auth_required", { context: "wall", mode: "login" }); +} + +function formatRelativeTime(createdAt) { + try { + if (!createdAt) return "now"; + const d = createdAt instanceof Date ? createdAt : new Date(String(createdAt)); + if (Number.isNaN(d.getTime())) return "now"; + const s = Math.floor((Date.now() - d.getTime()) / 1000); + if (s < 60) return "now"; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m`; + const h = Math.floor(m / 60); + if (h < 48) return `${h}h`; + const days = Math.floor(h / 24); + return `${days}d`; + } catch { + return "now"; + } +} + +function formatCount(value) { + const n = Number(value); + if (!Number.isFinite(n)) return "0"; + if (n < 1000) return String(Math.max(0, Math.floor(n))); + if (n < 1000000) return `${(n / 1000).toFixed(1)}k`; + return `${(n / 1000000).toFixed(1)}m`; +} + +function categoryLabel(post) { + const c = (post?.category || "NEWS").toString().toUpperCase(); + if (c === "EVENT") return "EVENTS"; + return c; +} + +function categoryIconClass(post) { + const c = (post?.category || "NEWS").toString().toUpperCase(); + switch (c) { + case "EVENT": + case "EVENTS": + return "fa-regular fa-calendar"; + case "FRIENDS": + return "fa-solid fa-user-group"; + case "MARKET": + return "fa-solid fa-store"; + default: + return "fa-regular fa-newspaper"; + } +} + +function getCategoryBadgeClass(category) { + const c = (category || "NEWS").toString().toUpperCase(); + switch (c) { + case "BREAKING": + return "badge-breaking"; + case "MARKET": + return "badge-market"; + case "FRIENDS": + return "badge-friends"; + case "EVENT": + case "EVENTS": + return "badge-events"; + case "LIVE": + return "badge-live"; + default: + return "badge-news"; + } +} + +function normalizeImageUrl(raw) { + const v = (raw || "").toString().trim(); + if (!v) return ""; + const lower = v.toLowerCase(); + if (["null", "undefined", "none", "false", "0", "self", "default"].includes(lower)) return ""; + if (lower.startsWith("data:")) return ""; + if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("//") || lower.startsWith("/") || lower.startsWith("./")) { + return v; + } + return ""; +} + +// Avatar cache +const wallAvatarCache = new Map(); +const wallAvatarInflight = new Map(); + +async function getAvatarForUser(name) { + const key = (name || "").toLowerCase().trim(); + if (!key) return ""; + if (wallAvatarCache.has(key)) return wallAvatarCache.get(key); + if (wallAvatarInflight.has(key)) return wallAvatarInflight.get(key); + const p = (async () => { + const profile = await fetchProfile(name); + const url = profile?.avatar_url ? String(profile.avatar_url) : ""; + wallAvatarCache.set(key, url); + wallAvatarInflight.delete(key); + return url; + })().catch(() => { + wallAvatarCache.set(key, ""); + wallAvatarInflight.delete(key); + return ""; + }); + wallAvatarInflight.set(key, p); + return p; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Animation Variants +// ───────────────────────────────────────────────────────────────────────────── + +const cardVariants = { + hidden: { opacity: 0, y: 20, scale: 0.98 }, + visible: { + opacity: 1, + y: 0, + scale: 1, + transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] }, + }, + hover: { + y: -4, + scale: 1.005, + transition: { duration: 0.3, ease: "easeOut" }, + }, + tap: { scale: 0.995 }, +}; + +const buttonVariants = { + hover: { scale: 1.05, y: -1 }, + tap: { scale: 0.95 }, +}; + +const statsVariants = { + initial: { scale: 1 }, + pulse: { + scale: [1, 1.2, 1], + transition: { duration: 0.3 }, + }, +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Sub-components +// ───────────────────────────────────────────────────────────────────────────── + +function ActionButton({ icon, label, active, danger, onClick, children }) { + return ( + + + {label && {label}} + {children} + + ); +} + +function StatItem({ icon, count, onClick }) { + const [didPulse, setDidPulse] = useState(false); + + return ( + setDidPulse(false)} + > + + {formatCount(count)} + + ); +} + +function ImageFallback({ category, iconClass }) { + return ( +
+
+ + + {category} + +
+
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Main Component +// ───────────────────────────────────────────────────────────────────────────── + +export default function PostCardNew({ post, selected, onSelect, onPostDeleted, onPostUpdated }) { + const { t } = useTranslation(); + const [liked, setLiked] = useState(false); + const [authed, setAuthed] = useState(isAuthed()); + const [activeImageIndex, setActiveImageIndex] = useState(0); + const [failedImageIndices, setFailedImageIndices] = useState(() => new Set()); + const [imgFailed, setImgFailed] = useState(false); + const [showPrivacy, setShowPrivacy] = useState(false); + const [privacySaving, setPrivacySaving] = useState(false); + const [privacyError, setPrivacyError] = useState(""); + const [postVisibility, setPostVisibility] = useState((post?.visibility || "public").toString()); + const [postGroupId, setPostGroupId] = useState((post?.group_id || "").toString()); + const [postUsers, setPostUsers] = useState(""); + const [applyToImages, setApplyToImages] = useState(true); + const [counts, setCounts] = useState(() => ({ + like: post?.like_count ?? 0, + comment: post?.comment_count ?? 0, + share: post?.share_count ?? 0, + })); + const { username, token, userId } = useAuth(); + + // Edit/Delete state + const [showEditDelete, setShowEditDelete] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editTitle, setEditTitle] = useState(""); + const [editSnippet, setEditSnippet] = useState(""); + const [editSaving, setEditSaving] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); + + const postId = post?.id ?? post?._id ?? 0; + + // Fetch like state + useEffect(() => { + let active = true; + if (!authed || !postId) return () => {}; + fetchPostLikeState(postId).then((res) => { + if (!active) return; + if (res?.ok && res.liked) setLiked(true); + }); + return () => { active = false; }; + }, [authed, postId]); + + // Auth change listener + useEffect(() => { + const handler = () => { + const next = isAuthed(); + setAuthed(next); + if (!next) setLiked(false); + }; + window.addEventListener("sociowire:auth-changed", handler); + return () => window.removeEventListener("sociowire:auth-changed", handler); + }, []); + + // Sync counts + useEffect(() => { + setCounts({ + like: post?.like_count ?? 0, + comment: post?.comment_count ?? 0, + share: post?.share_count ?? 0, + }); + }, [post?.like_count, post?.comment_count, post?.share_count]); + + useEffect(() => { + setPostVisibility((post?.visibility || "public").toString()); + setPostGroupId((post?.group_id || "").toString()); + }, [post?.visibility, post?.group_id]); + + // Media URLs + const mediaUrls = useMemo(() => { + const list = []; + const seen = new Set(); + const addUrl = (value) => { + const normalized = normalizeImageUrl(value); + if (!normalized || seen.has(normalized)) return; + seen.add(normalized); + list.push(normalized); + }; + if (Array.isArray(post?.media_urls)) { + post.media_urls.forEach(addUrl); + } + [post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url].forEach(addUrl); + return list; + }, [post?.media_urls, post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url]); + + const mediaKey = mediaUrls.join("|"); + + useEffect(() => { + setActiveImageIndex(0); + setFailedImageIndices(new Set()); + setImgFailed(false); + }, [mediaKey]); + + const title = post?.title || "Untitled"; + const snippet = post?.snippet || ""; + const author = (post?.author || post?.Author || "anon").toString(); + const authorId = post?.author_id || post?.authorId || null; + const isAuthor = + (username && author && author.toLowerCase() === username.toLowerCase()) || + (userId && authorId && Number(userId) === Number(authorId)); + const [authorAvatar, setAuthorAvatar] = useState(""); + const sub = (post?.sub_category || post?.subCategory || "").toString(); + const contentType = (post?.content_type || "").toString().toLowerCase(); + const isLiveContent = contentType === "camera" || contentType === "live" || contentType === "stream"; + const timeAgo = isLiveContent ? "LIVE" : formatRelativeTime(post?.created_at || post?.CreatedAt || post?.createdAt); + const cat = categoryLabel(post); + const badgeClass = getCategoryBadgeClass(cat); + + const metaLine = useMemo(() => { + const parts = []; + if (sub) parts.push(sub); + if (post?.region) parts.push(post.region); + return parts.join(" • "); + }, [sub, post?.region]); + + const safeImageIndex = mediaUrls.length ? Math.min(Math.max(activeImageIndex, 0), mediaUrls.length - 1) : 0; + const activeImage = mediaUrls[safeImageIndex] || ""; + const hasGalleryImage = Boolean(activeImage && !imgFailed); + + const findNextAvailableIndex = (current, failed) => { + if (mediaUrls.length === 0) return -1; + for (let offset = 1; offset <= mediaUrls.length; offset++) { + const idx = (current + offset) % mediaUrls.length; + if (!failed.has(idx)) return idx; + } + return -1; + }; + + const findPrevAvailableIndex = (current, failed) => { + if (mediaUrls.length === 0) return -1; + for (let offset = 1; offset <= mediaUrls.length; offset++) { + const idx = (current - offset + mediaUrls.length) % mediaUrls.length; + if (!failed.has(idx)) return idx; + } + return -1; + }; + + const handleImageError = () => { + if (!activeImage) { + setImgFailed(true); + return; + } + const nextFailed = new Set(failedImageIndices); + nextFailed.add(safeImageIndex); + setFailedImageIndices(nextFailed); + if (nextFailed.size >= mediaUrls.length) { + setImgFailed(true); + return; + } + const nextIndex = findNextAvailableIndex(safeImageIndex, nextFailed); + if (nextIndex >= 0) setActiveImageIndex(nextIndex); + }; + + // Fetch author avatar + useEffect(() => { + let cancelled = false; + const who = (author || "").trim(); + if (!who) { + setAuthorAvatar(""); + return; + } + getAvatarForUser(who).then((url) => { + if (cancelled) return; + setAuthorAvatar(url || ""); + }); + return () => { cancelled = true; }; + }, [author]); + + // ───────────────────────────────────────────────────────────────────────────── + // Handlers + // ───────────────────────────────────────────────────────────────────────────── + + const handleLike = async (e) => { + e.stopPropagation(); + if (!authed) { + requestAuth(t("post.signInToLike")); + return; + } + trackEvent("post_like", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat }); + const res = await togglePostLike(postId, !liked); + if (res?.ok) { + setLiked(!!res.liked); + if (res?.counts) { + setCounts({ + like: res.counts.like_count, + comment: res.counts.comment_count, + share: res.counts.share_count, + }); + } + } + }; + + const handleComment = (e) => { + e.stopPropagation(); + if (!authed) { + requestAuth(t("post.signInToComment")); + return; + } + trackEvent("post_comment", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat }); + if (onSelect) onSelect(post); + }; + + const handleShare = async (e) => { + e.stopPropagation(); + const shareUrl = `${window.location.origin}/p/${postId}`; + trackEvent("post_share", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat }); + try { + if (navigator.share) { + await navigator.share({ title: title || "SocioWire", url: shareUrl }); + } else if (navigator.clipboard) { + await navigator.clipboard.writeText(shareUrl); + } + } catch {} + const res = await recordPostShare(postId); + if (res?.counts) { + setCounts({ + like: res.counts.like_count, + comment: res.counts.comment_count, + share: res.counts.share_count, + }); + } + }; + + const handleSavePrivacy = async () => { + setPrivacyError(""); + setPrivacySaving(true); + try { + const payload = { + visibility: postVisibility, + group_id: postGroupId.trim(), + users: postUsers.split(",").map((u) => u.trim()).filter(Boolean), + }; + if (applyToImages) { + payload.image_visibility = postVisibility; + payload.image_group_id = postGroupId.trim(); + payload.image_users = payload.users; + } + await updatePostVisibility(postId, payload, token); + setPrivacySaving(false); + } catch { + setPrivacyError(t("post.visibilityError")); + setPrivacySaving(false); + } + }; + + const handleSaveEdit = async () => { + setEditSaving(true); + const res = await editPost(postId, { title: editTitle.trim(), snippet: editSnippet.trim() }, token); + setEditSaving(false); + if (res?.ok) { + setIsEditing(false); + setShowEditDelete(false); + if (onPostUpdated) onPostUpdated(res); + } + }; + + const handleDelete = async () => { + setDeleting(true); + const res = await deletePost(postId, token); + setDeleting(false); + if (res?.ok) { + setShowDeleteConfirm(false); + setShowEditDelete(false); + if (onPostDeleted) onPostDeleted(postId); + } + }; + + // ───────────────────────────────────────────────────────────────────────────── + // Render + // ───────────────────────────────────────────────────────────────────────────── + + return ( + { + trackEvent("post_view", { + post_id: postId, + post_title: title?.slice(0, 100), + post_category: cat, + post_author: author, + content_type: "post", + }); + onSelect && onSelect(post); + }} + > + {/* ───────────────────────────────────────────────────────────────────────── + Header + ───────────────────────────────────────────────────────────────────────── */} +
+ {/* Avatar */} + + {authorAvatar ? ( + {author} + ) : ( +
+ {author ? author[0]?.toUpperCase() : "U"} +
+ )} +
+ + {/* Meta */} +
+ + {author} + +
+ {isLiveContent && } + {timeAgo} +
+
+ + {/* Badge */} + + {cat} + +
+ + {/* ───────────────────────────────────────────────────────────────────────── + Image/Gallery + ───────────────────────────────────────────────────────────────────────── */} + {hasGalleryImage ? ( + { + const prev = findPrevAvailableIndex(safeImageIndex, failedImageIndices); + if (prev >= 0) setActiveImageIndex(prev); + }} + onNext={() => { + const next = findNextAvailableIndex(safeImageIndex, failedImageIndices); + if (next >= 0) setActiveImageIndex(next); + }} + onError={handleImageError} + fallback={} + /> + ) : ( + + )} + + {/* Thumbnails */} + + {mediaUrls.length > 1 && ( + + {mediaUrls.map((url, idx) => ( + { + e.stopPropagation(); + setActiveImageIndex(idx); + }} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + ))} + + )} + + + {/* ───────────────────────────────────────────────────────────────────────── + Body + ───────────────────────────────────────────────────────────────────────── */} +
+

+ {title} +

+ {snippet && ( +

+ {snippet} +

+ )} + {metaLine && ( +
+ + {metaLine} +
+ )} +
+ + {/* ───────────────────────────────────────────────────────────────────────── + Footer + ───────────────────────────────────────────────────────────────────────── */} +
+ {/* Stats */} +
+ + + +
+ + {/* Actions */} +
+ + + + + {isAuthor && ( + <> + { + e.stopPropagation(); + setShowPrivacy((prev) => !prev); + setShowEditDelete(false); + }} + /> + { + e.stopPropagation(); + setShowEditDelete((prev) => !prev); + setShowPrivacy(false); + if (!showEditDelete) { + setEditTitle(post?.title || ""); + setEditSnippet(post?.snippet || ""); + } + }} + /> + + )} +
+
+ + {/* ───────────────────────────────────────────────────────────────────────── + Privacy Panel + ───────────────────────────────────────────────────────────────────────── */} + + {isAuthor && showPrivacy && ( + e.stopPropagation()} + > +
+
+ + +
+ + {postVisibility === "group" && ( + setPostGroupId(e.target.value)} + /> + )} + + {postVisibility === "custom" && ( + setPostUsers(e.target.value)} + /> + )} + + + + {privacyError && ( +
+ {privacyError} +
+ )} + + +
+
+ )} +
+ + {/* ───────────────────────────────────────────────────────────────────────── + Edit/Delete Panel + ───────────────────────────────────────────────────────────────────────── */} + + {isAuthor && showEditDelete && ( + e.stopPropagation()} + > + {!isEditing && !showDeleteConfirm && ( +
+ setIsEditing(true)} + /> + setShowDeleteConfirm(true)} + /> +
+ )} + + {isEditing && ( +
+ setEditTitle(e.target.value)} + /> +