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 */}
-
-
-
- {/* 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 (
-