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..bd72ae9 100644
--- a/functions/index.js
+++ b/functions/index.js
@@ -1,5 +1,22 @@
/* eslint-disable */
+/**
+ * Firebase Cloud Functions (v2) entrypoint.
+ *
+ * Goals:
+ * - Keep all HTTP/callable endpoints, Firestore triggers and schedulers in one place.
+ * - Prefer correctness under retries over minimal writes (idempotency + ledger).
+ *
+ * Patterns used:
+ * - `processedEvents` docs act as idempotency markers (TTL ~30d) to avoid double-apply.
+ * - `reactionLedger` docs act as per-reaction source-of-truth ("counted" vs "not counted").
+ * - "stale_create/stale_delete" guards skip outdated events when users toggle quickly.
+ * - External cleanup (Cloudinary) is best-effort and must never crash the runtime.
+ *
+ * Conventions:
+ * - Comments/JSDoc are English-only and use plain latin (no diacritics).
+ */
+
// -------------------- IMPORTS --------------------
const admin = require("firebase-admin");
console.log("[CF] index loaded");
@@ -18,11 +35,12 @@ const {
} = require("firebase-functions/v2/firestore");
const { onSchedule } = require("firebase-functions/v2/scheduler");
-// Cloudinary helper je opcion (best-effort). Ako fali config ili modul — ne ruši ceo container.
+// Cloudinary helper is optional (best-effort cleanup).
+// If config/env/package is missing, keep the runtime alive and skip image deletes.
let cloudinary = { uploader: { destroy: async () => {} } };
try {
- // samo ako postoji i moze da se ucita
- // (ako baca zbog env var ili paketa, ostaje stub iznad)
+ // Load only if available.
+ // If it throws due to env vars or missing package, keep the stub above.
cloudinary = require("./cloudinary");
console.log("[CF] cloudinary helper loaded");
} catch (e) {
@@ -32,7 +50,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;
@@ -44,7 +62,12 @@ exports.ping = onRequest({ invoker: "public" }, (req, res) => {
// -------------------- HELPERS --------------------
-// Azuriranje permDelete statistike
+/**
+ * @helper bumpPermanentDeletesForUser
+ * Tracks "hard delete" stats for the post author.
+ * - Creates the stats doc if missing (safe defaults).
+ * - Uses atomic `increment` for retry safety.
+ */
async function bumpPermanentDeletesForUser(userId) {
try {
const ref = admin.firestore().collection("userStats").doc(userId);
@@ -65,7 +88,7 @@ async function bumpPermanentDeletesForUser(userId) {
permanentlyDeletedPosts: FieldValue.increment(1),
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
}
@@ -80,11 +103,16 @@ async function bumpPermanentDeletesForUser(userId) {
}
}
-// BFS brisanje cele grane komentara
+/**
+ * @helper deleteCommentBatch
+ * Deletes a full comment subtree (BFS) by following `parentID`.
+ * - Collects all descendants first, then deletes in batches (Firestore limit: 500 ops/batch).
+ * - Assumes `parentID` forms a tree (no cycles); BFS prevents missing children.
+ */
async function deleteCommentBatch(rootId) {
const toDelete = [rootId];
- // prikupi sve potomke
+ // Collect descendants first so we can delete the whole subtree deterministically.
for (let i = 0; i < toDelete.length; i++) {
const parentId = toDelete[i];
const snap = await db
@@ -94,7 +122,7 @@ async function deleteCommentBatch(rootId) {
snap.docs.forEach((doc) => toDelete.push(doc.id));
}
- // batched delete (max 500)
+ // Batch delete to stay under Firestore 500 writes per batch.
while (toDelete.length) {
const batch = db.batch();
const chunk = toDelete.splice(0, 500);
@@ -103,7 +131,20 @@ async function deleteCommentBatch(rootId) {
}
}
-// Kaskadno brisanje posta + veza
+/**
+ * @helper deletePostCascadeInternal
+ * Hard-deletes a post and its related data (reactions, comments, image).
+ *
+ * Why order matters:
+ * - Deletes reactions/comments before the post so onDelete triggers have a chance to read post data.
+ * - Uses cursor pagination + batched deletes to safely handle large datasets.
+ *
+ * Security:
+ * - Only the post author or an admin can execute.
+ *
+ * Best-effort:
+ * - Cloudinary delete must not block post deletion.
+ */
async function deletePostCascadeInternal({ postId, uid }) {
const postRef = db.collection("posts").doc(postId);
const postSnap = await postRef.get();
@@ -118,11 +159,11 @@ async function deletePostCascadeInternal({ postId, uid }) {
if (!authorId) {
throw new HttpsError(
"failed-precondition",
- "Post is missing author userId."
+ "Post is missing author userId.",
);
}
- // 1) Ucitamo user dokument da proverimo rolu
+ // Role gate: author or admin only.
const userRef = db.collection("users").doc(uid);
const userSnap = await userRef.get();
@@ -130,20 +171,19 @@ async function deletePostCascadeInternal({ postId, uid }) {
const isAuthor = authorId === uid;
const isAdmin = role === "admin";
- // 2) Author ili admin mogu da prodju
if (!isAuthor && !isAdmin) {
throw new HttpsError(
"permission-denied",
- "You are not allowed to delete this post."
+ "You are not allowed to delete this post.",
);
}
- // Fallback: FieldPath.documentId() moze biti undefined u nekim setup-ima
+ // Fallback: `FieldPath.documentId()` may be undefined in some setups.
const docIdField = admin.firestore.FieldPath?.documentId
? admin.firestore.FieldPath.documentId()
: "__name__";
- // 3) Reakcije PRVO (da onDelete CF ima vece sanse da ucita post dok jos postoji)
+ // Delete reactions first to reduce chance of post-missing in reaction triggers.
let lastReaction = null;
do {
let q = db
@@ -164,7 +204,7 @@ async function deletePostCascadeInternal({ postId, uid }) {
lastReaction = snap.docs[snap.docs.length - 1];
} while (true);
- // 4) Komentari
+ // Delete comments in chunks (cursor pagination + batch limit).
let lastComment = null;
do {
let q = db
@@ -185,7 +225,7 @@ async function deletePostCascadeInternal({ postId, uid }) {
lastComment = snap.docs[snap.docs.length - 1];
} while (true);
- // 5) Cloudinary slika (best-effort)
+ // Best-effort external cleanup; do not fail the hard-delete because of Cloudinary.
if (postData.imagePublicId) {
try {
await cloudinary.uploader.destroy(postData.imagePublicId);
@@ -194,10 +234,10 @@ async function deletePostCascadeInternal({ postId, uid }) {
}
}
- // 6) Na kraju obrisi sam post
+ // Delete the post last.
await postRef.delete();
- // 7) Stat bump treba da ide AUTORU posta (ne adminu koji je kliknuo delete)
+ // Stat bump must be attributed to the author (not an admin requestor).
try {
await bumpPermanentDeletesForUser(authorId);
} catch (err) {
@@ -211,20 +251,28 @@ async function deletePostCascadeInternal({ postId, uid }) {
}
// -------------------- ReactionThresholds helper and fallback --------------------
+
+// Reaction thresholds are configurable in Firestore.
+// Fallback prevents broken config from producing always-true badges or badge spam.
const REACTION_THRESHOLDS_FALLBACK = Object.freeze({
idea: 5,
hot: 10,
powerup: 30,
});
+/**
+ * @helper getReactionThresholds
+ * Loads badge thresholds from `appSettings/reactionThresholds`.
+ * - Normalizes invalid values back to safe fallbacks.
+ * - Keeps triggers resilient if the settings doc is missing or misconfigured.
+ */
async function getReactionThresholds() {
const ref = db.collection("appSettings").doc("reactionThresholds");
- // Validates one threshold value and falls back if invalid
+ // Guardrails: keep thresholds positive integers to avoid always-true badge logic.
const normalize = (value, key) => {
const fallback = REACTION_THRESHOLDS_FALLBACK[key];
- // Must be a finite number
if (!Number.isFinite(value)) {
console.warn(
"[warn] reactionThreshold invalid (not finite), using fallback",
@@ -232,12 +280,11 @@ async function getReactionThresholds() {
key,
value,
fallback,
- }
+ },
);
return fallback;
}
- // Must be > 0 to avoid always-true badge logic
if (value <= 0) {
console.warn("[warn] reactionThreshold invalid (<= 0), using fallback", {
key,
@@ -247,7 +294,6 @@ async function getReactionThresholds() {
return fallback;
}
- // Optional strictness: thresholds should be integers
if (!Number.isInteger(value)) {
console.warn(
"[warn] reactionThreshold invalid (not integer), using fallback",
@@ -255,7 +301,7 @@ async function getReactionThresholds() {
key,
value,
fallback,
- }
+ },
);
return fallback;
}
@@ -302,7 +348,7 @@ exports.deletePostCascade = onCall(
if (!postId) throw new HttpsError("invalid-argument", "Missing postId.");
await deletePostCascadeInternal({ postId, uid: req.auth.uid });
return { success: true };
- }
+ },
);
// deleteCommentAndChildren (v2)
@@ -310,7 +356,7 @@ exports.deleteCommentAndChildren = onCall(async (req) => {
if (!req.auth)
throw new HttpsError(
"unauthenticated",
- "You must be logged in to delete a comment."
+ "You must be logged in to delete a comment.",
);
const { commentId } = req.data || {};
if (!commentId)
@@ -327,7 +373,7 @@ exports.deleteCommentAndChildren = onCall(async (req) => {
console.error("Error while deleting comment and its children:", err);
throw new HttpsError(
"internal",
- "An error occurred while deleting the comment."
+ "An error occurred while deleting the comment.",
);
}
});
@@ -337,7 +383,7 @@ exports.softDeleteComment = onCall(async (req) => {
if (!req.auth)
throw new HttpsError(
"unauthenticated",
- "You must be logged in to delete a comment."
+ "You must be logged in to delete a comment.",
);
const { commentId } = req.data || {};
@@ -352,10 +398,9 @@ exports.softDeleteComment = onCall(async (req) => {
const commentData = docSnap.data();
const uid = req.auth.uid;
- // Author check
+ // Permission: author or admin can soft-delete.
const isAuthor = commentData.userID === uid;
- // Admin check
const userRef = db.collection("users").doc(uid);
const userSnap = await userRef.get();
const role = userSnap.exists ? userSnap.data().role : "user";
@@ -364,7 +409,7 @@ exports.softDeleteComment = onCall(async (req) => {
if (!isAuthor && !isAdmin) {
throw new HttpsError(
"permission-denied",
- "You are not authorized to delete this comment."
+ "You are not authorized to delete this comment.",
);
}
@@ -381,7 +426,7 @@ exports.addCommentSecure = onCall({ memory: "256MiB" }, async (req) => {
if (!req.auth)
throw new HttpsError(
"unauthenticated",
- "You must be authenticated to add a comment."
+ "You must be authenticated to add a comment.",
);
const { postId, content, parentId = null } = req.data || {};
@@ -390,18 +435,18 @@ exports.addCommentSecure = onCall({ memory: "256MiB" }, async (req) => {
if (!postId || !trimmedContent) {
throw new HttpsError(
"invalid-argument",
- "Missing postId or comment content is empty."
+ "Missing postId or comment content is empty.",
);
}
if (trimmedContent.length > 500) {
throw new HttpsError(
"invalid-argument",
- "Comment must not exceed 500 characters."
+ "Comment must not exceed 500 characters.",
);
}
- // Rate limit: max 3 komentara u 30 sekundi
+ // Rate limit: allow up to 3 comments per 30s window (4th is rejected).
const now = Timestamp.now();
const thirtySecondsAgo = Timestamp.fromMillis(now.toMillis() - 30_000);
@@ -415,7 +460,7 @@ exports.addCommentSecure = onCall({ memory: "256MiB" }, async (req) => {
if (recent.size >= 4) {
throw new HttpsError(
"resource-exhausted",
- "You are sending comments too quickly. Try again in a few seconds."
+ "You are sending comments too quickly. Try again in a few seconds.",
);
}
@@ -435,11 +480,16 @@ exports.addCommentSecure = onCall({ memory: "256MiB" }, async (req) => {
console.error("Error adding comment:", error);
throw new HttpsError(
"internal",
- "An error occurred while saving the comment."
+ "An error occurred while saving the comment.",
);
}
});
+// -------------------- FIRESTORE TRIGGERS / SCHEDULERS --------------------
+
+// NOTE: Post/userStats triggers and reaction triggers are designed for retry safety.
+// They use idempotency markers (`processedEvents`) and per-reaction ledgers (`reactionLedger`).
+
// -------------------- FIRESTORE TRIGGER: onCreate (v2) --------------------
exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
"posts/{postId}",
@@ -450,8 +500,7 @@ exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
const createdAt = data?.createdAt;
if (!userId) return;
- // monthKey = "YYYY-MM"
- // Ako nema createdAt, fallback je new Date()
+ // Derive month key ("YYYY-MM") from createdAt; fallback to current time if missing.
const d = createdAt?.toDate ? createdAt.toDate() : new Date();
const y = d.getUTCFullYear();
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
@@ -461,14 +510,14 @@ exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
const markerRef = db.collection("processedEvents").doc(event.id);
await db.runTransaction(async (tx) => {
- // Idempotency marker: ako postoji, prekidamo
+ // Idempotency marker: if already processed, do nothing.
const markerSnap = await tx.get(markerRef);
if (markerSnap.exists) return;
const statsSnap = await tx.get(statsRef);
if (!statsSnap.exists) {
- // Prvo kreiranje stats dokumenta
+ // First-time stats doc creation.
tx.set(statsRef, {
totalPosts: 1,
postsPerMonth: { [monthKey]: 1 },
@@ -478,12 +527,12 @@ exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
updatedAt: FieldValue.serverTimestamp(),
});
} else {
- // 1) Globalni inkrementi
+ // Update totals + month bucket (merge-safe map increment).
tx.update(statsRef, {
totalPosts: FieldValue.increment(1),
updatedAt: FieldValue.serverTimestamp(),
});
- // 2) Siguran inkrement u mapi preko merge objekta
+
tx.set(
statsRef,
{
@@ -491,17 +540,18 @@ exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
[monthKey]: FieldValue.increment(1),
},
},
- { merge: true }
+ { merge: true },
);
}
+ // Keep processedEvents small with TTL (cleanup handled by Firestore TTL policy).
tx.set(markerRef, {
type: "posts.onCreate",
postId: event.params.postId,
userId,
processedAt: FieldValue.serverTimestamp(),
expiresAt: Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
),
});
});
@@ -520,11 +570,10 @@ exports.updateUserStatsOnPostCreateV2 = onDocumentCreated(
});
return null;
}
- }
+ },
);
// On restore
-
exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
"posts/{postId}",
async (event) => {
@@ -533,9 +582,10 @@ exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
const after = event.data?.after?.data();
if (!before || !after) return;
+ // Only handle restore transition: deleted:true -> deleted:false
const wasDeleted = before.deleted === true;
const isDeleted = after.deleted === true;
- if (!(wasDeleted && !isDeleted)) return; // samo restore
+ if (!(wasDeleted && !isDeleted)) return;
const userId = after.userId;
if (!userId) return;
@@ -545,7 +595,7 @@ exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
await db.runTransaction(async (tx) => {
const markerSnap = await tx.get(markerRef);
- if (markerSnap.exists) return; // već obrađeno
+ if (markerSnap.exists) return;
const statsSnap = await tx.get(statsRef);
if (!statsSnap.exists) {
@@ -564,7 +614,7 @@ exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
restoredPosts: FieldValue.increment(1),
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
}
@@ -574,7 +624,7 @@ exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
userId,
processedAt: FieldValue.serverTimestamp(),
expiresAt: Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
),
});
});
@@ -592,9 +642,15 @@ exports.bumpRestoredOnPostUpdate = onDocumentUpdated(
});
return null;
}
- }
+ },
);
+// -------------------- Reaction triggers pattern --------------------
+// - Idempotency markers (`processedEvents`) prevent double-apply on retries.
+// - Ledgers (`reactionLedger`) prevent double-counting across rapid toggles.
+// - Stale guards skip outdated create/delete events when doc state has changed.
+// - TTL fields keep bookkeeping collections bounded over time.
+
// -------------------- FIRESTORE TRIGGER: reactions.idea.onCreate (v2) --------------------
exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
"reactions/{reactionId}",
@@ -607,6 +663,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
const postId = data?.postId;
const reactionType = data?.reactionType;
+ // Guard: only handle idea reactions with a postId.
if (!postId || reactionType !== "idea") return null;
const eventId = event?.id;
@@ -628,18 +685,18 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
const { idea: ideaThreshold } = await getReactionThresholds();
await db.runTransaction(async (tx) => {
- // ---------------- READS (all first) ----------------
+ // Firestore tx rule: do all reads before writes for clarity/safety.
// 0) Idempotency per event
const markerSnap = await tx.get(markerRef);
@@ -664,12 +721,12 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // B) stale-create: reaction must still exist
+ // B) Stale-create: reaction must still exist at apply-time.
const liveReactionSnap = await tx.get(reactionRef);
if (!liveReactionSnap.exists) {
tx.set(
@@ -683,7 +740,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
lastReason: "stale_create",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -699,12 +756,12 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // C) post must exist
+ // C) Post must exist to update aggregates.
const postSnap = await tx.get(postRef);
if (!postSnap.exists) {
tx.set(
@@ -718,7 +775,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -734,7 +791,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -749,7 +806,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
"badges.mostInspiring": next >= ideaThreshold,
});
- // ledger becomes active (counted) -> remove TTL field
+ // Ledger becomes active (counted) -> remove TTL field.
tx.set(
ledgerRef,
{
@@ -761,7 +818,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -777,7 +834,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -796,7 +853,7 @@ exports.reactionsIdeaOnCreateV2 = onDocumentCreated(
});
throw err;
}
- }
+ },
);
// -------------------- FIRESTORE TRIGGER: reactions.idea.onDelete (v2 + ledger) --------------------
@@ -830,17 +887,17 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
const markerId = `reactions.idea.onDelete__${eventId}`;
const markerRef = db.collection("processedEvents").doc(markerId);
- // Ledger is keyed by reactionId (deterministic id) -> tracks whether it was counted
+ // Ledger is keyed by reactionId -> tracks whether it was counted.
const ledgerRef = db.collection("reactionLedger").doc(reactionId);
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
const { idea: ideaThreshold } = await getReactionThresholds();
@@ -852,7 +909,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
const markerSnap = await tx.get(markerRef);
if (markerSnap.exists) return;
- // 1) stale_delete: if reaction doc exists again (user re-toggled ON), skip this delete
+ // 1) Stale-delete: if reaction exists again, skip decrement.
const liveReactionSnap = await tx.get(reactionRef);
if (liveReactionSnap.exists) {
tx.set(
@@ -868,12 +925,12 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // 2) Ledger gate: only decrement if it was previously counted
+ // 2) Ledger gate: only decrement if it was previously counted.
const ledgerSnap = await tx.get(ledgerRef);
const ledgerData = ledgerSnap.exists ? ledgerSnap.data() || {} : {};
const wasCounted = ledgerData.active === true;
@@ -892,12 +949,12 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // 3) Post must exist; if not, just flip ledger off and stop (with TTL)
+ // 3) Post must exist; if not, flip ledger off and stop (with TTL).
const postSnap = await tx.get(postRef);
if (!postSnap.exists) {
tx.set(
@@ -909,7 +966,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -925,7 +982,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -940,7 +997,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
"badges.mostInspiring": next >= ideaThreshold,
});
- // ledger becomes inactive -> set TTL
+ // Ledger becomes inactive -> set TTL.
tx.set(
ledgerRef,
{
@@ -950,7 +1007,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -966,7 +1023,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -985,7 +1042,7 @@ exports.reactionsIdeaOnDeleteV2 = onDocumentDeleted(
});
throw err;
}
- }
+ },
);
// -------------------- FIRESTORE TRIGGER: reactions.hot.onCreate (v2) --------------------
@@ -1000,6 +1057,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
const postId = data?.postId;
const reactionType = data?.reactionType;
+ // Guard: only handle hot reactions with a postId.
if (!postId || reactionType !== "hot") return null;
const eventId = event?.id;
@@ -1021,12 +1079,12 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
const { hot: hotThreshold } = await getReactionThresholds();
@@ -1055,11 +1113,12 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
+ // Stale-create: reaction must still exist at apply-time.
const liveReactionSnap = await tx.get(reactionRef);
if (!liveReactionSnap.exists) {
tx.set(
@@ -1073,7 +1132,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
lastReason: "stale_create",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1089,7 +1148,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1107,7 +1166,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1123,7 +1182,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1139,7 +1198,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
"badges.trending": next >= hotThreshold,
});
- // ledger becomes active -> remove TTL field
+ // Ledger becomes active -> remove TTL field.
tx.set(
ledgerRef,
{
@@ -1151,7 +1210,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1167,7 +1226,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -1186,7 +1245,7 @@ exports.reactionsHotOnCreateV2 = onDocumentCreated(
});
throw err;
}
- }
+ },
);
// -------------------- FIRESTORE TRIGGER: reactions.hot.onDelete (v2) --------------------
@@ -1222,12 +1281,12 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
const { hot: hotThreshold } = await getReactionThresholds();
@@ -1237,7 +1296,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
const markerSnap = await tx.get(markerRef);
if (markerSnap.exists) return;
- // stale-delete: if reaction exists again -> skip decrement
+ // Stale-delete: if reaction exists again, skip decrement.
const liveReactionSnap = await tx.get(reactionRef);
if (liveReactionSnap.exists) {
tx.set(
@@ -1253,18 +1312,18 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // ledger: only decrement if counted (and matches this post/type)
+ // Ledger gate: only decrement if it was counted (and matches this post/type).
const ledgerSnap = await tx.get(ledgerRef);
const ledgerData = ledgerSnap.exists ? ledgerSnap.data() || {} : {};
const wasCounted = ledgerData?.active === true;
- // optional sanity: avoid decrement if ledger belongs to other post/type
+ // Optional sanity: avoid decrement if ledger belongs to other post/type.
const ledgerPostId = ledgerData?.postId ?? null;
const ledgerType = ledgerData?.reactionType ?? null;
const ledgerMismatch =
@@ -1287,7 +1346,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1303,7 +1362,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1319,7 +1378,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1332,6 +1391,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
"reactionCounts.hot": next,
};
+ // Keep badge consistent with threshold (latch off when under).
if (next < hotThreshold) {
update["badges.trending"] = false;
}
@@ -1339,7 +1399,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
// ---------------- WRITES ----------------
tx.update(postRef, update);
- // ledger becomes inactive -> set TTL
+ // Ledger becomes inactive -> set TTL.
tx.set(
ledgerRef,
{
@@ -1349,7 +1409,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1365,7 +1425,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -1384,7 +1444,7 @@ exports.reactionsHotOnDeleteV2 = onDocumentDeleted(
});
throw err;
}
- }
+ },
);
// -------------------- SCHEDULER cleanupExpiredPosts (v2) --------------------
@@ -1398,7 +1458,7 @@ exports.cleanupExpiredPostsV2 = onSchedule(
async () => {
const now = Timestamp.now();
const cutoff = Timestamp.fromMillis(
- now.toMillis() - 30 * 24 * 60 * 60 * 1000
+ now.toMillis() - 30 * 24 * 60 * 60 * 1000,
);
const snap = await db
@@ -1418,6 +1478,7 @@ exports.cleanupExpiredPostsV2 = onSchedule(
const postId = doc.id;
const authorId = doc.get("userId");
try {
+ // Scheduler has no auth context; run as author so permission checks pass.
await deletePostCascadeInternal({ postId, uid: authorId });
console.log(`Deleted post ${postId}`);
} catch (err) {
@@ -1426,7 +1487,7 @@ exports.cleanupExpiredPostsV2 = onSchedule(
}
return null;
- }
+ },
);
// -------------------- SCHEDULER expireTrendingPosts (v2) --------------------
@@ -1448,6 +1509,7 @@ exports.expireTrendingPostsV2 = onSchedule(
async function processQuery(baseQuery, reason) {
let query = baseQuery;
+ // Cursor pagination + batch updates to avoid timeouts on large datasets.
while (true) {
const snap = await query.get();
if (snap.empty) break;
@@ -1498,7 +1560,7 @@ exports.expireTrendingPostsV2 = onSchedule(
});
return null;
- }
+ },
);
// -------------------- FIRESTORE TRIGGER: reactions.powerup.onCreate (v2) --------------------
@@ -1515,6 +1577,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
const reactorId = data?.userId;
const reactionType = data?.reactionType;
+ // Guard: only handle powerup reactions.
if (reactionType !== "powerup") return null;
const eventId = event?.id;
@@ -1531,12 +1594,12 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
if (!postId || !reactorId) {
@@ -1554,7 +1617,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return null;
}
@@ -1562,7 +1625,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
const postRef = db.collection("posts").doc(postId);
const reactionRef = db.collection("reactions").doc(reactionId);
- // Ledger: source-of-truth da li je reaction vec uracunat
+ // Ledger is the source-of-truth for whether this reaction was counted.
const ledgerRef = db.collection("reactionLedger").doc(reactionId);
const { powerup: powerupThreshold } = await getReactionThresholds();
@@ -1574,7 +1637,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
const markerSnap = await tx.get(markerRef);
if (markerSnap.exists) return;
- // A) Ledger state (da li je vec uracunat)
+ // A) Ledger state: already counted?
const ledgerSnap = await tx.get(ledgerRef);
const ledgerData = ledgerSnap.exists ? ledgerSnap.data() || {} : {};
const alreadyCounted = ledgerData?.active === true;
@@ -1595,15 +1658,14 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // B) Stale-create guard: reaction doc mora jos da postoji
+ // B) Stale-create guard: reaction doc must still exist.
const liveReactionSnap = await tx.get(reactionRef);
if (!liveReactionSnap.exists) {
- // Nista nije uracunato, ledger ostaje inactive + TTL
tx.set(
ledgerRef,
{
@@ -1616,7 +1678,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
lastReason: "stale_create",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1634,12 +1696,12 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // C) Post mora da postoji
+ // C) Post must exist
const postSnap = await tx.get(postRef);
if (!postSnap.exists) {
tx.set(
@@ -1654,7 +1716,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1672,7 +1734,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1693,7 +1755,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
lastReason: "author_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1711,14 +1773,14 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
const isSelf = reactorId === authorId;
- // userStats se cita samo ako nije self
+ // userStats is read only if not self (self-powerups are rejected).
const userStatsRef = db.collection("userStats").doc(authorId);
const userPublicRef = db.collection("users").doc(authorId);
@@ -1732,10 +1794,10 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
// ---------------- WRITES ----------------
- // Stamp authorId (safe update, ne moze resurrect)
+ // Stamp authorId for safer onDelete resolution (best-effort; reaction already exists).
tx.update(reactionRef, { authorId });
- // Self-powerup: ne uracunava se + ledger inactive + TTL
+ // Self-powerup: do not count; keep ledger inactive with TTL for cleanup.
if (isSelf) {
tx.set(
ledgerRef,
@@ -1750,7 +1812,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
lastReason: "self_powerup_rejected",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -1768,7 +1830,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1778,7 +1840,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
"reactionCounts.powerup": FieldValue.increment(1),
});
- // 2) Author aggregate (+1) + badge threshold
+ // 2) Author aggregate (+1) + badge threshold (latch on)
const currentTotal = userStatsData?.powerupsTotal ?? 0;
const nextTotal = currentTotal + 1;
@@ -1800,15 +1862,14 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
tx.set(userStatsRef, patch, { merge: true });
if (shouldSetTop) {
- // NESTED (avoid literal key)
tx.set(
userPublicRef,
{ badges: { topContributor: true } },
- { merge: true }
+ { merge: true },
);
}
- // 3) Ledger: sad JE uracunato -> remove TTL field
+ // 3) Ledger becomes active (counted) -> remove TTL field.
tx.set(
ledgerRef,
{
@@ -1822,7 +1883,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
// Marker applied
@@ -1841,7 +1902,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -1860,7 +1921,7 @@ exports.reactionsPowerupOnCreateV2 = onDocumentCreated(
});
throw err;
}
- }
+ },
);
// -------------------- FIRESTORE TRIGGER: reactions.powerup.onDelete (v2) --------------------
@@ -1879,6 +1940,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
const stampedAuthorId = data?.authorId ?? null;
+ // Guard: only handle powerup reactions.
if (reactionType !== "powerup") return null;
const eventId = event?.id;
@@ -1895,12 +1957,12 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
// processedEvents TTL
const expiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
// ledger TTL (only when active:false)
const ledgerExpiresAt = Timestamp.fromDate(
- new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
);
if (!postId || !reactorId) {
@@ -1918,7 +1980,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return null;
}
@@ -1934,7 +1996,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
const markerSnap = await tx.get(markerRef);
if (markerSnap.exists) return;
- // 1) Stale-delete guard: if reaction exists again, skip decrement
+ // 1) Stale-delete guard: if reaction exists again, skip decrement.
const liveReactionSnap = await tx.get(reactionRef);
if (liveReactionSnap.exists) {
tx.set(
@@ -1952,12 +2014,12 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // 2) Ledger: only decrement if it was counted
+ // 2) Ledger gate: only decrement if it was counted.
const ledgerSnap = await tx.get(ledgerRef);
const ledgerData = ledgerSnap.exists ? ledgerSnap.data() || {} : {};
const wasCounted = ledgerData?.active === true;
@@ -1978,7 +2040,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -1992,7 +2054,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
if (!authorId && postExists) authorId = postData?.userId ?? null;
- // If we cannot resolve authorId, we still MUST turn ledger off (with TTL)
+ // If authorId cannot be resolved, we still must turn ledger off (with TTL).
if (!authorId) {
tx.set(
ledgerRef,
@@ -2003,7 +2065,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
lastReason: postExists ? "author_missing" : "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -2021,12 +2083,12 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // Self guard (safety) -> ledger off with TTL
+ // Safety: self-powerups must never affect aggregates.
if (reactorId === authorId) {
tx.set(
ledgerRef,
@@ -2037,7 +2099,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
lastReason: "self_powerup_rejected",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -2055,7 +2117,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
@@ -2068,14 +2130,14 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
// ---------------- WRITES ----------------
- // A) Decrement post aggregate only if post exists
+ // A) Decrement post aggregate only if post exists.
if (postExists) {
const currentPostPowerups = postData?.reactionCounts?.powerup ?? 0;
const nextPostPowerups = Math.max(currentPostPowerups - 1, 0);
tx.update(postRef, { "reactionCounts.powerup": nextPostPowerups });
} else {
- // post missing, but reaction was counted -> ledger must go off (with TTL)
+ // Post missing, but reaction was counted -> ledger must go off (with TTL).
tx.set(
ledgerRef,
{
@@ -2085,7 +2147,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
lastReason: "post_missing",
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
tx.set(
@@ -2103,17 +2165,17 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
return;
}
- // B) Decrement author aggregate (clamp 0) - latch badges
+ // B) Decrement author aggregate (clamp to 0) - badges are latched on.
const currentTotal = userStatsData?.powerupsTotal ?? 0;
const nextTotal = Math.max(currentTotal - 1, 0);
tx.set(userStatsRef, { powerupsTotal: nextTotal }, { merge: true });
- // C) Ledger: no longer counted -> set TTL
+ // C) Ledger becomes inactive -> set TTL.
tx.set(
ledgerRef,
{
@@ -2123,7 +2185,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
lastReason: null,
updatedAt: FieldValue.serverTimestamp(),
},
- { merge: true }
+ { merge: true },
);
// Marker applied
@@ -2142,7 +2204,7 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
processedAt: FieldValue.serverTimestamp(),
expiresAt,
},
- { merge: true }
+ { merge: true },
);
});
@@ -2161,5 +2223,5 @@ exports.reactionsPowerupOnDeleteV2 = onDocumentDeleted(
});
throw err;
}
- }
+ },
);
diff --git a/index.html b/index.html
index 7819b15..2bfa35f 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,19 @@
-
+
+
+
+
- Vite + React
+
+
+
+
+
+
+
+ LifeRecompiled
diff --git a/package-lock.json b/package-lock.json
index 9583930..4b344d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,23 +11,23 @@
"@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",
"react-icons": "^5.5.0",
"react-is": "^19.2.3",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.2.0",
"react-tag-input": "^6.10.3",
"react-toastify": "^10.0.6",
"react-tooltip": "^5.29.1",
"recharts": "^3.0.0",
+ "remark-gfm": "^4.0.1",
"tailwind-scrollbar-hide": "^2.0.0"
},
"devDependencies": {
@@ -3380,16 +3380,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 +3444,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 +3462,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 +3813,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",
@@ -4149,6 +4061,15 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -4160,9 +4081,26 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4203,6 +4141,21 @@
"@types/lodash": "*"
}
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
@@ -4238,15 +4191,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",
@@ -4254,17 +4198,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"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/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
@@ -4944,6 +4894,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -5144,25 +5104,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",
@@ -5535,6 +5476,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -5572,6 +5523,46 @@
"node": ">=10"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chardet": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
@@ -5996,6 +5987,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
@@ -6611,7 +6612,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -6638,6 +6638,19 @@
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-equal-in-any-order": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.1.0.tgz",
@@ -6777,6 +6790,19 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -6838,16 +6864,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",
@@ -7556,6 +7572,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -7868,7 +7894,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
@@ -9294,6 +9319,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/heap-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz",
@@ -9373,6 +9438,16 @@
"node": ">=18"
}
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -9567,6 +9642,12 @@
"node": ">=10"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
"node_modules/install-artifact-from-github": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.4.0.tgz",
@@ -9603,15 +9684,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",
@@ -9642,6 +9714,30 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -9808,6 +9904,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -9874,6 +9980,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-installed-globally": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@@ -9990,6 +10106,18 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -10902,6 +11030,16 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11025,6 +11163,16 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/marked": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz",
@@ -11083,45 +11231,890 @@
"node": ">= 0.4"
}
},
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "dev": true,
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
"license": "MIT",
- "engines": {
- "node": ">= 0.6"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "dev": true,
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/merge2": {
- "version": "1.4.1",
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"license": "MIT",
- "engines": {
- "node": ">= 8"
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "dev": true,
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 0.6"
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -11465,7 +12458,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mute-stream": {
@@ -12323,6 +13315,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -12961,31 +13978,22 @@
"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",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -13263,37 +14271,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,11 +14338,32 @@
"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-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
},
"node_modules/react-refresh": {
"version": "0.17.0",
@@ -13482,22 +14480,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",
@@ -13791,6 +14773,72 @@
"node": ">=8"
}
},
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -14498,6 +15546,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -14819,6 +15877,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
@@ -14892,6 +15964,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -15643,6 +16733,16 @@
"node": ">=18"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
@@ -15653,6 +16753,16 @@
"node": ">= 14.0.0"
}
},
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -15886,21 +16996,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",
@@ -15917,6 +17012,25 @@
"node": ">=4"
}
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/unique-filename": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz",
@@ -15958,6 +17072,74 @@
"node": ">=8"
}
},
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/universal-analytics": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz",
@@ -16156,6 +17338,34 @@
"node": ">= 0.8"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
@@ -16388,15 +17598,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",
@@ -17049,6 +18250,16 @@
"peerDependencies": {
"zod": "^3.25 || ^4"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/package.json b/package.json
index bf7f3c5..98a45e4 100644
--- a/package.json
+++ b/package.json
@@ -16,23 +16,23 @@
"@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",
"react-icons": "^5.5.0",
"react-is": "^19.2.3",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.2.0",
"react-tag-input": "^6.10.3",
"react-toastify": "^10.0.6",
"react-tooltip": "^5.29.1",
"recharts": "^3.0.0",
+ "remark-gfm": "^4.0.1",
"tailwind-scrollbar-hide": "^2.0.0"
},
"devDependencies": {
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..09b261e
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,18 @@
+
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..62dfc58 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,37 +1,65 @@
-// Paketi
import { Routes, Route, Navigate } from "react-router-dom";
import { ToastContainer } from "react-toastify";
-// Komponente
+
import ProtectedRoute from "./components/ProtectedRoute";
import Layout from "./components/Layout";
-// Stranice
+import OrientationGuard from "./components/common/OrientationGuard";
+
import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register";
+import ForgotPassword from "./pages/ForgotPassword";
+import ReportIssue from "./pages/ReportIssue";
import MyPosts from "./pages/MyPosts";
import CreatePost from "./pages/CreatePost";
import EditPost from "./pages/EditPost";
import PostDetails from "./pages/PostDetails";
import Profile from "./pages/Profile";
-// Dashboard stranice
+import About from "./pages/About";
+
import DashboardLayout from "./pages/dashboard/components/DashboardLayout";
import SavedPosts from "./pages/dashboard/components/saved/SavedPosts";
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";
+/**
+ * @component App
+ *
+ * Top-level router + global providers mounted once for the whole app.
+ *
+ * Why:
+ * - Keeps shared shell (`Layout`) and global guards (`OrientationGuard`) consistent across routes.
+ * - Centralizes route access control via `ProtectedRoute` (dashboard + user-only pages).
+ * - Mounts a single `ToastContainer` to avoid duplicate containers and inconsistent toast behavior.
+ *
+ * Routing notes:
+ * - Public routes: home, auth pages, post details, about.
+ * - Protected routes: dashboard subtree + profile/report pages that require auth.
+ * - Fallback route redirects unknown paths to `/login` to keep the entry flow predictable.
+ *
+ * Toast notes:
+ * - `limit={2}` + `newestOnTop` reduces toast spam during rapid actions.
+ * - Higher `zIndex` ensures toasts stay above modals/sheets.
+ *
+ * @returns {JSX.Element}
+ */
function App() {
return (
+
+
+ {/* Public routes */}
} />
} />
} />
+ } />
} />
- {/* Zasticene rute */}
+ } />
+
+ {/* Protected routes */}
}>
}>
} />
@@ -42,15 +70,29 @@ function App() {
} />
} />
+
} />
} />
+ } />
+
+ {/* Public profile route (view other users) */}
} />
- {/* Default preusmeravanje ako ruta ne postoji */}
+
+ {/* Unknown routes -> auth entry */}
} />
- {/* ToastContainer: Komponenta za prikaz globalnih toast poruka*/}
-
+
+
);
}
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/AuthorLink.jsx b/src/components/AuthorLink.jsx
index 419f3f2..4460fd2 100644
--- a/src/components/AuthorLink.jsx
+++ b/src/components/AuthorLink.jsx
@@ -1,21 +1,39 @@
import PropTypes from "prop-types";
-
import { Link } from "react-router-dom";
/**
- * Uviverzalni link ka autoru
+ * @component AuthorLink
+ *
+ * Reusable navigation link to an author's profile page.
*
- * @param {{ author: { id: string, name: string }, children?: React.ReactNode }} props
+ * - Renders a to `/profile/:id`
+ * - Stops event propagation to prevent parent card click triggers
+ * - Supports optional label override via `children`
+ * - Returns null if author id is missing (defensive guard)
+ *
+ * @param {{ id: string, name: string }} author - Author identity object
+ * @param {React.ReactNode} [children] - Optional custom link label
+ * @param {string} [className] - Additional Tailwind/custom classes
+ * @returns {JSX.Element|null}
*/
-const AuthorLink = ({ author, children }) => {
+const AuthorLink = ({ author, children, className = "" }) => {
+ // Defensive guard: do not render invalid profile links
if (!author?.id) return null;
+ const base =
+ "font-semibold text-zinc-100 hover:text-zinc-100 " +
+ "hover:underline underline-offset-4 decoration-zinc-500/70 " +
+ "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-md";
+
return (
- // Link ka profilu autora
e.stopPropagation()}
+ aria-label={`Open profile: ${author?.name ?? "author"}`}
>
{children ?? author.name}
@@ -28,6 +46,7 @@ AuthorLink.propTypes = {
name: PropTypes.string.isRequired,
}).isRequired,
children: PropTypes.node,
+ className: PropTypes.string,
};
export default AuthorLink;
diff --git a/src/components/AvatarDropdown.jsx b/src/components/AvatarDropdown.jsx
index ee773f8..6a62744 100644
--- a/src/components/AvatarDropdown.jsx
+++ b/src/components/AvatarDropdown.jsx
@@ -7,32 +7,48 @@ import { doc, onSnapshot } from "firebase/firestore";
import { db } from "../firebase";
import { DEFAULT_PROFILE_PICTURE } from "../constants/defaults";
import ShieldIcon from "./ui/ShieldIcon";
+import Avatar from "./common/Avatar";
+
+import {
+ cx,
+ FOCUS_RING,
+ SURFACE_PANEL,
+ SURFACE_PANEL_INNER,
+ SURFACE_PANEL_ARROW,
+} from "../constants/uiClasses";
/**
* @component AvatarDropdown
*
- * Dropdown menu ispod avatara:
- * - klik na avatar otvara/zatvara meni
- * - ESC i klik van menija zatvaraju meni
- * - Top Contributor status se cita sa servera: users/{uid}.badges.topContributor
+ * Header user avatar dropdown menu with navigation links and logout action.
+ *
+ * - Closes automatically on route change (prevents stale open menus)
+ * - Subscribes to the current user's Firestore doc for live badge/avatar updates
+ * - Closes on outside click and `Escape` for predictable UX and accessibility
+ * - Uses shared UI tokens (`SURFACE_PANEL*`, `FOCUS_RING`) for consistent styling
+ *
+ * @param {Object} props
+ * @param {Object} props.user - Auth user object (supports multiple id shapes)
+ * @param {Function} props.logout - Logout handler
+ * @param {boolean} props.isLoggingOut - Disables logout button and shows loading label
+ * @returns {JSX.Element}
*/
const AvatarDropdown = ({ user, logout, isLoggingOut }) => {
const location = useLocation();
const [showMenu, setShowMenu] = useState(false);
const dropdownRef = useRef(null);
- // Normalize user id (prilagodi ako ti je siguran samo jedan key)
+ // Normalize user id shape across auth sources (uid/id/userId)
const userId = user?.uid || user?.id || user?.userId;
const [isTopContributor, setIsTopContributor] = useState(false);
const [liveProfilePicture, setLiveProfilePicture] = useState(null);
- // Close menu on route change (kad kliknes link, da ne ostane otvoren)
+ // Auto-close menu when navigation occurs
useEffect(() => {
setShowMenu(false);
}, [location.pathname]);
- // Read Top Contributor from public user doc
useEffect(() => {
if (!userId) {
setIsTopContributor(false);
@@ -42,28 +58,24 @@ const AvatarDropdown = ({ user, logout, isLoggingOut }) => {
const userRef = doc(db, "users", userId);
+ // Live read: keep badge state and profile picture in sync without refresh
const unsubscribe = onSnapshot(
userRef,
(snap) => {
const data = snap.data();
- const flag = !!data?.badges?.topContributor;
- const pic = data?.profilePicture || null;
- setLiveProfilePicture(pic);
- setIsTopContributor(flag);
+ setIsTopContributor(!!data?.badges?.topContributor);
+ setLiveProfilePicture(data?.profilePicture || null);
},
(err) => {
- console.error(
- "AvatarDropdown: failed to read TopContributor badge",
- err
- );
+ console.error("AvatarDropdown: failed to read user doc", err);
+ // Fail closed: do not show badge if user doc is missing/denied
setIsTopContributor(false);
- }
+ },
);
return () => unsubscribe();
}, [userId]);
- // Close on outside click + ESC
useEffect(() => {
const handleClickOutside = (event) => {
if (
@@ -76,11 +88,10 @@ const AvatarDropdown = ({ user, logout, isLoggingOut }) => {
};
const handleKeyDown = (event) => {
- if (showMenu && event.key === "Escape") {
- setShowMenu(false);
- }
+ if (showMenu && event.key === "Escape") setShowMenu(false);
};
+ // Global listeners while menu is open; cleaned up on unmount/update
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
@@ -90,29 +101,48 @@ 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 " +
+ "rounded-lg " +
+ FOCUS_RING;
+
+ const linkActive = "bg-zinc-900/60 font-medium text-zinc-100";
+
+ // Prefer live Firestore profile picture when available; fall back to auth/default
+ const avatarSrc =
+ liveProfilePicture || user?.profilePicture || DEFAULT_PROFILE_PICTURE;
+
+ const dropdownSurfaceClass = cx(
+ SURFACE_PANEL,
+ SURFACE_PANEL_INNER,
+ "relative",
+ "ring-1 ring-sky-200/10",
+ "border-sky-500/15",
+ );
+
+ const dividerClass = "my-1 border-t border-zinc-800/80";
+
return (
@@ -124,61 +154,90 @@ 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 z-50 mt-3.5 w-52 sm:mt-3"
role="menu"
>
-
-
-
- -
-
- Dashboard
-
-
-
- -
-
- Profile Info
-
-
-
- -
-
- Settings
-
-
-
- -
-
-
-
+
+
+
+
+ -
+
+ Dashboard
+
+
+
+ -
+
+ Profile Info
+
+
+
+ -
+
+ Settings
+
+
+
+
+
+ -
+
+ About
+
+
+
+ -
+
+ Support & feedback
+
+
+
+ -
+
+
+
+
)}
@@ -192,7 +251,6 @@ AvatarDropdown.propTypes = {
id: PropTypes.string,
userId: PropTypes.string,
profilePicture: PropTypes.string,
- // badges: PropTypes.shape({ topContributor: PropTypes.bool }), // optional
}).isRequired,
logout: PropTypes.func.isRequired,
isLoggingOut: PropTypes.bool.isRequired,
diff --git a/src/components/CloudinaryPreview.jsx b/src/components/CloudinaryPreview.jsx
index 6b78d49..1378d6a 100644
--- a/src/components/CloudinaryPreview.jsx
+++ b/src/components/CloudinaryPreview.jsx
@@ -2,25 +2,41 @@ import { Cloudinary } from "@cloudinary/url-gen";
import { AdvancedImage } from "@cloudinary/react";
import { fill } from "@cloudinary/url-gen/actions/resize";
+/**
+ * @component CloudinaryPreview
+ *
+ * Simple Cloudinary smoke-test component used during setup/debugging.
+ *
+ * - Initializes Cloudinary client from `VITE_CLOUDINARY_CLOUD_NAME`
+ * - Renders a known sample asset with a deterministic resize transformation
+ * - Returns null if Cloudinary config is missing (fail fast in dev)
+ *
+ * Notes:
+ * - This is intended for local verification, not production UI.
+ */
const CloudinaryPreview = () => {
- // Inicijalizacija Cloudinary instance pomocu cloudName iz .env fajla
+ // Initialize Cloudinary client using env config
const cld = new Cloudinary({
cloud: { cloudName: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME },
});
- // Provera da li je cloudName definisan
+
+ // Guard: do not attempt rendering if cloudName is missing
if (!import.meta.env.VITE_CLOUDINARY_CLOUD_NAME) {
console.error("Cloudinary Cloud Name is not defined!");
return null;
}
- // Kreiranje instance slike iz Cloudinary Media Library
+ // Use a known public sample image for predictable testing
const img = cld
- .image("cld-sample-4") // Slika iz Media Library
- .resize(fill().width(300).height(300)); // Transformacija slike
+ .image("cld-sample-4")
+ // Deterministic transform: fixed square preview for visual confirmation
+ .resize(fill().width(300).height(300));
+
return (
-
+
Test Cloudinary Image
- {/* Prikaz slike koriscenjem AdvancedImage komponente */}
+
+ {/* Render via Cloudinary React SDK */}
);
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
index 78efda5..bc74e23 100644
--- a/src/components/Footer.jsx
+++ b/src/components/Footer.jsx
@@ -1,17 +1,38 @@
+import { BRAND } from "../constants/brand";
+
+/**
+ * @component Footer
+ *
+ * Application footer displayed across public and authenticated pages.
+ *
+ * - Shows dynamic current year (auto-updates without manual changes)
+ * - Uses `BRAND.name` to stay aligned with brand configuration
+ * - Includes external author link with secure target attributes
+ *
+ * Notes:
+ * - `rel="noopener noreferrer"` prevents security risks when using `target="_blank"`
+ * - Pure presentational component (no state or side effects)
+ *
+ * @returns {JSX.Element}
+ */
const Footer = () => {
return (
-