diff --git a/.firebaserc b/.firebaserc index 03c8840..f359bf2 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,5 +1,9 @@ { "projects": { - "default": "myblogapp-4bae3" - } -} + "default": "myblogapp-4bae3", + "prod": "myblogapp-4bae3", + "staging": "liferecompiled-staging" + }, + "targets": {}, + "etags": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3bf199e..05fd4fa 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,12 @@ dist .yarn/install-state.gz .pnp.* .vscode/ + +# env files (never commit) +.env +.env.* +!.env.example + +# firebase local cache +.firebase/ + diff --git a/firebase.json b/firebase.json index a4b9c54..c717f36 100644 --- a/firebase.json +++ b/firebase.json @@ -15,11 +15,8 @@ ] }, "hosting": { - "public": "public", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [{ "source": "**", "destination": "/index.html" }] } } diff --git a/firestore.indexes.json b/firestore.indexes.json index 6eda1bd..2bc6e9d 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -11,8 +11,189 @@ { "fieldPath": "timestamp", "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "comments", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "userID", + "order": "ASCENDING" + }, + { + "fieldPath": "timestamp", + "order": "ASCENDING" + }, + { + "fieldPath": "__name__", + "order": "ASCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "category", + "order": "ASCENDING" + }, + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "ASCENDING" + }, + { + "fieldPath": "__name__", + "order": "ASCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "locked", + "order": "ASCENDING" + }, + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "deletedAt", + "order": "DESCENDING" + }, + { + "fieldPath": "__name__", + "order": "DESCENDING" + } + ], + "density": "SPARSE_ALL" + }, + { + "collectionGroup": "posts", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "deleted", + "order": "ASCENDING" + }, + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "title_lc", + "order": "ASCENDING" + }, + { + "fieldPath": "__name__", + "order": "ASCENDING" } - ] + ], + "density": "SPARSE_ALL" } ], "fieldOverrides": [] diff --git a/functions/index.js b/functions/index.js index 260206e..9fc7162 100644 --- a/functions/index.js +++ b/functions/index.js @@ -32,7 +32,7 @@ try { // -------------------- INIT -------------------- admin.initializeApp(); const { FieldValue, Timestamp } = require("firebase-admin/firestore"); -setGlobalOptions({ region: "europe-central2" }); +setGlobalOptions({ region: "europe-central2", invoker: "public" }); const db = admin.firestore(); //const isEmulator = !!process.env.FUNCTIONS_EMULATOR; diff --git a/package-lock.json b/package-lock.json index 9583930..ec25856 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,11 @@ "@cloudinary/react": "^1.13.1", "@cloudinary/url-gen": "^1.21.0", "@heroicons/react": "^2.2.0", - "bootstrap": "^5.3.3", "dayjs": "^1.11.13", "firebase": "^11.0.2", "framer-motion": "^12.4.10", "prop-types": "^15.8.1", "react": "^18.3.1", - "react-bootstrap": "^2.10.6", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", @@ -3380,16 +3378,6 @@ "node": ">=12" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3454,21 +3442,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-dnd/asap": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", @@ -3487,60 +3460,6 @@ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", "license": "MIT" }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", - "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@popperjs/core": "^2.11.8", - "@react-aria/ssr": "^3.5.0", - "@restart/hooks": "^0.5.0", - "@types/warning": "^3.0.3", - "dequal": "^2.0.3", - "dom-helpers": "^5.2.0", - "uncontrollable": "^8.0.4", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - } - }, - "node_modules/@restart/ui/node_modules/@restart/hooks": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", - "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui/node_modules/uncontrollable": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", - "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.14.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -3892,15 +3811,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -4216,12 +4126,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4238,15 +4150,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -4260,12 +4163,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", - "license": "MIT" - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -5144,25 +5041,6 @@ "node": ">= 0.8" } }, - "node_modules/bootstrap": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, "node_modules/boxen": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", @@ -6393,6 +6271,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/csv-parse": { @@ -6761,6 +6640,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6838,16 +6718,6 @@ "license": "MIT", "peer": true }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -9603,15 +9473,6 @@ "node": ">=12" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -12961,25 +12822,6 @@ "react-is": "^16.13.1" } }, - "node_modules/prop-types-extra": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", - "license": "MIT", - "dependencies": { - "react-is": "^16.3.2", - "warning": "^4.0.0" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, - "node_modules/prop-types-extra/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13263,37 +13105,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-bootstrap": { - "version": "2.10.10", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", - "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7", - "@restart/hooks": "^0.4.9", - "@restart/ui": "^1.9.4", - "@types/prop-types": "^15.7.12", - "@types/react-transition-group": "^4.4.6", - "classnames": "^2.3.2", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.8.1", - "prop-types-extra": "^1.1.0", - "react-transition-group": "^4.4.5", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "@types/react": ">=16.14.8", - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -13361,12 +13172,6 @@ "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -13482,22 +13287,6 @@ "react-dom": ">=16.14.0" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15886,21 +15675,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -16388,15 +16162,6 @@ "node": ">=18" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index bf7f3c5..6cec0f3 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,11 @@ "@cloudinary/react": "^1.13.1", "@cloudinary/url-gen": "^1.21.0", "@heroicons/react": "^2.2.0", - "bootstrap": "^5.3.3", "dayjs": "^1.11.13", "firebase": "^11.0.2", "framer-motion": "^12.4.10", "prop-types": "^15.8.1", "react": "^18.3.1", - "react-bootstrap": "^2.10.6", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 9e4df53..0000000 --- a/src/App.css +++ /dev/null @@ -1,9 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -#root { - padding: 0rem; - text-align: center; -} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 9114e0b..20e23ce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,8 +20,7 @@ import Stats from "./pages/dashboard/Stats"; import Trash from "./pages/dashboard/Trash"; import Settings from "./pages/dashboard/settings/Settings"; import ModerationPage from "./pages/dashboard/moderation/ModerationPage"; -// Stilovi -import "./App.css"; + function App() { return ( diff --git a/src/___legacy___/EditProfileModal.legacy.jsx b/src/___legacy___/EditProfileModal.legacy.jsx deleted file mode 100644 index 622db53..0000000 --- a/src/___legacy___/EditProfileModal.legacy.jsx +++ /dev/null @@ -1,290 +0,0 @@ -import { updateDoc, doc } from "firebase/firestore"; -import { db } from "../firebase"; -import { PropTypes } from "prop-types"; -import { useState, useEffect } from "react"; -import CloudinaryUpload from "../pages/CloudinaryUpload"; -import { DEFAULT_PROFILE_PICTURE } from "../constants/defaults"; - -/** - * ⚠️ Legacy komponenta - * Ova komponenta je zamenjena novim `EditProfileForm` + `Settings` kombinacijom. - * Ostavlja se u kodbazi privremeno radi tranzicije i testiranja. - * - * @component EditProfileModal - * - * Prikazuje Bootstrap modal za izmenu korisnickih podataka: - * ime, biografija, status i profilna slika. - */ - - -const EditProfileModal = ({ show, handleClose, userData, updateUserData }) => { - // State za podatke forme - const [formData, setFormData] = useState({ - name: "", - bio: "", - status: "Active", - profilePicture: "", - }); - - // State za validacione greske - const [errors, setErrors] = useState({}); - - // State za pracenje snimanja podataka - const [isSaving, setIsSaving] = useState(false); - - // State za Btn hover - const [hoverMessage, setHoverMessage] = useState("Save changes"); - - // Postavljanje pocetnih vrednosti forme iz userData - useEffect(() => { - if (userData) { - setFormData({ - name: userData.name || "", - bio: userData.bio || "", - status: userData.status || "Active", - profilePicture: userData.profilePicture || DEFAULT_PROFILE_PICTURE, - }); - } - }, [userData]); - - const handleMouseEnter = () => { - if (isSaving) return; // Ako se podaci cuvaju, nista ne radi - if (isSaveDisabled()) { - setHoverMessage("No changes :)"); - } else { - setHoverMessage("Save Changes"); // Ako su podaci izmenjeni, ostaje "Save Changes" - } - }; - - const handleMouseLeave = () => { - if (isSaving) return; - setHoverMessage("Save Changes"); - }; - - // Funkcija za validaciju podataka unetih u formu - const validateForm = () => { - const newErrors = {}; - const nameRegex = /^[\p{L}' -]+$/u; // Regex pravilo: dozvoljeni karakteri (slova, razmaci, crtice, apostrofi) - const allowedStatuses = ["Active", "Inactive"]; // Niz dozvoljenih statusa - - // Validacija unosa za ime - if (!formData.name.trim()) { - newErrors.name = "Name is required."; // Greska ako je polje prazno - } else if (!nameRegex.test(formData.name)) { - newErrors.name = - "Allowed characters: letters (A-Z, a-z), spaces, hyphens (-), and apostrophes (')."; // Greska ako ime sadrzi nedozvoljene karaktere - } else if (formData.name.length > 20) { - newErrors.name = "Name cannot exceed 20 characters."; // Greska ako je ime predugacko - } - // Provera da li biografija ima manje od 200 karaktera - if (formData.bio.length > 200) { - newErrors.bio = "Bio must be 200 characters or less."; - } - // Validacija unosa za status - if (!allowedStatuses.includes(formData.status)) { - newErrors.status = "Invalid status"; - } - // Postavljanje gresaka i vracanje rezultata validacije - setErrors(newErrors); - - return Object.keys(newErrors).length === 0; - }; - - // Funkcija za cuvanje podataka - const handleSave = async () => { - setIsSaving(true); - - if (validateForm()) { - const updatedData = {}; - // Proveravamo i pripremamo podatke za azuriranje - if (formData.name !== userData.name) updatedData.name = formData.name; - if (formData.bio !== userData.bio) updatedData.bio = formData.bio; - if (formData.status !== userData.status) - updatedData.status = formData.status; - if (formData.profilePicture !== userData.profilePicture) { - updatedData.profilePicture = formData.profilePicture; - } - - try { - // Referenca na dokument korisnika u Firestore - const docRef = doc(db, "users", userData.id); - // Azuriranje podataka u Firestore - await updateDoc(docRef, updatedData); - console.log("Data updated successfully:", updatedData); - updateUserData(updatedData); // Azuriramo lokalne podatke - handleClose(); // Zatvaranje modala nakon uspesnog cuvanja - } catch (error) { - console.error("Error updating document:", error); - } finally { - setIsSaving(false); - } - } else { - setIsSaving(false); - } - }; - const isSaveDisabled = () => { - if (isSaving) return true; // Ako se trenutno cuva, onemoguci dugme - return ( - formData.name === userData.name && - formData.bio === userData.bio && - formData.status === userData.status && - formData.profilePicture === userData.profilePicture - ); // Onemoguci ako podaci nisu promenjeni - }; - - const handleUploadComplete = (uploadedUrl) => { - setFormData((prev) => ({ ...prev, profilePicture: uploadedUrl })); - }; - - return ( -
-
-
-
-
Edit Profile
{/* Naslov modala */} - -
-
- {/* Forma za unos podataka */} -
- {/* Prikaz profilne slike na modalu */} -
- - Profile - -
- {/* Polje za ime */} -
- - { - const value = e.target.value; // Trenutna vrednost uneta u polje - const capitalizedName = - value.charAt(0).toUpperCase() + value.slice(1); // Pretvaramo prvo slovo u veliko, ostatak ostaje nepromenjen - setFormData({ ...formData, name: capitalizedName }); // Azuriramo stanje forme sa novom vrednoscu - }} - /> - {errors.name &&

{errors.name}

}{" "} - {/* Prikaz greske za ime */} -
- - {/* Polje za biografiju */} -
- - - {/* Sekcija za dinamicki brojac preostalih karaktera */} -
- {200 - formData.bio.length} characters left -
- {errors.bio &&

{errors.bio}

}{" "} - {/* Prikaz greske za biografiju */} -
- - {/* Polje za status */} -
- - - {errors.status && ( -

{errors.status}

// Prikaz greske za status - )} -
-
-
-
- {/* Dugme za zatvaranje modala */} - - {/* Dugme za cuvanje promena */} -
- -
-
-
-
-
- ); -}; - -EditProfileModal.propTypes = { - show: PropTypes.bool.isRequired, - handleClose: PropTypes.func.isRequired, - userData: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string, - bio: PropTypes.string, - status: PropTypes.string, - profilePicture: PropTypes.string, - }), - updateUserData: PropTypes.func.isRequired, -}; - -export default EditProfileModal; diff --git a/src/___legacy___/PostReactions.legacy.jsx b/src/___legacy___/PostReactions.legacy.jsx deleted file mode 100644 index 893cb54..0000000 --- a/src/___legacy___/PostReactions.legacy.jsx +++ /dev/null @@ -1,210 +0,0 @@ -import Spinner from "./Spinner"; -import { useState, useEffect } from "react"; -import PropTypes from "prop-types"; -import { FaRegLightbulb, FaFire, FaBolt } from "react-icons/fa"; -import { - collection, - query, - where, - getDocs, - onSnapshot, - setDoc, - deleteDoc, - doc, -} from "firebase/firestore"; -import { db, auth } from "../firebase"; - -/** - * ⚠️ Legacy komponenta - * Ova komponenta je zamenjena novom ReactionSummary komponentom. - * Ostavljena je ovde za referencu i ne koristi se vise u aplikaciji. - */ - -/** - * Komponenta za prikaz i upravljanje reakcijama na post. - * - * - Prikazuje sve dostupne reakcije uz broj glasova - * - Dozvoljava korisniku da klikne ili ukloni svoju reakciju - * - Real-time azuriranje putem Firestore onSnapshot - * - Ako je `locked`, onemogucava sve interakcije - * - * @component - * @param {string} postId - ID posta za koji se prikazuju reakcije - * @param {boolean} [locked=false] - Da li je post zakljucan (onemogucava klik) - */ - -// Komponenta koja upravlja reakcijama na postove -const PostReactions = ({ postId, locked }) => { - /** - * State za pracenje korisnickih reakcija (da li je korisnik kliknuo na neku reakciju). - * Popunjava se nakon sto ucitamo podatke iz Firestore-a putem `onSnapshot()`. - */ - - const [userReactions, setUserReactions] = useState({ - idea: false, - hot: false, - powerup: false, - }); - - /** - * State za brojanje reakcija po tipu. - * Pocetno stanje je 0 za svaku reakciju, ali se azurira iz Firestore-a. - */ - const [reactionCounts, setReactionCounts] = useState({ - idea: 0, - hot: 0, - powerup: 0, - }); - - /** - * Mapa koja povezuje naziv reakcije sa odgovarajucom ikonicom. - * Ovo omogucava dinamicko prikazivanje odgovarajuce ikonice u UI-u. - */ - const reactionComponents = { - idea: FaRegLightbulb, - hot: FaFire, - powerup: FaBolt, - }; - // State za pracnje ucitavanja - const [isLoading, setIsLoading] = useState(true); - - /** - * `useEffect` slusa promene u Firestore-u i azurira UI u realnom vremenu. - * Kada se komponenta mount-uje ili `postId` promeni, preuzimamo reakcije iz Firestore-a. - * Koristimo `onSnapshot()` da slusamo promene u bazi (real-time update). - */ - useEffect(() => { - if (!postId) return; // Ako postId ne postoji, ne radimo nista. - - // Kreiramo upit za sve reakcije koje pripadaju ovom postId - const q = query(collection(db, "reactions"), where("postId", "==", postId)); - - // Pretplacujemo se na real-time azuriranja - const unsubscribe = onSnapshot(q, (snapshot) => { - // Resetujemo brojace i korisnicke reakcije pre nego sto ih azuriramo - const newCounts = { - idea: 0, - hot: 0, - powerup: 0, - }; - const newUserReactions = { - idea: false, - hot: false, - powerup: false, - }; - // Prolazimo kroz sve dokumente u snapshot-u i racunamo reakcije - snapshot.forEach((doc) => { - const data = doc.data(); - const rType = data.reactionType; - - // Ako postoji validna reakcija, povecavamo njen brojac - if (newCounts[rType] !== undefined) { - newCounts[rType]++; - } - - // Ako je reakciju dodao trenutno prijavljeni korisnik, oznacavamo je - if (data.userId === auth.currentUser?.uid) { - newUserReactions[rType] = true; - } - }); - - // Azuriramo state sa najnovijim podacima iz Firestore-a - setReactionCounts(newCounts); - setUserReactions(newUserReactions); - setIsLoading(false); // Podaci su stigli, prekidamo loading - }); - - // Cleanup funkcija – prekidamo pretplatu kada se komponenta unmount-uje ili `postId` promeni - return () => unsubscribe(); - }, [postId]); - - /** - * Funkcija koja se poziva kada korisnik klikne na reakciju. - * Ako je reakcija vec dodata, brisemo je iz Firestore-a. - * Ako reakcija ne postoji, dodajemo novi dokument u Firestore. - */ - - const handleReactionClick = async (event, reactionType) => { - event.stopPropagation(); // Sprecava prebacivanje na stranicu posta pri kliku - - if (!auth.currentUser) return; // Ako korisnik nije prijavljen, ne dozvoljavamo reakciju - const userId = auth.currentUser.uid; - - if (locked) return; // Ako je post zaklucan ne izvrsavaj reakciju - - try { - // Proveravamo da li korisnik vec ima ovu reakciju - const q = query( - collection(db, "reactions"), - where("postId", "==", postId), - where("userId", "==", userId), - where("reactionType", "==", reactionType) - ); - const querySnapshot = await getDocs(q); - - if (!querySnapshot.empty) { - // Ako reakcija vec postoji, brisemo je - const docId = querySnapshot.docs[0].id; - await deleteDoc(doc(db, "reactions", docId)); - } else { - // Ako reakcija ne postoji, dodajemo novi dokument - const newDocRef = doc(collection(db, "reactions")); - await setDoc(newDocRef, { - postId: postId, - userId: userId, - reactionType: reactionType, - createdAt: new Date(), - }); - } - // `onSnapshot()` ce automatski azurirati state, pa ne moramo rucno menjati `useState`. - } catch (error) { - console.error("Greska pri azuriranju reakcije:", error); - } - }; - - return ( -
-
- {Object.entries(reactionCounts).map(([reactionType, count]) => { - const IconComponent = reactionComponents[reactionType]; - const isActive = userReactions[reactionType]; - - return ( - - ); - })} -
-
- ); -}; - -PostReactions.propTypes = { - postId: PropTypes.string.isRequired, - locked: PropTypes.bool, -}; - -export default PostReactions; diff --git a/src/___legacy___/statsService.legacy.js b/src/___legacy___/statsService.legacy.js deleted file mode 100644 index fdf7162..0000000 --- a/src/___legacy___/statsService.legacy.js +++ /dev/null @@ -1,57 +0,0 @@ -import dayjs from "dayjs"; -import { - doc, - getDoc, - setDoc, - updateDoc, - serverTimestamp, - increment, -} from "firebase/firestore"; - -import { db } from "../firebase"; - - -/** - * Azurira statistiku korisnika prilikom kreiranja novog posta. - * - * - Ako dokument u `userStats/{userId}` vec postoji: - * → Inkrementira ukupan broj postova i broj postova za tekuci mesec. - * - Ako dokument ne postoji: - * → Kreira novi dokument sa pocetnim vrednostima. - * - * @async - * @function updateUserStats - * @param {string} userId - ID korisnika koji kreira post - * @param {Timestamp} createdAt - Datum i vreme kada je post kreiran (Firestore Timestamp) - * - * @returns {Promise} - */ - - -export const updateUserStats = async (userId, createdAt) => { - - const month = dayjs(createdAt.toDate()).format("YYYY-MM"); - - const statsRef = doc(db, "userStats", userId); - const statsSnap = await getDoc(statsRef); - - if (statsSnap.exists()) { - await updateDoc(statsRef, { - [`postsPerMonth.${month}`]: increment(1), - totalPosts: increment(1), - updatedAt: serverTimestamp(), - }); - } else { - await setDoc(statsRef, { - totalPosts: 1, - postsPerMonth: { - [month]: 1 - }, - restoredPosts: 0, - permanentlyDeletedPosts: 0, - createdAt: serverTimestamp(), - updatedAt: serverTimestamp(), - }) - console.log("User stats updated for:", month); - } -}; \ No newline at end of file diff --git a/src/components/AvatarDropdown.jsx b/src/components/AvatarDropdown.jsx index ee773f8..f1ae159 100644 --- a/src/components/AvatarDropdown.jsx +++ b/src/components/AvatarDropdown.jsx @@ -90,12 +90,17 @@ const AvatarDropdown = ({ user, logout, isLoggingOut }) => { }; }, [showMenu]); + const linkBase = + "block w-full px-4 py-2 text-sm text-zinc-200 hover:bg-zinc-900/50 hover:text-zinc-100 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-400 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 rounded-lg"; + + const linkActive = "bg-zinc-900/60 font-medium text-zinc-100"; + return (
@@ -124,61 +129,60 @@ const AvatarDropdown = ({ user, logout, isLoggingOut }) => { animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -5 }} transition={{ duration: 0.2, ease: "easeInOut" }} - className="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-50" + className="absolute right-0 mt-2 w-52 z-50" role="menu" > -
- -
    -
  • - - Dashboard - -
  • - -
  • - - Profile Info - -
  • - -
  • - - Settings - -
  • - -
  • - -
  • -
+
+ {/* Arrow */} +
+ +
    +
  • + + Dashboard + +
  • + +
  • + + Profile Info + +
  • + +
  • + + Settings + +
  • + +
  • + +
  • +
+
)} diff --git a/src/components/CloudinaryPreview.jsx b/src/components/CloudinaryPreview.jsx index 6b78d49..9d4ed69 100644 --- a/src/components/CloudinaryPreview.jsx +++ b/src/components/CloudinaryPreview.jsx @@ -18,7 +18,7 @@ const CloudinaryPreview = () => { .image("cld-sample-4") // Slika iz Media Library .resize(fill().width(300).height(300)); // Transformacija slike return ( -
+

Test Cloudinary Image

{/* Prikaz slike koriscenjem AdvancedImage komponente */} diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 78efda5..8536ac7 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,17 +1,23 @@ const Footer = () => { return ( -