[READ-ONLY] Mirror of https://github.com/danielroe/dwaring87-webdev-wedding. Marissa & David's Wedding Website!
0

Configure Feed

Select the types of activity you want to include in your feed.

RSVP: Refactor Edit / Update logic

+73 -63
+23 -17
components/Form/Check.vue
··· 1 - <script setup> 2 - import RiCheck from '~icons/ri/checkbox-circle-line'; 3 - import RiCheckBlank from '~icons/ri/checkbox-blank-circle-line'; 4 - 5 - defineProps({ 6 - code: String, 7 - label: String, 8 - enabled: Boolean 9 - }); 10 - </script> 11 - 12 - <template> 13 - <button :data-code="code" :data-label="label" :data-enabled="enabled" @click="enabled = !enabled" :class="{'border-cyan-700': enabled, 'border-gray-100': !enabled}" class="flex flex-row items-center gap-2 p-4 rounded-md shadow-md bg-white cursor-pointer border-2 w-full sm:w-fit hover:bg-gray-50"> 14 - <RiCheck class="text-cyan-800" v-if="enabled" /> 15 - <RiCheckBlank class="text-cyan-800" v-else /> 16 - <p :class="{'text-cyan-800 font-bold': enabled}">{{ label }}</p> 17 - </button> 1 + <script setup> 2 + import RiCheck from '~icons/ri/checkbox-circle-line'; 3 + import RiCheckBlank from '~icons/ri/checkbox-blank-circle-line'; 4 + 5 + const props = defineProps({ 6 + code: String, 7 + label: String, 8 + enabled: Boolean 9 + }); 10 + const emit = defineEmits(['check']); 11 + 12 + const state = ref(props.enabled); 13 + watch(state, (newState) => { 14 + emit('check', newState); 15 + }); 16 + </script> 17 + 18 + <template> 19 + <button :data-code="code" :data-label="label" :data-enabled="state" @click="state = !state" :class="{'border-cyan-700': state, 'border-gray-100': !state}" class="flex flex-row items-center gap-2 p-4 rounded-md shadow-md bg-white cursor-pointer border-2 w-full sm:w-fit hover:bg-gray-50"> 20 + <RiCheck class="text-cyan-800" v-if="state" /> 21 + <RiCheckBlank class="text-cyan-800" v-else /> 22 + <p :class="{'text-cyan-800 font-bold': state}">{{ label }}</p> 23 + </button> 18 24 </template>
+13 -7
components/Form/Toggle.vue
··· 13 13 default: "YES" 14 14 } 15 15 }); 16 + const emit = defineEmits(['toggle']); 17 + 18 + const state = ref(props.enabled); 19 + watch(state, (newState) => { 20 + emit('toggle', newState); 21 + }); 16 22 </script> 17 23 18 24 <template> 19 - <div class="flex gap-2" :data-enabled="enabled"> 20 - <p :class="{'text-black font-bold': !enabled, 'text-gray-400 italic': enabled}" class="cursor-pointer" @click="enabled = false">{{ label_no }}</p> 21 - <button type="button" @click="enabled = !enabled" :class="{'bg-gradient-to-r from-cyan-700 to-cyan-800': enabled, 'bg-gray-200': !enabled}" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cyan-700 focus:ring-offset-2" role="switch" aria-checked="false"> 22 - <span :class="{'translate-x-5': enabled, 'translate-x-0': !enabled}" class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"> 23 - <span :class="{'opacity-0 ease-out duration-100': enabled, 'opacity-100 ease-in duration-200': !enabled}" class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" aria-hidden="true"> 25 + <div class="flex gap-2" :data-enabled="state"> 26 + <p :class="{'text-black font-bold': !state, 'text-gray-400 italic': state}" class="cursor-pointer" @click="state = false">{{ label_no }}</p> 27 + <button type="button" @click="state = !state" :class="{'bg-gradient-to-r from-cyan-700 to-cyan-800': state, 'bg-gray-200': !state}" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cyan-700 focus:ring-offset-2" role="switch" aria-checked="false"> 28 + <span :class="{'translate-x-5': state, 'translate-x-0': !state}" class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"> 29 + <span :class="{'opacity-0 ease-out duration-100': state, 'opacity-100 ease-in duration-200': !state}" class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" aria-hidden="true"> 24 30 <svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12"> 25 31 <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> 26 32 </svg> 27 33 </span> 28 - <span :class="{'opacity-100 ease-in duration-200': enabled, 'opacity-0 ease-out duration-100': !enabled}" class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" aria-hidden="true"> 34 + <span :class="{'opacity-100 ease-in duration-200': state, 'opacity-0 ease-out duration-100': !state}" class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" aria-hidden="true"> 29 35 <svg class="h-3 w-3 text-cyan-800" fill="currentColor" viewBox="0 0 12 12"> 30 36 <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" /> 31 37 </svg> 32 38 </span> 33 39 </span> 34 40 </button> 35 - <p :class="{'text-cyan-800 font-bold': enabled, 'text-gray-400 italic': !enabled}" class="cursor-pointer" @click="enabled = true">{{ label_yes }}</p> 41 + <p :class="{'text-cyan-800 font-bold': state, 'text-gray-400 italic': !state}" class="cursor-pointer" @click="state = true">{{ label_yes }}</p> 36 42 </div> 37 43 </template>
+36 -38
components/RSVP/Edit.vue
··· 6 6 invitation: Object 7 7 }); 8 8 const emit = defineEmits(['cancel']); 9 + const guests = ref(JSON.parse(JSON.stringify(props.invitation.guests))); 9 10 10 11 const email = await getDetails("contact_email"); 11 12 const DIET = { ··· 17 18 dairy: "Dairy Allergy", 18 19 other: "Other" 19 20 } 21 + const updateDietaryRestriction = (guest, key, enabled) => { 22 + if ( enabled ) { 23 + if ( guest.dietary_restrictions ) { 24 + guest.dietary_restrictions.push(key); 25 + } 26 + else { 27 + guest.dietary_restrictions = [key]; 28 + } 29 + } 30 + else { 31 + guest.dietary_restrictions = guest.dietary_restrictions.filter((item) => item !== key); 32 + } 33 + } 20 34 21 35 const cancel = () => { 22 36 emit('cancel'); ··· 39 53 40 54 window.scrollTo(0, 0); 41 55 42 - for ( const guest of props.invitation.guests ) { 43 - let id = guest.id; 44 - let container = document.getElementById(`guest-container-${id}`); 45 - 46 - let name = container.getElementsByClassName('guest-name')[0].value; 47 - let email = container.getElementsByClassName('guest-email')[0].value; 48 - let rsvp_welcome = container.getElementsByClassName('guest-rsvp-welcome')[0].dataset.enabled === 'true'; 49 - let rsvp = container.getElementsByClassName('guest-rsvp')[0].dataset.enabled === 'true'; 50 - let dietary_restrictions = []; 51 - let diet_els = container.getElementsByClassName('guest-diet'); 52 - for ( let i = 0; i < diet_els.length; i++ ) { 53 - if ( diet_els[i].dataset.enabled && diet_els[i].dataset.enabled === 'true' ) dietary_restrictions.push(diet_els[i].dataset.code); 56 + guests.value.forEach(async (guest, index) => { 57 + const original = props.invitation.guests[index]; 58 + const updated_guest_props = { 59 + name: guest.name !== original.name ? guest.name : undefined, 60 + email: guest.email !== original.email ? guest.email : undefined, 61 + rsvp_welcome: guest.rsvp_welcome !== original.rsvp_welcome ? guest.rsvp_welcome : undefined, 62 + rsvp: guest.rsvp !== original.rsvp ? guest.rsvp : undefined, 63 + dietary_restrictions: JSON.stringify(guest.dietary_restrictions) !== JSON.stringify(original.dietary_restrictions) ? guest.dietary_restrictions : undefined, 64 + notes: guest.notes !== original.notes ? guest.notes : undefined 54 65 } 55 - let notes = container.getElementsByClassName('guest-notes')[0].value; 56 - 57 - if ( rsvp || rsvp_welcome ) { 66 + if ( guest.rsvp || guest.rsvp_welcome ) { 58 67 attending.value = true; 59 68 } 60 69 61 - let success = await updateGuest(id, { 62 - name: name !== guest.name ? name : undefined, 63 - email: email !== guest.email ? email : undefined, 64 - rsvp_welcome: rsvp_welcome !== guest.rsvp_welcome ? rsvp_welcome : undefined, 65 - rsvp: rsvp !== guest.rsvp ? rsvp : undefined, 66 - dietary_restrictions: 67 - !guest.dietary_restrictions || 68 - dietary_restrictions.length !== guest.dietary_restrictions.length || 69 - !dietary_restrictions.every((v, i) => v === guest.dietary_restrictions[i]) 70 - ? dietary_restrictions 71 - : undefined, 72 - notes: notes !== guest.notes ? notes : undefined 73 - }); 70 + let success = await updateGuest(guest.id, updated_guest_props); 74 71 await sleep(500); 75 - 76 72 if ( !success ) { 77 73 errors.push(`Could not update Guest <strong><em>${name}</em></strong>.`); 78 - useTrackEvent('RSVP Error', { props: { code: props.invitation.invite_code, guest: name } }); 74 + useTrackEvent('RSVP Error', { props: { code: props.invitation.invite_code, guest: guest.name } }); 79 75 } 80 - } 76 + }); 81 77 82 78 if ( errors.length > 0 ) { 83 79 error.value = errors.join(`<br />`); ··· 104 100 /> 105 101 106 102 <div v-show="!saving"> 107 - <div class="mx-1 sm:mx-2 md:mx-4 mt-4 mb-8 px-4 bg-gray-100 border border-gray-400 rounded-md shadow" :id="`guest-container-${guest.id}`" v-for="(guest) in invitation.guests" :key="guest.id"> 103 + <div class="mx-1 sm:mx-2 md:mx-4 mt-4 mb-8 px-4 bg-gray-100 border border-gray-400 rounded-md shadow" :id="`guest-container-${guest.id}`" v-for="(guest) in guests" :key="guest.id"> 108 104 <div class="group"> 109 105 <p>Name:</p> 110 - <input class="guest-name" :value="guest.name" /> 106 + <input class="guest-name" v-model="guest.name" @input="onInput" /> 111 107 </div> 112 108 <div class="group"> 113 109 <p>Email:</p> 114 - <input class="guest-email" :value="guest.email" /> 110 + <input class="guest-email" v-model="guest.email" /> 115 111 <p class="info">Enter your email to receive updates from us about the wedding</p> 116 112 </div> 117 113 <div class="group"> 118 114 <p>RSVP (Friday Welcome Dinner):</p> 119 - <FormToggle class="guest-rsvp-welcome" :enabled="guest.rsvp_welcome" /> 115 + <FormToggle class="guest-rsvp-welcome" :enabled="guest.rsvp_welcome" @toggle="(s) => guest.rsvp_welcome = s" /> 120 116 <p class="info">Will you be attending the welcome dinner (potluck) the Friday evening before the wedding?</p> 121 117 </div> 122 118 <div class="group"> 123 119 <p>RSVP (Saturday Ceremony &amp; Reception):</p> 124 - <FormToggle class="guest-rsvp" :enabled="guest.rsvp" /> 120 + <FormToggle class="guest-rsvp" :enabled="guest.rsvp" @toggle="(s) => guest.rsvp = s" /> 125 121 <p class="info">Will you be attending the wedding ceremony and reception on Saturday?</p> 126 122 </div> 127 123 <div class="group"> 128 124 <p>Dietary Restrictions:</p> 129 125 <div class="flex flex-wrap gap-2"> 130 - <FormCheck class="guest-diet" v-for="(value, key) in DIET" :code="key" :label="value" :enabled="guest.dietary_restrictions && guest.dietary_restrictions.includes(key)" /> 126 + <FormCheck class="guest-diet" v-for="(value, key) in DIET" :code="key" :label="value" 127 + :enabled="guest.dietary_restrictions && guest.dietary_restrictions.includes(key)" 128 + @check="(s) => updateDietaryRestriction(guest, key, s)" /> 131 129 </div> 132 130 </div> 133 131 <div class="group"> 134 132 <p>Comments:</p> 135 - <textarea class="guest-notes" rows="5">{{ guest.notes }}</textarea> 133 + <textarea class="guest-notes" v-model="guest.notes" rows="5"></textarea> 136 134 <p class="info">Any other information we should know?</p> 137 135 </div> 138 136 </div>
+1 -1
components/RSVP/Loading.vue
··· 70 70 </ul> 71 71 </div> 72 72 </div> 73 - <Image class="mx-auto mt-8 max-w-sm max-h-sm rounded-md shadow-md" d-key="rsvp-success" /> 73 + <Image d-key="rsvp-success" class="mx-auto mt-8 max-w-sm max-h-sm rounded-md shadow-md" /> 74 74 <br /> 75 75 <button class="btn-dark mx-auto" @click="emit('continue')" v-html="continueLabel"></button> 76 76 </div>