
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、私には姪っ子が2人いるのですが、定期的に私のところに来てくれることもあってお小遣いやお年玉をあげています。
ただ、普通にあげるだけじゃ面白くないので、これまで以下のようなあげ方をしてきました。
- 部屋にお金を隠しておいて宝探しをさせる(達成感もプレゼント)
- 「わいろだよ」と書いたポチ袋であげる(クスッと笑いがほしかった)
- カナダドルであげる(いつかカナダに行くんだよと言いました。迷惑かな…
)
しかし、正直もういろいろとやったので今年は何にしようかと考えていたところひとつアイデアが浮かびました。
それは・・・・・・
宝箱にダイヤル式のロックをかけておき、3つの数字があればお年玉をゲットできる
という企画です。
そのためだけに 2,500円の宝箱をamazon
で買いました
↓↓↓
宝箱 鍵付き アンティーク 箱 ジュエリーボックス おもちゃ ヴィンテージ レトロ風 ボックス 海賊 木製 宝石箱 小物入れ【PR】
※ できるだけ費用を回収したいので、リンクはAmazon
アフィリエイトになってます。この企画をやってみたい方はぜひ
そして、せっかくプログラマーなので何かウェブサイトを作って「へぇ、こういうの自分で作れるんだ」というのも感じてもらえたら、と思い宝箱を開ける3つの数字は「Laravel + Vue」で作ってみることにしました。
デモページもつくってます。
以下からどうぞ。(3つの数字の答えはコードの中に入ってます)
デモページ(もう一台スマホいります!)
ぜひ何かの参考になりましたら嬉しいです
「部屋にお金を隠したときは、
次は町内のどこかに隠せ、
と言われました 」
開発環境: Laravel 10.x、Vue 3(Options API)
ビューをつくる
今回は技術的なことはほぼないので、いきなりコードをご紹介します。(CDN
を使っているのでパッケージのインストールも不要です)
※ なお、画像はデモページとは違い「https://placehold.jp/150×150.png」に変更しています。
resources/views/treasure/index.blade.php
<html>
<head>
<title>新春お年玉企画❗3つの数字で宝箱を開けろ💰✨</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hina+Mincho&family=Noto+Serif+JP:wght@600&display=swap" rel="stylesheet">
<style>
.noto-serif-jp {
font-family: 'Noto Serif JP', serif;
}
.hina-mincho {
font-family: 'Hina Mincho', serif;
}
</style>
</head>
<body class="p-8 bg-[url('https://placehold.jp/150x150.png')] bg-cover w-full h-screen">
<div id="app" >
<div class="text-3xl text-red-700 text-center noto-serif-jp font-bold mb-3">
【新春お年玉企画❗】
</div>
<div class="text-5xl text-red-700 text-center mb-10 noto-serif-jp font-bold" style="line-height:5rem;">
3つの数字で宝箱を開けろ💰✨
</div>
<div class="flex mb-10">
<div class="w-1/2 flex justify-center items-start pt-24">
<img src="https://placehold.jp/150x150.png" class="w-2/3 border-4 border-white">
</div>
<div class="w-1/2 flex justify-center items-start">
<img src="https://placehold.jp/150x150.png" class="w-3/4">
</div>
</div>
<!-- 1つ目の問題 -->
<heading-component
title="〜 1つ目の数字 〜"
difficulty="1"
description="ちょうど真ん中の数字をゲットしろ❗❗"></heading-component>
<div class="flex mb-32">
<div class="w-full flex justify-center items-start">
<div id="qrcode" class="p-5 bg-white border-4 border-black"></div>
</div>
</div>
<!-- 2つ目の問題 -->
<heading-component
title="〜 2つ目の数字 〜"
difficulty="3"
description="□ に当てはまる数字をゲットしろ❗❗"></heading-component>
<div class="text-4xl text-center bg-gray-50 border-gray-100 rounded-lg p-5 mb-32" style="line-height:4rem;">
<div class="text-right text-sm text-blue-600 underline">
<a href="#" @click.prevent="isShowingClueOfSecondQuestion = true">ヒントを使う</a>
</div>
<div v-html="secondQuestionText"></div>
</div>
<!-- 3つ目の問題 -->
<heading-component
title="〜 3つ目の数字 〜"
difficulty="5"
description="□ に当てはまる数字をゲットしろ❗❗"></heading-component>
<div class="text-5xl text-center bg-gray-50 border-gray-100 rounded-lg p-5 mb-12" style="line-height:4rem;">
<div class="text-right text-sm text-blue-600 underline">
<a href="#" @click.prevent="isShowingClueOfThirdQuestion = true">ヒントを使う</a>
</div>
<div v-html="thirdQuestionText"></div>
</div>
<br>
</div>
<!-- Template -->
<template id="heading-template">
<div class="text-4xl text-red-700 text-center mb-5 hina-mincho font-bold" v-text="title"></div>
<div class="text-2xl text-red-700 text-center mb-5 hina-mincho" v-text="difficultyStars"></div>
<div class="mb-5 hina-mincho text-black bg-gray-50 border-2 border-gray-100 p-6 rounded-lg">
<div class="text-2xl " v-text="description"></div>
</div>
<div class="text-4xl text-center text-gray-800 mb-8 font-bold">
↓ ↓ ↓
</div>
</template>
<script src="https://unpkg.com/vue@3.3.12/dist/vue.global.prod.js"></script>
<script src="https://cdn.tailwindcss.com/3.3.7"></script>
<script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script>
const headingComponent = {
props: {
title: {
type: String,
required: true,
},
difficulty: {
type: Number,
required: true,
},
description: {
type: String,
required: true,
},
},
template: '#heading-template',
computed: {
difficultyStars() {
const starCount = 5;
const blackStars = _.repeat('★', this.difficulty);
const whiteStars = _.repeat('☆', starCount - this.difficulty);
return `難易度: ${blackStars}${whiteStars}`;
}
}
};
Vue.createApp({
components: {
headingComponent,
},
data() {
return {
answers: [
8, // 0 〜 9
4, // 0 〜 6
9, // 6 or 9
],
isShowingClueOfSecondQuestion: false,
isShowingClueOfThirdQuestion: false,
}
},
methods: {
init() {
this.initQrcode();
},
initQrcode() {
const qrcodeAnswer = this.answers[0];
const firstRandomNumber = _.random(1111111111, 9999999999);
const secondRandomNumber = _.random(1111111111, 9999999999);
const answer = `${firstRandomNumber}${qrcodeAnswer}${secondRandomNumber}`;
new QRCode(document.getElementById('qrcode'), answer);
}
},
computed: {
secondQuestionText() {
let questionTexts = [];
const answerNumber = this.answers[1];
const dayNames = (this.isShowingClueOfSecondQuestion === true)
? ['に○○○び', 'げ○○○び', 'か○○び', 'す○○○び', 'も○○○び', 'き○○○び', 'ど○○び']
: ['に', 'げ', 'か', 'す', 'も', 'き', 'ど'];
if(typeof dayNames[answerNumber] === 'undefined') {
const maxIndex = dayNames.length - 1;
alert(`2目の答えは、0 〜 ${maxIndex}の数字で指定してください`);
} else {
dayNames.forEach((dayName, index) => {
const questionText = (index === answerNumber)
? `${dayName} → <span class="text-red-600 font-bold">□</span>`
: `${dayName} → ${index}`;
questionTexts.push(questionText);
});
const shuffledQuestionTexts = _.shuffle(questionTexts);
return shuffledQuestionTexts.join('<br>');
}
},
thirdQuestionText() {
let questionTexts = [];
const answerNumber = this.answers[2];
if([6, 9].includes(answerNumber) === false) {
alert('3目の答えは、6 か 9で指定してください');
return;
}
const answerPatterns = [
{ answer: 6, question: '→ + →' },
{ answer: 9, question: '→ + ↓' },
{ answer: 12, question: '↓ + ↓' },
{ answer: 15, question: '→ + ↑' },
{ answer: 18, question: '↓ + ↑' },
{ answer: 21, question: '← + ↑' },
{ answer: 24, question: '↑ + ↑' },
];
const shuffledAnswerPatterns = _.shuffle(answerPatterns);
shuffledAnswerPatterns.forEach((answerPattern) => {
let answer = '';
if(answerPattern.answer !== answerNumber) {
answer = answerPattern.answer;
} else {
answer = (this.isShowingClueOfThirdQuestion === true)
? '🕒'
: '□';
}
const question = answerPattern.question;
const questionText = `${question} = <span class="text-red-600 font-bold">${answer}</span>`;
questionTexts.push(questionText);
});
return questionTexts.join('<br>');
}
},
mounted() {
this.init();
}
})
.mount('#app');
</script>
</body>
</html>
ちなみに背景のテクスチャはChatGPT
のDALL·E
で作り、加工したものを使いました。ホントに便利になりましたね。
ルートをつくる
routes/web.php
// 省略
Route::get('treasure', fn() => view('treasure.index'));
これもシンプルですね。
では、今回は短いですがこれで作業は完了です。
お疲れ様でした。
テストしてみる
では、実際にテスト…といたいところですがデモページを用意しているので実際に試してみてください。
デモページ
企業様へのご提案
今回は家庭内での話でしたが、私はこういったリアルとデジタルの融合したコンテンツが好きなので、もし何かそういったことでお力になれそうでしたらいつでもお気軽にご相談ください。
お待ちしております。
おわりに
ということで今回は100%自己満足な記事でしたがいかがだったでしょうか。
せっかく子供たちにお金というチャンスを上げるなら、プラスアルファで何かをゲットしてほしいものです。
※ ちなみに、実際にセットしてみたところダイヤルロックの番号変更がわからず相当焦りました(小さな黒い穴を耳かきのような細い棒で押し込むと変更できるようでした)
みなさんもぜひ何か「面白いじゃなく、面白そう
」な企画を考えてみてくださいね。
ではでは〜
「姪っ子たちには
前倒しでトライしてもらいました
楽しそうにやってました」