diff --git a/quiz-app2/src/App.vue b/quiz-app2/src/App.vue
index e8f6e6b..50316ac 100644
--- a/quiz-app2/src/App.vue
+++ b/quiz-app2/src/App.vue
@@ -129,7 +129,6 @@ const compactTierTitle = (titleText: I18nText | string) => {
let timerId: number | null = null;
let startedAt = 0;
-// --- i18n Dictionary ---
const i18n = computed(() => {
const isEn = selectedLanguage.value === "en";
return {
@@ -161,12 +160,12 @@ const i18n = computed(() => {
completedText: isEn ? "Completed" : "完了",
specialUnlocked: isEn ? "Special Unlocked" : "スペシャル解放",
allCorrectTitle: isEn
- ? "Special Page Unlocked!"
- : "スペシャルページ解放!",
+ ? "Present a souvenir!"
+ : "おみやげをプレゼント!",
allCorrectDesc: isEn
- ? "You have completed this set and unlocked the special page."
- : "このセットを最後まで進めたため、スペシャルページへ進めます。",
- goToSpecial: isEn ? "Go to Special Page" : "スペシャルページへ",
+ ? "Prepared some souvenirs for everyone played."
+ : "プレイしてくれたみんなのために記念品を用意したよ。",
+ goToSpecial: isEn ? "Go to Souvenirs Page" : "おみやげページ",
nextLevel: isEn ? "Next Level" : "次のレベルへ",
retryLevel: isEn
? isSingleTier
@@ -199,6 +198,18 @@ const i18n = computed(() => {
};
});
+watch(selectedLanguage, () => {
+ if (
+ keywordMessage.value === "合言葉が違います。" ||
+ keywordMessage.value === "Incorrect keyword."
+ ) {
+ keywordMessage.value =
+ selectedLanguage.value === "en"
+ ? "Incorrect keyword."
+ : "合言葉が違います。";
+ }
+});
+
const correctFeedbackPhrases: I18nText[] = [
{ ja: "その調子!", en: "Keep it up!" },
{ ja: "素晴らしい!", en: "Excellent!" },
@@ -389,14 +400,14 @@ const shareText = computed(() => {
const tierName = t(currentTier.value.title);
const baseText = isEn
- ? `Cleared ${isSingleTier ? "all stages" : tierName} on Go Conference 2026 CodeLab! Correct: ${runCorrectCount.value}/${tierSize.value} Score: ${runScore.value}`
- : `Go Conference 2026 CodeLabで${isSingleTier ? "全問題を" : ` ${tierName} を`}クリアしました! 正解数: ${runCorrectCount.value}/${tierSize.value} スコア: ${runScore.value}`;
+ ? `Cleared ${isSingleTier ? "all stages" : tierName} on Go Conference 2026 CodeLab!\nCorrect: ${runCorrectCount.value}/${tierSize.value} Score: ${runScore.value}`
+ : `Go Conference 2026 CodeLabで${isSingleTier ? "全問題を" : ` ${tierName} を`}クリアしました!\n正解数: ${runCorrectCount.value}/${tierSize.value} スコア: ${runScore.value}`;
const url =
typeof window !== "undefined"
? window.location.href.split("#")[0].split("?")[0]
: "https://gocon.jp/";
- return `${baseText}\n#gocon26cl\n${url}`;
+ return `${baseText}\n${url}\n#gocon26 #gocon26cl`;
});
const xShareUrl = computed(
@@ -571,6 +582,10 @@ const finishTierRun = () => {
const goToNextStage = () => {
if (!currentResult.value) return;
+ if (typeof window !== "undefined") {
+ window.scrollTo(0, 0);
+ }
+
if (currentQuestionIndex.value === tierSize.value - 1) {
finishTierRun();
return;
@@ -624,7 +639,10 @@ const closeKeywordModal = () => {
const submitKeyword = () => {
if (keywordValue.value.trim() !== PREVIEW_UNLOCK_KEYWORD) {
- keywordMessage.value = i18n.value.keywordIncorrect;
+ keywordMessage.value =
+ selectedLanguage.value === "en"
+ ? "Incorrect keyword."
+ : "合言葉が違います。";
return;
}
diff --git a/quiz-app2/src/components/games/FillBlankTapGame.vue b/quiz-app2/src/components/games/FillBlankTapGame.vue
index e32322b..560c549 100644
--- a/quiz-app2/src/components/games/FillBlankTapGame.vue
+++ b/quiz-app2/src/components/games/FillBlankTapGame.vue
@@ -65,8 +65,15 @@ const clearSelection = () => {
const slotIsFilled = (item: PoolItem) =>
slots.value.some((slot) => slot?.id === item.id);
-const addToken = (item: PoolItem) => {
+const toggleToken = (item: PoolItem) => {
if (props.locked) return;
+
+ const existingIndex = slots.value.findIndex((s) => s?.id === item.id);
+ if (existingIndex !== -1) {
+ slots.value[existingIndex] = null;
+ return;
+ }
+
const index = slots.value.findIndex((s) => s === null);
if (index !== -1) {
slots.value[index] = item;
@@ -124,6 +131,10 @@ watch(
@@ -171,8 +182,8 @@ watch(
}}
{{
locale === "en"
- ? "Tap to insert from left"
- : "タップで左から挿入"
+ ? "Tap to insert/remove"
+ : "タップで追加 / 解除"
}}
@@ -181,9 +192,10 @@ watch(
v-for="item in poolItems"
:key="item.id"
tone="secondary"
- :disabled="slotIsFilled(item) || locked"
- class="px-4 py-3 font-mono text-[13px]"
- @click="addToken(item)"
+ :disabled="locked"
+ class="px-4 py-3 font-mono text-[13px] transition-all"
+ :class="{ 'opacity-30': slotIsFilled(item) }"
+ @click="toggleToken(item)"
>
{{ item.label }}
diff --git a/quiz-app2/src/components/ui/StageOutputPanel.vue b/quiz-app2/src/components/ui/StageOutputPanel.vue
index ed91f01..1c5fdce 100644
--- a/quiz-app2/src/components/ui/StageOutputPanel.vue
+++ b/quiz-app2/src/components/ui/StageOutputPanel.vue
@@ -2,11 +2,24 @@
import { inject, ref, type Ref } from "vue";
import type { Locale } from "../../types";
-defineProps<{
+const props = defineProps<{
lines: string[];
+ isFill?: boolean;
+ slots?: any[];
+ locked?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (e: "remove-token", index: number): void;
}>();
const locale = inject("locale", ref("ja")) as Ref;
+
+// プレースホルダーの解析ロジック
+const splitLine = (line: string) => line.split(/(\[\d+\])/).filter(Boolean);
+const isSlot = (part: string) => /^\[\d+\]$/.test(part);
+const slotIndex = (part: string) =>
+ parseInt(part.replace(/[\[\]]/g, ""), 10) - 1;
@@ -22,9 +35,37 @@ const locale = inject("locale", ref("ja")) as Ref;
- {{ line }}
+
+
+
+ {{ part }}
+
+
+
+ {{ line }}
+
diff --git a/quiz-app2/src/data/stages.toml b/quiz-app2/src/data/stages.toml
index 755a0f6..e58b964 100644
--- a/quiz-app2/src/data/stages.toml
+++ b/quiz-app2/src/data/stages.toml
@@ -17,7 +17,7 @@ why = { ja = "Go には `while` や `loop` といったキーワードは存在
takeaway = { ja = "`for` の条件式を省略して `for {}` と書くだけで、もっともシンプルでスッキリとした無限ループを作成できます。", en = "By omitting the condition from a `for` statement and simply writing `for {}`, you can create an infinite loop more simply and clearly." }
templateLines = """
[1] {
- // 無限ループ
+
}
"""
pool = [ "for", "while", "loop", "(true)", "True:" ]
@@ -161,20 +161,17 @@ label = { ja = "defer", en = "defer" }
title = { ja = "defer の実行順序", en = "Execution order of defer" }
prompt = { ja = "コードの実行結果(出力)の順番を上から完成させてください。", en = "Complete the execution order (output) from top to bottom." }
outputLines = """
+[1]
+[2]
+[3]
"""
playgroundUrl = "https://go.dev/play/p/p9uTA5pzU1G"
why = { ja = "`defer` に登録された関数は、関数が終了する際に「最後に追加されたものから順に(LIFO: Last-In, First-Out)」逆順で実行されます。", en = "`defer` statements are executed in reverse order (LIFO: Last-In, First-Out) when the function returns." }
takeaway = { ja = "通常の文(C)が先に実行され、その後に `defer` が下から上の順(B -> A)で実行されるため、出力は C、B、A の順になります。", en = "Since regular statements (C) execute first, followed by `defer` statements in reverse order (B -> A), the output will be C, B, then A." }
templateLines = """
-// ── 実行されるコード ──
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
-
-// ── 出力結果 ──
-[1]
-[2]
-[3]
"""
pool = [ "A", "B", "C" ]
correctAnswers = [ "C", "B", "A" ]
diff --git a/quiz-app2/src/data/stages.ts b/quiz-app2/src/data/stages.ts
index 1f09935..8c8190b 100644
--- a/quiz-app2/src/data/stages.ts
+++ b/quiz-app2/src/data/stages.ts
@@ -11,7 +11,7 @@ import stagesDocument from "./stages.toml";
export const STAGE_TIME_LIMIT_MS = 30_000;
export const PREVIEW_UNLOCK_KEYWORD = "gofar,gotogether";
export const FOOTER_TAP_THRESHOLD = 10;
-export const SPECIAL_PAGE_URL = "https://example.com";
+export const SPECIAL_PAGE_URL = "https://gocon.jp/2026/";
type TomlRecord = Record;
@@ -160,13 +160,15 @@ const parseStageBase = (
};
const validateFillTemplate = (stage: FillStage, path: string) => {
- const placeholderIndexes = stage.templateLines.flatMap((line) =>
+ const allLines = [...stage.templateLines, ...stage.outputLines];
+
+ const placeholderIndexes = allLines.flatMap((line) =>
Array.from(line.matchAll(/\[(\d+)\]/g), (match) => Number(match[1])),
);
if (placeholderIndexes.length !== stage.correctAnswers.length) {
throw new Error(
- `${path}.templateLines must contain ${stage.correctAnswers.length} placeholders.`,
+ `${path} must contain exactly ${stage.correctAnswers.length} placeholders across templateLines and outputLines.`,
);
}
@@ -178,7 +180,7 @@ const validateFillTemplate = (stage: FillStage, path: string) => {
for (const [index, value] of placeholderIndexes.entries()) {
if (value !== expectedIndexes[index]) {
throw new Error(
- `${path}.templateLines placeholders must be numbered [1]...[${stage.correctAnswers.length}] in order.`,
+ `${path} placeholders must be numbered [1]...[${stage.correctAnswers.length}] in order across templateLines and outputLines.`,
);
}
}
diff --git a/quiz-app2/test/app.ui.test.ts b/quiz-app2/test/app.ui.test.ts
index 2f92e3a..39c4300 100644
--- a/quiz-app2/test/app.ui.test.ts
+++ b/quiz-app2/test/app.ui.test.ts
@@ -121,7 +121,7 @@ describe("quiz-app2 campaign flow", () => {
expect(shell().attributes("data-screen")).toBe("score");
expect(wrapper.text()).toContain(`${stages.length}/${stages.length}`);
- expect(wrapper.text()).toContain("スペシャルページへ");
+ expect(wrapper.text()).toContain("おみやげをプレゼント");
// Tierが1つだけの場合はボタン名が「もう一度挑戦する」に変化する対応
const isSingleTier = campaignTiers.length === 1;