* 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
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Emoji reactions on photos (#839).
|
||||
*
|
||||
* - event_feedback_settings.allow_reactions: per-event toggle next to
|
||||
* allow_likes / allow_ratings / allow_comments. Defaults TRUE for parity
|
||||
* with the sibling toggles — the master feedback_enabled gate (default
|
||||
* false, opt-in per event) still decides whether any feedback UI shows.
|
||||
* - photo_feedback.reaction: the emoji value for feedback_type='reaction'
|
||||
* rows (validated against the fixed set in constants/reactions.js).
|
||||
* - photos.reaction_count: denormalized total, maintained by
|
||||
* updatePhotoFeedbackStats alongside like_count / favorite_count.
|
||||
*/
|
||||
|
||||
exports.up = async function (knex) {
|
||||
const hasAllowReactions = await knex.schema.hasColumn('event_feedback_settings', 'allow_reactions');
|
||||
if (!hasAllowReactions) {
|
||||
await knex.schema.alterTable('event_feedback_settings', (table) => {
|
||||
table.boolean('allow_reactions').defaultTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
const hasReaction = await knex.schema.hasColumn('photo_feedback', 'reaction');
|
||||
if (!hasReaction) {
|
||||
await knex.schema.alterTable('photo_feedback', (table) => {
|
||||
// 16 chars: emoji are multi-byte/multi-codepoint (variation selectors),
|
||||
// but well under 16 characters each.
|
||||
table.string('reaction', 16);
|
||||
});
|
||||
}
|
||||
|
||||
const hasReactionCount = await knex.schema.hasColumn('photos', 'reaction_count');
|
||||
if (!hasReactionCount) {
|
||||
await knex.schema.alterTable('photos', (table) => {
|
||||
table.integer('reaction_count').defaultTo(0);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
if (await knex.schema.hasColumn('photos', 'reaction_count')) {
|
||||
await knex.schema.alterTable('photos', (table) => {
|
||||
table.dropColumn('reaction_count');
|
||||
});
|
||||
}
|
||||
if (await knex.schema.hasColumn('photo_feedback', 'reaction')) {
|
||||
await knex.schema.alterTable('photo_feedback', (table) => {
|
||||
table.dropColumn('reaction');
|
||||
});
|
||||
}
|
||||
if (await knex.schema.hasColumn('event_feedback_settings', 'allow_reactions')) {
|
||||
await knex.schema.alterTable('event_feedback_settings', (table) => {
|
||||
table.dropColumn('allow_reactions');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user