0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTML単語テンプレート

0
Posted at

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=5.0,user-scalable=yes,viewport-fit=cover">
    <title>汎用暗記修行システム</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <style>
        :root {
            --bg: #030712;
            --container: #111827;
            --card-bg: #1f2937;
            --text: #f9fafb;
            --primary: #3b82f6;
            --success: #10b981;
            --danger: #ef4444;
            --gray: #6b7280;
            --accent: #8b5cf6;
            --gold: #fbbf24;
            --silver: #94a3b8;
        }
        * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; outline: none; }
        html, body { 
            margin: 0; width: 100%; height: 100%; background: var(--bg); 
            font-family: 'Inter', system-ui, -apple-system, sans-serif; color: var(--text); 
            overflow-x: hidden; touch-action: manipulation;
        }
        
        #setup, #report { padding: 20px; min-height: 100vh; overflow-y: auto; display: none; }
        #setup.active, #report.active { display: block; }
        #app { display: none; position: fixed; inset: 0; background: var(--bg); flex-direction: column; padding: 12px; z-index: 100; }
        #app.active { display: flex; }

        .container { max-width: 650px; margin: 0 auto; background: var(--container); border-radius: 28px; padding: 20px; border: 1px solid #374151; box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
        h1 { font-size: 24px; text-align: center; margin: 0 0 15px 0; color: var(--primary); font-weight: 900; letter-spacing: 1px; }

        .dashboard { background: rgba(31, 41, 55, 0.5); border-radius: 24px; padding: 18px; margin-bottom: 20px; border: 1px solid #374151; }
        .db-bar { height: 14px; display: flex; border-radius: 7px; overflow: hidden; background: #030712; margin-bottom: 12px; }
        .db-seg { height: 100%; transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
        .seg-a { background: var(--gold); }
        .seg-b { background: var(--accent); }
        .seg-c { background: var(--danger); }
        .seg-new { background: #4b5563; }
        .db-legend { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
        .lg-item { text-align: center; }
        .lg-label { font-size: 10px; color: var(--gray); display: block; margin-bottom: 2px; }
        .lg-val { font-size: 12px; font-weight: 900; }

        .label { font-size: 11px; font-weight: 800; color: var(--silver); margin: 16px 0 6px 4px; display: flex; justify-content: space-between; align-items: center; }
        .param-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
        @media (max-width: 420px) { .param-grid { grid-template-columns: 1fr; } }

        .input-group { background: #1f2937; padding: 12px; border-radius: 18px; border: 1px solid #374151; }
        .input-group span { font-size: 10px; margin-bottom: 6px; color: #9ca3af; display: block; font-weight: bold; }
        
        .adj-container { display: flex; align-items: center; justify-content: center; gap: 10px; }
        .adj-btn { background: #334155; color: white; border: none; padding: 10px 14px; border-radius: 12px; font-size: 13px; font-weight: 800; cursor: pointer; min-width: 44px; }
        .adj-btn:active { transform: scale(0.9); background: var(--primary); }

        input { width: 75px; background: transparent; border: none; color: white; font-size: 20px; font-weight: 900; text-align: center; }
        input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

        .range-box { background: linear-gradient(145deg, #1e1b4b, #111827); padding: 18px; border-radius: 24px; border: 1px solid var(--accent); margin-bottom: 18px; }
        textarea { width: 100%; height: 55px; background: rgba(0,0,0,0.4); color: #94a3b8; border: 1px solid #374151; border-radius: 14px; padding: 12px; font-size: 12px; margin-top: 6px; outline: none; resize: none; word-break: break-all; }

        button.main-btn { width: 100%; height: 64px; border: none; border-radius: 22px; font-size: 22px; font-weight: 900; cursor: pointer; background: var(--primary); color: white; transition: 0.2s; box-shadow: 0 12px 24px -6px rgba(59, 130, 246, 0.5); }
        button.main-btn:active { transform: scale(0.97); }

        .sm-btn { background: #374151; color: white; border: none; padding: 7px 14px; border-radius: 10px; font-size: 11px; cursor: pointer; font-weight: 800; }
        .io-btn { background: var(--accent); color: white; border: none; padding: 7px 14px; border-radius: 10px; font-size: 11px; font-weight: 800; cursor: pointer; }
        
        #card { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 25px; gap: 10px; background: var(--card-bg); border-radius: 48px; margin: 10px 0; position: relative; border: 1px solid #374151; box-shadow: inset 0 0 40px rgba(0,0,0,0.4); }
        #itemBadge { position: absolute; top: 30px; font-size: 20px; font-weight: 900; padding: 4px 18px; border-radius: 12px; background: rgba(0,0,0,0.3); border: 2px solid currentColor; }
        
        #questionText { font-size: 40px; font-weight: 900; line-height: 1.1; color: var(--text); word-break: break-all; max-width: 95%; margin-top: 30px; }
        #answerText { font-size: 24px; color: var(--primary); font-weight: 800; visibility: hidden; min-height: 1.5em; margin-top: 15px; }
        #answerText.visible { visibility: visible; }
        
        #timerTrack { width: 100%; height: 10px; background: #030712; margin-top: 25px; border-radius: 10px; overflow: hidden; opacity: 0; }
        #timerTrack.active { opacity: 1; }
        #timerBar { height: 100%; background: var(--danger); width: 0%; }

        .progress-container { width: 100%; height: 8px; background: #1f2937; border-radius: 10px; overflow: hidden; margin: 8px 0; }
        #progressBar { height: 100%; background: var(--primary); width: 0%; transition: width 0.3s linear; }
        
        .ans-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; width: 100%; margin-bottom: 12px; }
        .ans-btn { height: 90px; border: none; border-radius: 32px; color: white; font-size: 26px; font-weight: 900; cursor: pointer; transition: 0.15s; }
        .ans-btn:active { transform: scale(0.94); }
        #correctBtn { background: var(--success); }
        #wrongBtn { background: var(--danger); }
    </style>
</head>
<body>

<div id="setup" class="active">
    <div class="container">
        <h1 id="titleLabel">修行システム</h1>

        <div class="dashboard">
            <div class="db-bar">
                <div id="bar-a" class="db-seg seg-a"></div>
                <div id="bar-b" class="db-seg seg-b"></div>
                <div id="bar-c" class="db-seg seg-c"></div>
                <div id="bar-new" class="db-seg seg-new"></div>
            </div>
            <div class="db-legend">
                <div class="lg-item"><span class="lg-label">完了(a)</span><span id="val-a" class="lg-val" style="color:var(--gold)">0</span></div>
                <div class="lg-item"><span class="lg-label">継続(b)</span><span id="val-b" class="lg-val" style="color:var(--accent)">0</span></div>
                <div class="lg-item"><span class="lg-label">苦手(c)</span><span id="val-c" class="lg-val" style="color:var(--danger)">0</span></div>
                <div class="lg-item"><span class="lg-label">未着手</span><span id="val-new" class="lg-val" style="color:var(--silver)">0</span></div>
            </div>
        </div>
        
        <div class="input-group" style="margin-bottom:15px; background: #1e1b4b;">
            <div style="display:flex; justify-content: space-between; align-items: center; margin-bottom:6px;">
                <span style="color:#fff">進捗復元 / 書き出し</span>
                <div style="display:flex; gap:6px;">
                    <button class="io-btn" style="background: var(--success);" onclick="importData()">復元</button>
                    <button class="io-btn" onclick="exportData()">出す</button>
                </div>
            </div>
            <textarea id="importArea" oninput="updateDashboard()" placeholder="保存用データを貼り付けて「復元」"></textarea>
        </div>

        <div class="range-box">
            <div class="label">修行範囲と出題数</div>
            <div class="param-grid">
                <div class="input-group">
                    <span>開始ID</span>
                    <div class="adj-container"><button class="adj-btn" onclick="adj('rangeStart', -100)">-</button><input type="number" id="rangeStart" value="1"><button class="adj-btn" onclick="adj('rangeStart', 100)">+</button></div>
                </div>
                <div class="input-group">
                    <span>終了ID</span>
                    <div class="adj-container"><button class="adj-btn" onclick="adj('rangeEnd', -100)">-</button><input type="number" id="rangeEnd" value="100"><button class="adj-btn" onclick="adj('rangeEnd', 100)">+</button></div>
                </div>
            </div>
            <div class="input-group" style="background: rgba(255,255,255,0.05); margin-top:8px;">
                <span>今回の修行数</span>
                <div class="adj-container"><button class="adj-btn" onclick="adj('testLimit', -10)">-10</button><input type="number" id="testLimit" value="20"><button class="adj-btn" onclick="adj('testLimit', 10)">+10</button></div>
            </div>
        </div>

        <div class="label">テンポ設定</div>
        <div class="param-grid">
            <div class="input-group"><span>解答猶予(秒)</span><input type="number" id="limitTime" value="1.5" step="0.1"></div>
            <div class="input-group"><span>自動読み上げ</span><input type="checkbox" id="voiceToggle" checked></div>
        </div>

        <button class="main-btn" onclick="startTraining()">修行開始</button>
    </div>
</div>

<div id="app">
    <div id="infoText" style="text-align:center; font-size:11px; color:var(--gray); margin-bottom:4px; font-weight:bold;"></div>
    <div class="progress-container"><div id="progressBar"></div></div>
    <div id="card">
        <div id="itemBadge">NEW</div>
        <div id="questionText"></div>
        <div id="answerText"></div>
        <div id="timerTrack"><div id="timerBar"></div></div>
    </div>
    <div class="ans-btns">
        <button id="wrongBtn" class="ans-btn" onclick="processAnswer(false)">苦手 (A)</button>
        <button id="correctBtn" class="ans-btn" onclick="processAnswer(true)">わかる (Z)</button>
    </div>
    <div style="text-align:center;">
        <button onclick="finishTraining()" style="background:none; border:none; color:var(--gray); font-size:11px; font-weight:bold; cursor:pointer;">中断 (Space)</button>
    </div>
</div>

<div id="report">
    <div class="container" style="text-align:center;">
        <h1>修行完了</h1>
        <div id="resultScore" style="font-size:32px; font-weight:900; color:var(--success); margin-bottom:20px;"></div>
        <textarea id="numericalArea" style="height:70px;" readonly></textarea>
        <button class="main-btn" onclick="location.reload()" style="margin-top:25px; background:var(--gray);">トップへ戻る</button>
    </div>
</div>

<script>
// --- [設定エリア] アプリ名やデータを定義 ---
const APP_TITLE = "一般暗記修行システム";
const STORAGE_KEY = 'memorize_mastery_template';
// ----------------------------------------

let ITEMS = [];
let mastery = {};
let queue = [];
let results = [];
let activeItem = null;
let stressTimer = null;
let audioCtx = null;

function playSound(type) {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.type = type === 'correct' ? 'sine' : 'square';
    osc.frequency.setValueAtTime(type === 'correct' ? 880 : 220, audioCtx.currentTime);
    gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.2);
}

function loadData() {
    const raw = document.getElementById('raw-data').textContent.trim();
    ITEMS = raw.split('\n').filter(l => l.includes(',')).map((line, idx) => {
        const parts = line.split(',');
        return { id: idx + 1, q: parts[0].trim(), a: parts[1].trim() };
    });
    document.getElementById('rangeEnd').value = ITEMS.length;
    document.getElementById('titleLabel').textContent = APP_TITLE;
}

function updateDashboard() {
    const total = ITEMS.length || 1;
    const counts = { a: 0, b: 0, c: 0, new: 0 };
    Object.values(mastery).forEach(v => counts[v]++);
    counts.new = Math.max(0, total - (counts.a + counts.b + counts.c));
    ['a', 'b', 'c', 'new'].forEach(k => {
        const val = counts[k];
        const pct = (val / total * 100).toFixed(1);
        document.getElementById(`bar-${k}`).style.width = pct + '%';
        document.getElementById(`val-${k}`).textContent = `${val} (${pct}%)`;
    });
}

function speak(txt, lang) {
    if (!document.getElementById('voiceToggle').checked) return;
    window.speechSynthesis.cancel();
    const ut = new SpeechSynthesisUtterance(txt);
    ut.lang = lang;
    ut.rate = 1.2;
    window.speechSynthesis.speak(ut);
}

function startTraining() {
    const start = parseInt(document.getElementById('rangeStart').value);
    const end = parseInt(document.getElementById('rangeEnd').value);
    const limit = parseInt(document.getElementById('testLimit').value);
    
    let available = ITEMS.filter(item => item.id >= start && item.id <= end);
    if (available.length === 0) return alert("範囲内にデータがありません");

    available.sort(() => Math.random() - 0.5);
    queue = available.slice(0, limit);
    results = [];
    document.getElementById("setup").classList.remove('active');
    document.getElementById("app").classList.add('active');
    nextItem();
}

function nextItem() {
    clearTimeout(stressTimer);
    const track = document.getElementById("timerTrack");
    const bar = document.getElementById("timerBar");
    track.classList.remove('active');
    bar.style.transition = "none";
    bar.style.width = "0%";
    document.getElementById("answerText").classList.remove('visible');
    
    if (queue.length === 0) return finishTraining();
    activeItem = queue.shift();
    
    document.getElementById("infoText").textContent = `ID:${activeItem.id} | ${results.length+1} / ${results.length + queue.length + 1}`;
    document.getElementById("progressBar").style.width = `${(results.length / (results.length + queue.length + 1)) * 100}%`;
    
    const status = mastery[activeItem.id];
    const badge = document.getElementById("itemBadge");
    badge.textContent = status ? status.toUpperCase() : "NEW";
    badge.style.color = status === 'c' ? "var(--danger)" : (status === 'b' ? "var(--accent)" : (status === 'a' ? "var(--gold)" : "var(--gray)"));

    document.getElementById("questionText").textContent = activeItem.q;
    document.getElementById("answerText").textContent = activeItem.a;
    
    speak(activeItem.q, 'en-US'); // 必要に応じて言語設定を変更
    
    // 解答表示とタイマー開始
    setTimeout(() => {
        document.getElementById("answerText").classList.add('visible');
        speak(activeItem.a, 'ja-JP');
        startStressTimer();
    }, 800);
}

function startStressTimer() {
    const sec = parseFloat(document.getElementById('limitTime').value) || 1.5;
    const track = document.getElementById("timerTrack");
    const bar = document.getElementById("timerBar");
    track.classList.add('active');
    bar.style.transition = `width ${sec}s linear`;
    bar.style.width = "100%";
    stressTimer = setTimeout(() => processAnswer(false), sec * 1000);
}

function processAnswer(isOk) {
    clearTimeout(stressTimer);
    if (!activeItem) return;
    playSound(isOk ? 'correct' : 'wrong');
    const id = activeItem.id;
    if (isOk) {
        mastery[id] = (mastery[id] === 'c') ? 'b' : 'a';
    } else {
        mastery[id] = 'c';
    }
    results.push({ ...activeItem, ok: isOk });
    localStorage.setItem(STORAGE_KEY, JSON.stringify(mastery));
    nextItem();
}

function finishTraining() {
    window.speechSynthesis.cancel();
    document.getElementById("app").classList.remove('active');
    document.getElementById("report").classList.add('active');
    const okCount = results.filter(r => r.ok).length;
    document.getElementById("resultScore").textContent = `${okCount} / ${results.length}`;
    document.getElementById("numericalArea").value = Object.keys(mastery).map(id=>`${id}${mastery[id]}`).join(',');
}

function adj(id, delta) {
    const el = document.getElementById(id);
    el.value = Math.max(1, parseInt(el.value) + delta);
    updateDashboard();
}

function exportData() {
    const s = Object.keys(mastery).map(id=>`${id}${mastery[id]}`).join(',');
    document.getElementById('importArea').value = s;
    navigator.clipboard.writeText(s).then(() => alert("進捗をコピーしました"));
}

function importData() {
    const txt = document.getElementById('importArea').value.trim();
    mastery = {};
    txt.split(',').forEach(t => {
        const m = t.match(/(\d+)([abc])/);
        if (m) mastery[parseInt(m[1])] = m[2];
    });
    localStorage.setItem(STORAGE_KEY, JSON.stringify(mastery));
    updateDashboard();
    alert("進捗を復元しました");
}

window.onload = () => {
    loadData();
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) mastery = JSON.parse(saved);
    updateDashboard();
};

document.onkeydown = (e) => {
    if (!document.getElementById("app").classList.contains('active')) return;
    if (e.key.toLowerCase() === 'z') processAnswer(true);
    if (e.key.toLowerCase() === 'a') processAnswer(false);
    if (e.code === 'Space') { e.preventDefault(); finishTraining(); }
};
</script>

<script type="text/plain" id="raw-data">
質問,回答
Apple,りんご
Book,
Car,
Dog,
</script>

</body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?