Files
Lucky_Wheel/apps/client/src/stores/spin.store.ts
T

167 lines
5.2 KiB
TypeScript

import confetti from 'canvas-confetti';
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useSoundEffects } from '../composables/useSoundEffects';
import { apiClient } from '../services/api/client';
import { useAuthStore } from './auth.store';
export interface PrizeResult {
id: string;
title: string;
segment: number;
code?: string | null;
type: string;
image?: string | null;
}
export const useSpinStore = defineStore('spin', () => {
const authStore = useAuthStore();
const { playWin, playNoPrize } = useSoundEffects();
const isRequesting = ref(false);
const isWheelSpinning = ref(false);
const targetSegmentIndex = ref<number | null>(null);
const spinResult = ref<any>(null);
const showResultModal = ref(false);
const errorMessage = ref<string | null>(null);
const pendingIdempotencyKey = ref<string | null>(null);
function generateIdempotencyKey(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
async function executeSpin(campaignId: string, segments: any[]) {
if (isRequesting.value || isWheelSpinning.value) return;
if (!authStore.isAuthenticated) {
errorMessage.value = 'لطفاً ابتدا وارد حساب کاربری خود شوید.';
return;
}
if (authStore.chances.remainingChances <= 0) {
errorMessage.value = 'شما شانس کافی برای چرخاندن گردونه ندارید!';
return;
}
isRequesting.value = true;
errorMessage.value = null;
if (!pendingIdempotencyKey.value) {
pendingIdempotencyKey.value = generateIdempotencyKey();
}
try {
const res = await apiClient.post(`/campaigns/${campaignId}/spin`, {
idempotencyKey: pendingIdempotencyKey.value,
});
const data = res.data;
spinResult.value = data;
// Update remaining chances from server
if (typeof data.remainingChances === 'number') {
authStore.setRemainingChances(data.remainingChances);
} else {
authStore.decrementChance();
}
// Find the segment index in the client segments list
let segIdx = 0;
if (data.prize) {
// Priority 1: Match strictly by prize id first
let found = segments.findIndex((s) => s.id === data.prize.id);
// Priority 2: Fallback match by exact title
if (found === -1 && data.prize.title) {
found = segments.findIndex((s) => s.title === data.prize.title);
}
// Priority 3: Fallback match by sortOrder / segment
if (found === -1 && data.prize.segment !== undefined) {
found = segments.findIndex((s) => s.sortOrder === data.prize.segment);
}
segIdx = found !== -1 ? found : 0;
}
targetSegmentIndex.value = segIdx;
isWheelSpinning.value = true;
// Clear the idempotency key once successfully acknowledged by server
pendingIdempotencyKey.value = null;
} catch (err: any) {
let msg =
err.response?.data?.message ||
(Array.isArray(err.response?.data?.message)
? err.response?.data?.message.join(' - ')
: 'خطا در ارتباط با سرور گردونه شانس');
if (typeof msg === 'string') {
if (msg === 'CAMPAIGN_DRAFT') {
msg = 'این گردونه شانس در حال آماده‌سازی است و هنوز فعال نشده است.';
} else if (msg === 'CAMPAIGN_PAUSED') {
msg = 'این گردونه شانس موقتاً متوقف شده است.';
} else if (msg === 'CAMPAIGN_NOT_STARTED') {
msg = 'زمان شروع این گردونه شانس هنوز فرا نرسیده است.';
} else if (msg === 'CAMPAIGN_FINISHED') {
msg = 'مهلت شرکت در این گردونه شانس به پایان رسیده است.';
}
}
errorMessage.value = msg;
} finally {
isRequesting.value = false;
}
}
function onWheelAnimationFinished() {
isWheelSpinning.value = false;
targetSegmentIndex.value = null;
const prize = spinResult.value?.prize;
const isNoPrize = !prize || prize.type === 'NO_PRIZE' || prize.title?.trim() === 'پوچ' || prize.title?.includes('پوچ');
if (!isNoPrize) {
playWin();
// Confetti burst
confetti({
particleCount: 80,
spread: 70,
origin: { y: 0.6 },
});
} else {
// Play famous iconic Sad Trombone losing sound
playNoPrize();
}
showResultModal.value = true;
// Trigger SMS notification now that the wheel stopped and the modal is visible
if (spinResult.value?.spinId) {
apiClient.post(`/campaigns/spins/${spinResult.value.spinId}/notify`).catch(() => {});
}
}
function closeResultModal() {
showResultModal.value = false;
spinResult.value = null;
}
return {
isRequesting,
isWheelSpinning,
targetSegmentIndex,
spinResult,
showResultModal,
errorMessage,
executeSpin,
onWheelAnimationFinished,
closeResultModal,
};
});