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 @@ + + + + + + LR + 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 */} -
- {/* Prikaz profilne slike na modalu */} -
- - Profile - -
- {/* Polje za ime */} -
- - { - const value = e.target.value; // Trenutna vrednost uneta u polje - const capitalizedName = - value.charAt(0).toUpperCase() + value.slice(1); // Pretvaramo prvo slovo u veliko, ostatak ostaje nepromenjen - setFormData({ ...formData, name: capitalizedName }); // Azuriramo stanje forme sa novom vrednoscu - }} - /> - {errors.name &&

{errors.name}

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

{errors.bio}

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

{errors.status}

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