You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

317 lines
11 KiB

<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center relative">
Here's one:
<div
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div>
</h1>
<span class="mt-2 flex justify-between">
<span
v-if="currentCategory === CATEGORY_IDEAS"
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="prevIdea()"
>
<font-awesome icon="chevron-left" class="m-auto" />
</span>
<div class="m-2">
<span v-if="currentCategory === CATEGORY_IDEAS">
<p class="text-center text-lg">
{{ IDEAS[currentIdeaIndex] }}
</p>
</span>
<div v-if="currentCategory === CATEGORY_CONTACTS">
<p class="text-center">
<span
v-if="currentContact == null"
class="text-orange-500 text-lg"
>
That's all your contacts.
</span>
<span v-else>
<span class="text-lg">
Did {{ displayContactName }}
<br />
or someone near them do anything &ndash; maybe a while ago?
</span>
<span class="flex justify-between">
<span />
<button
:class="buttonClasses"
@click="nextIdeaPastContacts()"
>
Skip Contacts <font-awesome icon="forward" />
</button>
</span>
</span>
</p>
</div>
</div>
<span
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="nextIdea()"
>
<font-awesome icon="chevron-right" class="m-auto" />
</span>
</span>
<button :class="proceedButtonClasses" @click="proceed">That's it!</button>
</div>
</div>
</template>
<script lang="ts">
/**
* GiftedPrompts.vue
*
* A dialog component that displays gift prompts and contact suggestions to help users
* record gifts. The component cycles through predefined gift ideas and then through
* the user's contacts to provide inspiration for gift recording.
*
* Features:
* - Displays a carousel of gift prompt ideas
* - Cycles through user contacts for gift suggestions
* - Provides navigation between ideas and contacts
* - Handles callback for gift recording
* - Template streamlined with extracted CSS classes and computed properties
*
* @author Matthew Raymer
* @since 2024-12-19
*/
import { Vue, Component } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { GiverReceiverInputInfo } from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
mixins: [PlatformServiceMixin],
})
export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
CATEGORY_CONTACTS = 1;
CATEGORY_IDEAS = 0;
IDEAS = [
"What food did someone make? (How did it free up your time for something? Was something doable because it eased your stress?)",
"What did a family member do? (How did you take better action because it made you feel loved?)",
"What compliment did someone give you? (What task could you tackle because it boosted your confidence?)",
"Who is someone you can always rely on, and how did they demonstrate that? (What project tasks were enabled because you could depend on them?)",
"What did you see someone give to someone else? (What is the effect of the positivity you gained from seeing that?)",
"What is a way that someone helped you even though you have never met? (What different action did you take due to that newfound perspective or inspiration?)",
"How did a musician or author or artist inspire you? (What were you motivated to do more creatively because of that?)",
"What inspiration did you get from someone who handled tragedy well? (What could you accomplish with better grace or resilience after seeing their example?)",
"What is something worth respect that an organization gave you? (How did their contribution improve the situation or enable new activities?)",
"Who last gave you a good laugh? (What kind of bond or revitalization did that bring to a situation?)",
"What do you recall someone giving you while you were young? (How did it bring excitement or teach a skill or ignite a passion that resulted in improvements in your life?)",
"Who forgave you or overlooked a mistake? (How did that free you or build trust that enabled better relationships?)",
"What is a way an ancestor contributed to your life? (What in your life is now possible because of their efforts? What challenges are you undertaking knowing of their lives?)",
"What kind of help did someone at work give you? (How did that help with team progress? How did that lift your professional growth?)",
"How did a teacher or mentor or great example help you? (How did their guidance enhance your attitude or actions?)",
"What is a surprise gift you received? (What extra possibilities did it give you?)",
];
callbackOnFullGiftInfo?: (
contactInfo?: GiverReceiverInputInfo,
description?: string,
) => void;
currentCategory = this.CATEGORY_IDEAS; // 0 = IDEAS, 1 = CONTACTS
currentContact: Contact | undefined = undefined;
currentIdeaIndex = 0;
numContacts = 0;
shownContactDbIndices: Array<boolean> = [];
visible = false;
AppString = AppString;
// =================================================
// COMPUTED PROPERTIES - Template Streamlining
// =================================================
/**
* Consistent button styling classes used throughout the component
* Extracts repeated Tailwind CSS classes to single source of truth
*/
get buttonClasses(): string {
return "text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4";
}
/**
* Display name for current contact with fallback
* Centralizes contact name display logic and fallback handling
*/
get displayContactName(): string {
return this.currentContact?.name || AppString.NO_CONTACT_NAME;
}
/**
* Router configuration for navigating to contact-gift with current prompt
* Extracts router push configuration to computed property
*/
get routerConfig(): { name: string; query: { prompt: string } } {
return {
name: "contact-gift",
query: {
prompt: this.IDEAS[this.currentIdeaIndex],
},
};
}
/**
* Styling classes for the main proceed button
* Extracts the full button styling including block and full width
*/
get proceedButtonClasses(): string {
return `block w-full ${this.buttonClasses}`;
}
// =================================================
// LIFECYCLE & EVENT METHODS
// =================================================
async open(
callbackOnFullGiftInfo?: (
contactInfo?: GiverReceiverInputInfo,
description?: string,
) => void,
) {
this.visible = true;
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
const contacts = await this.$contacts();
this.numContacts = contacts.length;
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
}
cancel() {
this.currentCategory = this.CATEGORY_IDEAS;
this.currentContact = undefined;
this.currentIdeaIndex = 0;
this.numContacts = 0;
this.shownContactDbIndices = [];
this.visible = false;
}
proceed() {
// proceed with logic but don't change values (just in case some actions are added later)
this.visible = false;
if (this.currentCategory === this.CATEGORY_IDEAS) {
this.$router.push(this.routerConfig);
} else {
// must be this.CATEGORY_CONTACTS
this.callbackOnFullGiftInfo?.(
this.currentContact as GiverReceiverInputInfo,
);
}
}
/**
* Get the next idea.
* If it is a contact prompt, loop through.
*/
async nextIdea() {
// check if the next one is an idea or a contact
if (this.currentCategory === this.CATEGORY_IDEAS) {
this.currentIdeaIndex++;
if (this.currentIdeaIndex === this.IDEAS.length) {
// must have just finished ideas so move to contacts
this.findNextUnshownContact();
}
} else {
// must be this.CATEGORY_CONTACTS
this.findNextUnshownContact();
// when that's finished, it'll reset to ideas
}
}
/**
* Get the previous idea.
* If it is a contact prompt, loop through.
*/
async prevIdea() {
// check if the next one is an idea or a contact
if (this.currentCategory === this.CATEGORY_IDEAS) {
this.currentIdeaIndex--;
if (this.currentIdeaIndex < 0) {
// must have just finished ideas so move to contacts
this.findNextUnshownContact();
}
} else {
// must be this.CATEGORY_CONTACTS
this.findNextUnshownContact();
// when that's finished, it'll reset to ideas
}
}
nextIdeaPastContacts() {
this.currentContact = undefined;
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
this.currentCategory = this.CATEGORY_IDEAS;
// look at the previous idea and switch to the other side of the list
this.currentIdeaIndex =
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1;
}
async findNextUnshownContact() {
if (this.currentCategory === this.CATEGORY_IDEAS) {
// we're not in the contact prompts, so reset index array
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
}
this.currentCategory = this.CATEGORY_CONTACTS;
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
let count = 0;
// as long as the index has an entry, loop
while (
this.shownContactDbIndices[someContactDbIndex] != null &&
count++ < this.numContacts
) {
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
}
if (count >= this.numContacts) {
// all contacts have been shown
this.nextIdeaPastContacts();
} else {
// get the contact at that offset using the contacts array
const contacts = await this.$contacts();
this.currentContact = contacts[someContactDbIndex];
this.shownContactDbIndices[someContactDbIndex] = true;
}
}
}
</script>
<style>
.dialog-overlay {
z-index: 50;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>