Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions src/components/Room.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
import { computed, ref, watch, onUnmounted } from 'vue';
import {
room,
setName,
Expand Down Expand Up @@ -148,15 +148,50 @@ watch(
{ immediate: true }
);

// --- 全員回答済みなら自動で公開(computedで管理) ---
const isOpen = computed(() => {
// --- 全員回答済みなら自動で公開(ちょっと遅延させて裏面を見せてからめくる) ---
const isOpen = ref(false);
const shouldBeOpen = computed(() => {
if (!room.value) return false;
const participants = room.value.participants;
// 観戦者(isAudience)を除外
const answerable = participants.filter(p => !p.isAudience);
return answerable.length > 0 && answerable.every(p => p.answer !== '');
});

let isOpenTimeout: ReturnType<typeof setTimeout> | null = null;
Copy link

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout isn't cleared on component unmount. You may want to add an onUnmounted hook to clear isOpenTimeout and prevent potential memory leaks.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. recheck it.

watch(shouldBeOpen, newValue => {
Comment thread
ef81sp marked this conversation as resolved.
if (isOpenTimeout) {
clearTimeout(isOpenTimeout);
}

if (newValue) {
isOpenTimeout = setTimeout(() => {
isOpen.value = true;
}, 600);
Comment thread
ef81sp marked this conversation as resolved.
} else {
isOpen.value = false;
}
});

// --- audience以外のメンバーの回答がすべて一致しているかを判定 ---
const allAnswersMatch = computed(() => {
if (!room.value || !isOpen.value) return false;
const participants = room.value.participants;
// 観戦者(isAudience)を除外し、回答がある参加者のみを取得
const answerable = participants.filter(p => !p.isAudience && p.answer !== '');
if (answerable.length < 2) return false; // 2人未満なら一致の概念がない

const firstAnswer = answerable[0].answer;
return answerable.every(p => p.answer === firstAnswer);
});

// コンポーネントアンマウント時にタイムアウトをクリア
onUnmounted(() => {
if (isOpenTimeout) {
clearTimeout(isOpenTimeout);
isOpenTimeout = null;
}
});
</script>

<template>
Expand All @@ -176,6 +211,7 @@ const isOpen = computed(() => {
:key="p.userNumber"
:participant="p"
:is-open="isOpen"
:all-answers-match="allAnswersMatch"
/>
</section>
<section class="mt-8">
Expand Down
8 changes: 7 additions & 1 deletion src/components/RoomParticipant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RoomParticipantCard from './RoomParticipantCard.vue';
const props = defineProps<{
participant: RoomForClient['participants'][number];
isOpen: boolean;
allAnswersMatch?: boolean;
}>();
const answer = computed(() => {
if (props.participant.isAudience) return '';
Expand All @@ -23,6 +24,11 @@ const answer = computed(() => {
>
<p class="rotate-[54deg] text-base text-center">Audience</p>
</div>
<RoomParticipantCard v-else :answer="answer" :is-open="props.isOpen" />
<RoomParticipantCard
v-else
:answer="answer"
:is-open="props.isOpen"
:all-answers-match="props.allAnswersMatch || false"
/>
</article>
</template>
44 changes: 38 additions & 6 deletions src/components/RoomParticipantCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import backUrl from '@/assets/trading_card08_back_red.png';
const props = defineProps<{
answer: string;
isOpen: boolean;
allAnswersMatch: boolean;
}>();
const answer = computed(() => {
if (props.answer === '') return '';
Expand All @@ -31,7 +32,10 @@ const cardClass = computed(() => {
</script>

<template>
<Transition mode="out-in">
<Transition
mode="out-in"
:name="props.allAnswersMatch ? 'card-match' : 'card'"
>
<div :class="cardClass" v-if="status === 'reverse'">
<img :src="backUrl" />
</div>
Expand All @@ -43,13 +47,41 @@ const cardClass = computed(() => {
</template>

<style scoped>
.v-enter-active,
.v-leave-active {
@apply transition-transform;
/* 通常のアニメーション */
.card-enter-active,
.card-leave-active {
transition: transform 0.3s ease;
}

.v-enter-from,
.v-leave-to {
.card-enter-from,
.card-leave-to {
transform: rotateY(90deg);
}

/* 回答一致時の特別なアニメーション */
.card-match-enter-active {
transition: transform 0.5s linear;
Comment thread
ef81sp marked this conversation as resolved.
animation: matchReveal 0.3s ease;
}

.card-match-leave-active {
transition: transform 0.5s linear;
}

.card-match-enter-from,
.card-match-leave-to {
transform: rotateY(calc(90deg * 6));
Comment thread
ef81sp marked this conversation as resolved.
Comment thread
ef81sp marked this conversation as resolved.
}

@keyframes matchReveal {
0% {
transform: rotateY(0deg) scale(1);
}
70% {
transform: rotateY(0deg) scale(1.15); /* 少し大きくなる */
}
100% {
transform: rotateY(0deg) scale(1); /* 元のサイズに戻る */
}
}
</style>