* feat(feedback): emoji reactions on photos (#839) Per-photo emoji reactions from a fixed curated set (❤️ 😂 😍 👏 🎉), one reaction per guest per photo — same emoji toggles off, another switches in place. Stored as feedback_type='reaction' rows with per-guest scoping identical to likes (guest_id when present, device hash otherwise). - migration 164: allow_reactions toggle (default on, still gated by the opt-in feedback_enabled master switch), photo_feedback.reaction value column, denormalized photos.reaction_count - emoji whitelist enforced in the route validator AND the service (shared constants/reactions.js, mirrored in the frontend) - per-emoji tallies + my_feedback.reaction in the photo feedback endpoint; hidden-by-moderator reactions leave all counts - reactions ride the existing rate limiting (like-tier), guest identity modes, and moderation actions; long + pivot exports carry the emoji - gallery: reaction bar in the photo feedback panel (grid lightbox); admin: allow_reactions toggle next to likes, analytics tile, create/duplicate event paths - i18n for all 8 locales; 9 service-level tests * fix(feedback): reach reactions without comments; numeric analytics totals (#839) - the lightbox feedback-panel toggle was gated on allow_comments only — with comments off the new reaction bar was unreachable; the gate now opens for comments OR reactions - the analytics summary now coerces Postgres string counts to numbers: total_feedback concatenated instead of adding ("00006") * fix(feedback): harden reactions from review round 1 (#839) - per-emoji tallies are gated on show_feedback_to_guests — with sharing off a guest sees only their own selection, no aggregate counts - reaction toggle/switch operate on the guest-scoped row SET, so rows duplicated by the (like-parity) check-then-insert race collapse on the next interaction instead of counting twice - rate-limit defaults merge UNDER the persisted settings object — stored rows predating the reaction key otherwise dropped it to the generic 100/h fallback - optimistic revert uses the pre-mutation value via mutation context; the onError closure sees the post-optimistic render, so the old revert froze the wrong state on failed toggles * fix(feedback): review round 2 — hide reaction_count with sharing off, admin list shows emoji (#839) - summary.reaction_count is gated on show_feedback_to_guests like the per-emoji map, keeping the "no aggregates while sharing is off" promise consistent - the admin feedback list renders the reaction emoji on reaction rows and the type filter gains a Reactions option (7 locales; es has no types block and falls back to EN defaults) * fix(feedback): register reaction activity types with translated labels (#839) photo_reaction / guest_feedback_reaction are logged by the submission paths but were absent from the frontend activity-type union and the admin.activities label maps — the recent-activity feed would have shown the raw identifiers. All 8 locales. * feat(feedback): reactions in guest CRM and the premium gallery layout (#839) - guest CRM: per-guest reaction counts in the list aggregation and a Reacted tab (photo grid with emoji badges) + stats card in the guest detail modal; picks/aggregate/exports stay selection-only by design - premium layout: its own yet-another-react-lightbox now gets a fixed reaction-bar overlay (per-photo fetch, optimistic switch) — reactions were otherwise unreachable in this layout since it bypasses the shared PhotoLightbox - allowReactions threaded through the layout feedbackOptions; guest i18n keys for the 7 locales that carry the guests block * fix(feedback): portal the premium reaction bar to document.body (#839) Inside the layout tree an ancestor stacking context (framer-motion transforms) painted the bar under yarl's body-level portal — visible but unclickable, every tap landed on the slide image. As a direct body child the z-index 10000 genuinely wins over yarl's 9999. Verified by clicking through in the running app. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
f8a95d29d2
commit
3d6c9848dc
@@ -1,6 +1,7 @@
|
||||
const { body, param, validationResult } = require('express-validator');
|
||||
const validator = require('validator');
|
||||
const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('./emailNormalization');
|
||||
const { REACTION_EMOJIS } = require('../constants/reactions');
|
||||
|
||||
/**
|
||||
* Validation rules for feedback submission
|
||||
@@ -133,14 +134,20 @@ function getValidationRules(feedbackType) {
|
||||
*/
|
||||
const validateFeedbackSubmission = [
|
||||
body('feedback_type')
|
||||
.isIn(['rating', 'like', 'comment', 'favorite'])
|
||||
.isIn(['rating', 'like', 'comment', 'favorite', 'reaction'])
|
||||
.withMessage('Invalid feedback type'),
|
||||
|
||||
|
||||
// Conditional validation based on feedback type
|
||||
body('rating')
|
||||
.if(body('feedback_type').equals('rating'))
|
||||
.isInt({ min: 1, max: 5 })
|
||||
.withMessage('Rating must be between 1 and 5'),
|
||||
|
||||
// Reactions (#839): fixed curated set only — no free-form emoji.
|
||||
body('reaction')
|
||||
.if(body('feedback_type').equals('reaction'))
|
||||
.custom((value) => REACTION_EMOJIS.includes(value))
|
||||
.withMessage('Invalid reaction'),
|
||||
|
||||
body('comment_text')
|
||||
.if(body('feedback_type').equals('comment'))
|
||||
@@ -183,6 +190,7 @@ const validateFeedbackSettings = [
|
||||
body('allow_likes').optional().isBoolean(),
|
||||
body('allow_comments').optional().isBoolean(),
|
||||
body('allow_favorites').optional().isBoolean(),
|
||||
body('allow_reactions').optional().isBoolean(),
|
||||
body('require_name_email').optional().isBoolean(),
|
||||
body('moderate_comments').optional().isBoolean(),
|
||||
body('show_feedback_to_guests').optional().isBoolean(),
|
||||
|
||||
Reference in New Issue
Block a user