たべものを あか・き・みどりの グループに わけよう。ゆびで つまんで、いれたい はこまで うごかしてね。
③ README.txt の手順に沿って script.google.com に貼り付けてください。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>たべもの なかまわけ(あか・き・みどり)</title>
<style>
:root{
--cream:#FBF6EC;
--ink:#2E3440;
--red:#E0684E;
--red-dark:#B84B34;
--red-soft:#FBE2DC;
--yellow:#E8B84B;
--yellow-dark:#C79626;
--yellow-soft:#FBF0D8;
--green:#6FA96B;
--green-dark:#4F8A4B;
--green-soft:#E4F1E1;
--panel:#FFFFFF;
--line:#D9D2C2;
}
*{box-sizing:border-box;}
html,body{
margin:0; padding:0; height:100%;
font-family:"Hiragino Maru Gothic ProN","Yu Gothic",-apple-system,sans-serif;
background:var(--cream);
color:var(--ink);
overflow:hidden;
}
#app{ width:100vw; height:100vh; position:relative; }
#title-screen{
position:absolute; inset:0;
display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:28px; text-align:center; padding:24px;
background:
radial-gradient(circle at 20% 20%, #FFF6E4 0%, transparent 45%),
radial-gradient(circle at 80% 80%, #FDEDE8 0%, transparent 45%),
var(--cream);
}
#title-screen h1{
font-size:clamp(24px, 5vw, 40px);
margin:0;
letter-spacing:.03em;
color:var(--red-dark);
text-shadow: 3px 3px 0 #fff;
}
#title-screen .emoji-row{ font-size:52px; }
#title-screen p{ font-size:14px; color:#6B6A62; max-width:44ch; line-height:1.8; margin:0; }
#start-btn{
font-size:24px; font-weight:700; padding:16px 52px;
border:none; border-radius:999px;
background:var(--red); color:#fff;
box-shadow:0 6px 0 var(--red-dark);
cursor:pointer; transition:all 0.15s ease-out;
}
#start-btn:active{ transform:translateY(6px); box-shadow:0 0 0 var(--red-dark); }
#work-screen{
position:absolute; inset:0;
display:flex; flex-direction:column;
background:var(--cream);
}
header{
padding:10px 20px; font-size:18px; font-weight:700; color:var(--red-dark);
text-align:center; background:var(--panel); border-bottom:3px solid var(--line);
flex:0 0 auto;
}
#zones{
flex: 0 0 auto;
display:flex; gap:10px; padding:12px 14px 6px;
}
.zone{
flex:1 1 0; min-width:0;
display:flex; flex-direction:column;
border-radius:14px; border:3px solid var(--line);
overflow:hidden;
}
.zone[data-group="red"]{ border-color:var(--red); }
.zone[data-group="yellow"]{ border-color:var(--yellow-dark); }
.zone[data-group="green"]{ border-color:var(--green-dark); }
.zone-label{
padding:6px 8px; text-align:center; font-size:12.5px; font-weight:700; line-height:1.5;
color:#fff;
}
.zone[data-group="red"] .zone-label{ background:var(--red); }
.zone[data-group="yellow"] .zone-label{ background:var(--yellow-dark); }
.zone[data-group="green"] .zone-label{ background:var(--green-dark); }
.zone-label .desc{ display:block; font-size:10.5px; font-weight:500; opacity:0.9; }
.zone-drop{
flex:1 1 auto; min-height:64px;
display:flex; flex-wrap:wrap; align-content:flex-start; gap:5px;
padding:6px;
}
.zone[data-group="red"] .zone-drop{ background:var(--red-soft); }
.zone[data-group="yellow"] .zone-drop{ background:var(--yellow-soft); }
.zone[data-group="green"] .zone-drop{ background:var(--green-soft); }
#tray{
flex:1 1 auto; min-height:0;
display:flex; flex-wrap:wrap; align-content:flex-start; justify-content:center;
gap:8px; padding:10px 14px; overflow-y:auto;
}
.food-card{
touch-action:none;
user-select:none;
display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:2px;
width:74px; height:74px;
background:#fff; border:2px solid var(--line); border-radius:12px;
box-shadow:0 3px 0 rgba(0,0,0,0.08);
cursor:grab;
transition: box-shadow 0.15s ease-out, border-color 0.15s ease-out;
}
.food-card .emoji{ font-size:28px; line-height:1; }
.food-card .name{ font-size:10px; font-weight:700; text-align:center; line-height:1.2; padding:0 2px; }
.food-card.dragging{ z-index:999; box-shadow:0 8px 18px rgba(0,0,0,0.25); cursor:grabbing; }
.food-card.placed{ width:60px; height:60px; cursor:default; }
.food-card.placed .emoji{ font-size:22px; }
.food-card.placed .name{ font-size:9px; }
.food-card.correct{ border-color:var(--green-dark); box-shadow:0 0 0 2px var(--green-dark) inset; }
.food-card.incorrect{ border-color:var(--red-dark); box-shadow:0 0 0 2px var(--red-dark) inset; opacity:0.7; }
#action-row{
flex:0 0 auto;
display:flex; gap:10px; justify-content:center; padding:10px 14px;
background:var(--panel); border-top:3px solid var(--line);
}
.action-btn{
font-size:14px; font-weight:700; padding:11px 22px; border:none; border-radius:999px;
cursor:pointer; transition:all 0.15s ease-out;
}
#check-btn{ background:var(--red); color:#fff; box-shadow:0 4px 0 var(--red-dark); }
#check-btn:active{ transform:translateY(4px); box-shadow:0 0 0 var(--red-dark); }
#check-btn:disabled{ background:#cbd5e1; box-shadow:none; cursor:default; }
#reset-btn{ background:#e2e8f0; color:#334155; }
#result-panel{
flex:0 0 auto; display:none;
padding:10px 16px; text-align:center; font-size:14px; font-weight:700;
background:#fff7ea; border-top:3px solid var(--yellow-dark); color:var(--red-dark);
}
#result-panel.show{ display:block; }
#notes-section{
flex:0 0 auto; display:flex; align-items:center; gap:10px;
padding:8px 16px; background:var(--panel); border-top:3px solid var(--line);
}
#notes-label{ font-size:13px; font-weight:700; color:var(--red-dark); white-space:nowrap; }
#notes-text{
flex:1 1 auto; min-height:36px; max-height:70px; resize:vertical;
font-size:14px; line-height:1.5; padding:8px 10px; border-radius:10px; border:2px solid var(--line);
font-family:inherit; background:#FFFDF8;
}
#notes-text:focus{ outline:3px solid var(--yellow); border-color:var(--yellow-dark); }
#hint{
position:fixed; bottom:120px; left:50%; transform:translateX(-50%);
background:rgba(46,52,64,0.85); color:#fff; padding:8px 18px; border-radius:999px;
font-size:13px; pointer-events:none; opacity:0; transition:opacity .3s; text-align:center; max-width:80vw;
}
#hint.show{ opacity:1; }
button:focus-visible, [tabindex]:focus-visible, textarea:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
[hidden]{ display:none !important; }
#send-section{
margin-top: 8px;
padding: 8px 16px 12px;
background: var(--panel);
border-top: 3px solid var(--line);
display: flex;
gap: 8px;
align-items: center;
}
#name-input{
flex: 1 1 auto;
padding: 9px 12px;
font-size: 13.5px;
border-radius: 10px;
border: 2px solid var(--line);
font-family: inherit;
}
#name-input:focus{ outline:none; border-color:#2563eb; box-shadow:0 0 0 3px #dbeafe; }
#send-btn{
flex-shrink: 0;
padding: 10px 18px;
font-size: 13px;
font-weight: 700;
border: none;
border-radius: 10px;
background: #D9634C;
color: #fff;
cursor: pointer;
transition: all 0.15s ease-out;
}
#send-btn:hover{ background: #B5432E; }
#send-status{
position: fixed;
bottom: 14px; left: 50%; transform: translateX(-50%);
font-size: 12.5px; font-weight: 700;
background: rgba(46,52,64,0.9); color: #fff;
padding: 7px 16px; border-radius: 999px;
opacity: 0; pointer-events: none; transition: opacity 0.3s;
white-space: nowrap;
}
#send-status.show{ opacity: 1; }
</style>
</head>
<body>
<div id="app">
<section id="title-screen">
<div class="emoji-row">🥛🍚🥕</div>
<h1>たべもの なかまわけ(あか・き・みどり)</h1>
<p>たべものを あか・き・みどりの グループに わけよう。ゆびで つまんで、いれたい はこまで うごかしてね。</p>
<button id="start-btn">はじめる</button>
</section>
<section id="work-screen" hidden>
<header>たべもの なかまわけ(あか・き・みどり)</header>
<div id="zones">
<div class="zone" data-group="red">
<div class="zone-label">🔴 あか<span class="desc">からだを つくる</span></div>
<div class="zone-drop" data-group="red"></div>
</div>
<div class="zone" data-group="yellow">
<div class="zone-label">🟡 きいろ<span class="desc">ちからの もとになる</span></div>
<div class="zone-drop" data-group="yellow"></div>
</div>
<div class="zone" data-group="green">
<div class="zone-label">🟢 みどり<span class="desc">からだの ちょうしを ととのえる</span></div>
<div class="zone-drop" data-group="green"></div>
</div>
</div>
<div id="tray"></div>
<div id="action-row">
<button class="action-btn" id="check-btn" type="button" disabled>たしかめる</button>
<button class="action-btn" id="reset-btn" type="button">やりなおす</button>
</div>
<div id="result-panel"></div>
<div id="notes-section">
<span id="notes-label">気づいたこと</span>
<textarea id="notes-text" placeholder="気づいたことを書いてみよう"></textarea>
</div>
<div id="send-section">
<input type="text" id="name-input" placeholder="なまえを入力してね">
<button id="send-btn" type="button">📤 送る</button>
</div>
<div id="send-status"></div>
</section>
<div id="hint"></div>
</div>
<script>
(function(){
const svgNS = 'http://www.w3.org/2000/svg';
const titleScreen = document.getElementById('title-screen');
const workScreen = document.getElementById('work-screen');
const startBtn = document.getElementById('start-btn');
const trayEl = document.getElementById('tray');
const checkBtn = document.getElementById('check-btn');
const resetBtn = document.getElementById('reset-btn');
const resultPanel = document.getElementById('result-panel');
const hint = document.getElementById('hint');
const notesText = document.getElementById('notes-text');
startBtn.addEventListener('click', () => {
titleScreen.hidden = true;
workScreen.hidden = false;
});
function showHint(text, duration){
hint.textContent = text;
hint.classList.add('show');
if(duration){
clearTimeout(showHint._t);
showHint._t = setTimeout(() => hint.classList.remove('show'), duration);
}
}
const NOTES_KEY = 'food-groups-lowgrade-notes';
function safeGet(key){ try{ return localStorage.getItem(key); } catch(e){ return null; } }
function safeSet(key, val){ try{ localStorage.setItem(key, val); } catch(e){} }
let saveTimer = null;
notesText.value = safeGet(NOTES_KEY) || '';
notesText.addEventListener('input', () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => safeSet(NOTES_KEY, notesText.value), 400);
});
const foods = [{"id": "milk", "emoji": "🥛", "name": "ぎゅうにゅう", "group": "red"}, {"id": "egg", "emoji": "🥚", "name": "たまご", "group": "red"}, {"id": "meat", "emoji": "🍖", "name": "にく", "group": "red"}, {"id": "rice", "emoji": "🍚", "name": "ごはん", "group": "yellow"}, {"id": "bread", "emoji": "🍞", "name": "パン", "group": "yellow"}, {"id": "potato", "emoji": "🍠", "name": "さつまいも", "group": "yellow"}, {"id": "carrot", "emoji": "🥕", "name": "にんじん", "group": "green"}, {"id": "cabbage", "emoji": "🥬", "name": "キャベツ", "group": "green"}, {"id": "tomato", "emoji": "🍅", "name": "トマト", "group": "green"}];
const placements = {}; // id -> group チョゼン(まだ答え合わせ前)
let checked = false;
function renderTray(){
trayEl.innerHTML = '';
foods.forEach((f) => {
if(placements[f.id]) return;
trayEl.appendChild(createCard(f));
});
}
function createCard(food){
const card = document.createElement('div');
card.className = 'food-card';
card.dataset.id = food.id;
card.innerHTML = '<span class="emoji">' + food.emoji + '</span><span class="name">' + food.name + '</span>';
card.addEventListener('pointerdown', onPointerDown);
return card;
}
let dragState = null;
function onPointerDown(e){
if(checked) return;
const card = e.currentTarget;
if(card.classList.contains('placed')){
// 配置済みのカードは答え合わせ前ならつまみ直せる
delete placements[card.dataset.id];
card.classList.remove('placed');
updateCheckButton();
}
card.setPointerCapture(e.pointerId);
const rect = card.getBoundingClientRect();
dragState = {
card: card,
id: card.dataset.id,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top,
startParent: trayEl,
};
card.classList.add('dragging');
card.style.position = 'fixed';
card.style.left = rect.left + 'px';
card.style.top = rect.top + 'px';
card.style.width = rect.width + 'px';
card.style.height = rect.height + 'px';
document.body.appendChild(card);
card.addEventListener('pointermove', onPointerMove);
card.addEventListener('pointerup', onPointerUp);
}
function onPointerMove(e){
if(!dragState) return;
const card = dragState.card;
card.style.left = (e.clientX - dragState.offsetX) + 'px';
card.style.top = (e.clientY - dragState.offsetY) + 'px';
}
function onPointerUp(e){
if(!dragState) return;
const card = dragState.card;
card.style.display = 'none';
const target = document.elementFromPoint(e.clientX, e.clientY);
card.style.display = '';
const zoneDrop = target && target.closest ? target.closest('.zone-drop') : null;
card.classList.remove('dragging');
card.style.position = '';
card.style.left = '';
card.style.top = '';
card.style.width = '';
card.style.height = '';
card.removeEventListener('pointermove', onPointerMove);
card.removeEventListener('pointerup', onPointerUp);
if(zoneDrop){
const group = zoneDrop.dataset.group;
placements[dragState.id] = group;
card.classList.add('placed');
zoneDrop.appendChild(card);
} else {
trayEl.appendChild(card);
}
dragState = null;
updateCheckButton();
}
function updateCheckButton(){
const allPlaced = foods.every((f) => placements[f.id]);
checkBtn.disabled = !allPlaced;
}
checkBtn.addEventListener('click', () => {
checked = true;
let correctCount = 0;
foods.forEach((f) => {
const card = document.querySelector('.food-card[data-id="' + f.id + '"]');
if(!card) return;
const isCorrect = placements[f.id] === f.group;
card.classList.add(isCorrect ? 'correct' : 'incorrect');
if(isCorrect) correctCount++;
});
if(correctCount === foods.length){
resultPanel.textContent = '🎉 全部せいかい!(' + correctCount + ' / ' + foods.length + ')';
} else {
resultPanel.textContent = correctCount + ' / ' + foods.length + ' 問正解。赤いわくのカードをもう一度考えてみよう。';
}
resultPanel.classList.add('show');
checkBtn.disabled = true;
});
resetBtn.addEventListener('click', () => {
checked = false;
for(const key in placements) delete placements[key];
document.querySelectorAll('.food-card').forEach((c) => {
c.classList.remove('correct', 'incorrect', 'placed');
});
resultPanel.classList.remove('show');
renderTray();
updateCheckButton();
});
const nameInput = document.getElementById('name-input');
const sendBtn = document.getElementById('send-btn');
const sendStatus = document.getElementById('send-status');
const SEND_NAME_KEY = 'food-groups-lowgrade-notes-name';
nameInput.value = safeGet(SEND_NAME_KEY) || '';
function showSendStatus(text, cls){
sendStatus.textContent = text;
sendStatus.className = cls ? 'show ' + cls : 'show';
clearTimeout(showSendStatus._t);
showSendStatus._t = setTimeout(() => sendStatus.classList.remove('show'), 2600);
}
sendBtn.addEventListener('click', () => {
if(!checked){
showSendStatus('先に「たしかめる」を押してね');
return;
}
const name = nameInput.value.trim();
if(!name){
showSendStatus('なまえを入力してね');
nameInput.focus();
return;
}
safeSet(SEND_NAME_KEY, name);
if (typeof google === 'undefined' || !google.script || !google.script.run) {
showSendStatus('この機能はGoogleスプレッドシート連携版として配信された環境でのみ使えます。');
return;
}
let correctCount = 0;
const summary = foods.map((f) => {
const ok = placements[f.id] === f.group;
if(ok) correctCount++;
const labels = { red: 'あか', yellow: 'きいろ', green: 'みどり' };
return f.name + ':' + (labels[placements[f.id]] || '?') + (ok ? '(正)' : '(誤/正解=' + labels[f.group] + ')');
}).join(' / ');
sendBtn.disabled = true;
showSendStatus('送信中…');
google.script.run
.withSuccessHandler(function(){
showSendStatus('送りました!ありがとう!');
sendBtn.disabled = false;
})
.withFailureHandler(function(){
showSendStatus('送信に失敗しました。もう一度試してね');
sendBtn.disabled = false;
})
.sendFoodGroupResult({
name: name,
correctCount: correctCount,
total: foods.length,
summary: summary,
notes: notesText.value,
});
});
renderTray();
})();
</script>
</body>
</html>
/**
* 「たべもの なかまわけ(あか・き・みどり)」 - みんなの回答をスプレッドシートに送る機能
*
* 使い方(デプロイ手順)は README.txt を参照してください。
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('たべもの なかまわけ(あか・き・みどり)')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getOrCreateSpreadsheet_() {
const props = PropertiesService.getScriptProperties();
const savedId = props.getProperty('SPREADSHEET_ID');
if (savedId) {
try {
return SpreadsheetApp.openById(savedId);
} catch (err) {
// 保存されていたIDが無効な場合は作り直す
}
}
const ss = SpreadsheetApp.create('たべもの なかまわけ(あか・き・みどり) - みんなの回答');
const sheet = ss.getSheets()[0];
sheet.appendRow(['送信日時', 'なまえ', '正答数', '全問数', '回答内容(食品:選んだ色(正/誤))', '気づいたこと']);
sheet.setFrozenRows(1);
props.setProperty('SPREADSHEET_ID', ss.getId());
return ss;
}
/**
* フロント側から google.script.run 経由で呼ばれる。
* data = { name: string, correctCount: number, total: number, summary: string, notes: string }
*/
function sendFoodGroupResult(data) {
if (!data || !data.name) {
throw new Error('なまえがありません');
}
const ss = getOrCreateSpreadsheet_();
const sheet = ss.getSheets()[0];
sheet.appendRow([
new Date(),
data.name.toString().slice(0, 40),
data.correctCount,
data.total,
(data.summary || '').toString().slice(0, 3000),
(data.notes || '').toString().slice(0, 1000),
]);
return { ok: true };
}
「たべもの なかまわけ(あか・き・みどり)」 スプレッドシート送信版 - デプロイ手順
====================================================
このフォルダには3つのファイルがあります。
・index.html … 児童が触る画面(オフライン版に「なまえ」欄と「送る」ボタンを追加したもの)
・Code.gs … スプレッドシートにデータを送るためのバックエンド処理
・README.txt … このファイル
【1】Google Apps Script プロジェクトを作る
1. Google ドライブを開く(学校アカウントでログイン)
2. 「新規」→「その他」→「Google Apps Script」
(見当たらない場合は https://script.google.com/ から「新しいプロジェクト」)
3. プロジェクト名を「たべもの なかまわけ(あか・き・みどり)」などにする
【2】コードを貼り付ける
1. 左側のファイル一覧にある「Code.gs」を開き、中身をすべて削除
2. 渡した Code.gs の内容をすべて貼り付けて保存(Ctrl+S)
3. 左側の「+」→「HTML」を選び、ファイル名を必ず「index」にする(拡張子は自動でつきます)
4. 渡した index.html の内容をすべて貼り付けて保存
【3】ウェブアプリとして公開する
1. 右上の「デプロイ」→「新しいデプロイ」
2. 種類の選択(歯車マーク)で「ウェブアプリ」を選ぶ
3. 「次のユーザーとして実行」→ 自分(先生のアカウント)
4. 「アクセスできるユーザー」→ 学校のドメイン内の全員
(例:kita9.ed.jp 内の全員、など。学校のGoogle Workspace設定に合わせてください)
5. 「デプロイ」をクリックし、表示されたウェブアプリのURLをコピーする
6. 初回のみ、権限の承認画面が出るので許可する
【4】児童に配布する
・コピーしたURLをブックマークやGoogle Classroom等で共有すれば、
Chromebookのブラウザからそのまま使えます。
【5】スプレッドシートの確認
・誰かが初めて「送る」を押したときに、自動で
「たべもの なかまわけ(あか・き・みどり) - みんなのふりかえり」という
スプレッドシートがドライブに作成されます。
・以降の送信は、そのシートに1人1行ずつ追加されていきます。
・共有設定(誰が見られるか)は、作成後にドライブから
先生が調整してください(デフォルトは先生のみ閲覧可)。
【困ったときは】
・貼り付けたコードを直接書き換えても大丈夫です。次に相談するときは
「Code.gsの◯◯を直したい」のように伝えてもらえれば調整できます。