<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Memory Recorder Pro</title>
<!-- Bootstrap CSS & Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<!-- Chart.js for statistics -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
:root{
--bg-main:#ffffff;
--bg-gradient:linear-gradient(135deg,#f8f9fa 0%,#e9ecef 100%);
--text-main:#212529;
--accent:#0d6efd;
}
:root.dark{
--bg-main:#1e1e1e;
--bg-gradient:linear-gradient(135deg,#2b2b2b 0%,#1e1e1e 100%);
--text-main:#f8f9fa;
}
body{
background:var(--bg-gradient);
color:var(--text-main);
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
transition:background .3s ease,color .3s ease;
}
.memory-app{
width:100%;
max-width:920px;
background:var(--bg-main);
padding:2rem 2.5rem;
border-radius:1.5rem;
box-shadow:0 4px 20px rgba(0,0,0,.1);
transition:background .3s ease;
}
.memory-card{
border-left:4px solid var(--accent);
padding-left:1rem;
margin-bottom:1rem;
}
.tag-badge{
background:var(--accent);
color:#fff;
margin-right:.25rem;
cursor:pointer;
}
.btn-accent{
background:var(--accent);
border-color:var(--accent);
color:#fff;
}
</style>
</head>
<body>
<div class="memory-app">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="m-0">📝 Memory Recorder <small class="h6 fw-light">Pro</small></h1>
<div class="d-flex align-items-center gap-3">
<button id="statsBtn" class="btn btn-outline-secondary btn-sm"><i class="bi bi-bar-chart"></i></button>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" id="darkModeSwitch">
</div>
</div>
</div>
<!-- Search & Stats row -->
<div class="row g-3 align-items-end mb-4">
<div class="col-md-8">
<label for="searchInput" class="form-label">検索(テキスト / タグ)</label>
<input type="text" id="searchInput" class="form-control" placeholder="キーワードで検索...">
</div>
<div class="col-md-4 text-md-end">
<p id="stats" class="mb-0 small text-muted"></p>
</div>
</div>
<!-- Input Area -->
<div class="mb-3">
<label for="memoryText" class="form-label">新しい記憶</label>
<textarea class="form-control" id="memoryText" rows="3" placeholder="出来事・思い出など..."></textarea>
</div>
<div class="mb-4 row g-2 align-items-end">
<div class="col-md-8">
<label for="memoryTags" class="form-label">タグ(カンマ区切り)</label>
<input type="text" id="memoryTags" class="form-control" placeholder="例: 仕事, 家族, 趣味">
</div>
<div class="col-md-4 d-flex gap-2 mt-md-4">
<button id="saveBtn" class="btn btn-primary flex-grow-1"><i class="bi bi-save"></i> 保存</button>
<button id="voiceBtn" class="btn btn-outline-secondary" title="音声入力"><i class="bi bi-mic"></i></button>
</div>
</div>
<!-- File / Clear row -->
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="exportBtn" class="btn btn-outline-secondary"><i class="bi bi-download"></i> エクスポート</button>
<button id="importBtn" class="btn btn-outline-secondary"><i class="bi bi-upload"></i> インポート</button>
<button id="clearAllBtn" class="btn btn-outline-danger ms-auto"><i class="bi bi-trash"></i> 全削除</button>
<input type="file" id="importFile" accept="application/json" class="d-none">
</div>
<hr>
<h2 class="h5 mb-3">📚 保存された記憶</h2>
<div id="memoryList"></div>
</div>
<!-- Statistics Modal -->
<div class="modal fade" id="statsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-graph-up"></i> 記憶統計</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<canvas id="statsChart" height="120"></canvas>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">記憶を編集</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="editText" class="form-label">内容</label>
<textarea id="editText" class="form-control" rows="4"></textarea>
</div>
<div class="mb-3">
<label for="editTags" class="form-label">タグ(カンマ区切り)</label>
<input id="editTags" class="form-control">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">キャンセル</button>
<button id="editSaveBtn" class="btn btn-primary">保存</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script>
// --------- Constants ---------
const STORAGE_KEY = "memories";
const THEME_KEY = "prefers-dark";
// --------- Helpers ---------
const $ = sel => document.querySelector(sel);
const modal = id => new bootstrap.Modal($(id));
const memories = {
list() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); },
save(arr) { localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); },
add(obj){ const arr = this.list(); arr.push(obj); this.save(arr);} ,
remove(id){ this.save(this.list().filter(m=>m.id!==id));},
update(id,data){ const arr=this.list().map(m=>m.id===id?{...m,...data}:m); this.save(arr);} ,
};
const fmtDate = d => new Intl.DateTimeFormat("ja-JP",{dateStyle:"medium",timeStyle:"short"}).format(d);
// --------- Rendering ---------
function renderMemories(filter=""){
const listEl = $("#memoryList");
listEl.innerHTML="";
const all = memories.list();
const lower = filter.toLowerCase();
const visible = all.filter(m=>{
const tagMatch = m.tags.some(t=>t.toLowerCase().includes(lower));
const textMatch= m.text.toLowerCase().includes(lower);
return !lower || tagMatch || textMatch;
}).reverse();
// stats
$("#stats").textContent=`合計: ${all.length} 件`;
if(!visible.length){listEl.innerHTML='<p class="text-muted">該当する記憶がありません。</p>';return;}
visible.forEach(m=>{
const card=document.createElement("div");
card.className="memory-card card";
card.innerHTML=`<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start flex-wrap">
<h5 class="card-title mb-1">${fmtDate(new Date(m.date))}</h5>
<div class="btn-group btn-group-sm">
<button class="btn btn-link text-primary" title="編集" onclick="openEditor('${m.id}')"><i class="bi bi-pencil"></i></button>
<button class="btn btn-link text-danger" title="削除" onclick="deleteMemory('${m.id}')"><i class="bi bi-trash"></i></button>
</div>
</div>
<p class="card-text" style="white-space:pre-wrap;">${m.text}</p>
<div class="mt-2">${m.tags.map(t=>`<span class=\"badge tag-badge\" onclick=\"filterTag('${t}')\">${t}</span>`).join(" ")}</div>
</div>`;
listEl.appendChild(card);
});
}
// --------- CRUD ---------
function saveMemory(){
const text=$("#memoryText").value.trim();
const tagRaw=$("#memoryTags").value.trim();
if(!text)return;
const tags=tagRaw?tagRaw.split(/\s*,\s*/).filter(Boolean):[];
memories.add({id:crypto.randomUUID(),text,tags,date:new Date().toISOString()});
$("#memoryText").value="";
$("#memoryTags").value="";
renderMemories($("#searchInput").value);
}
function deleteMemory(id){
if(!confirm("削除しますか?"))return;
memories.remove(id);
renderMemories($("#searchInput").value);
}
// --------- Edit ---------
let editingId=null;
function openEditor(id){
editingId=id;
const m=memories.list().find(x=>x.id===id);
$("#editText").value=m.text;
$("#editTags").value=m.tags.join(", ");
modal('#editModal').show();
}
$("#editSaveBtn").addEventListener("click",()=>{
const text=$("#editText").value.trim();
const tags=$("#editTags").value.trim().split(/\s*,\s*/).filter(Boolean);
memories.update(editingId,{text,tags});
modal('#editModal').hide();
renderMemories($("#searchInput").value);
});
// --------- Filter helper ---------
function filterTag(tag){
$("#searchInput").value=tag;
renderMemories(tag);
}
// --------- Export / Import ---------
function exportMemories(){
const blob=new Blob([JSON.stringify(memories.list(),null,2)],{type:"application/json"});
const url=URL.createObjectURL(blob);
const a=document.createElement("a");
a.href=url;a.download="memories.json";a.click();URL.revokeObjectURL(url);
}
function importMemories(file){
const reader=new FileReader();
reader.onload=e=>{try{const arr=JSON.parse(e.target.result);if(Array.isArray(arr)){memories.save([...memories.list(),...arr]);renderMemories($("#searchInput").value);}else throw 0;}catch{alert("無効なファイルです");}};
reader.readAsText(file);
}
// --------- Statistics ---------
let chartInstance=null;
function openStats(){
const data=memories.list();
const counts={};
data.forEach(m=>{
const key=m.date.slice(0,7); // YYYY-MM
counts[key]=(counts[key]||0)+1;
});
const labels=Object.keys(counts).sort();
const values=labels.map(l=>counts[l]);
const ctx=$("#statsChart");
if(chartInstance)chartInstance.destroy();
chartInstance=new Chart(ctx,{type:'bar',data:{labels,datasets:[{label:'記憶数',data:values}]},options:{plugins:{legend:{display:false}}}});
modal('#statsModal').show();
}
// --------- Dark Mode ---------
function applyTheme(dark){
document.documentElement.classList.toggle('dark',dark);
localStorage.setItem(THEME_KEY,dark?'1':'0');
$("#darkModeSwitch").checked=dark;
}
// --------- Voice Input (Experimental) ---------
let rec=null;
async function startVoice(){
if(!('webkitSpeechRecognition'in window||'SpeechRecognition'in window)){alert('音声認識非対応');return;}
const Speech=window.SpeechRecognition||window.webkitSpeechRecognition;
rec=new Speech();
rec.lang='ja-JP';
rec.continuous=false;
rec.interimResults=false;
rec.onresult=e=>{$('#memoryText').value=e.results[0][0].transcript;};
rec.start();
}
// --------- Init ---------
document.addEventListener('DOMContentLoaded',()=>{
// Theme
applyTheme(localStorage.getItem(THEME_KEY)==='1');
// Render memories
renderMemories();
// Listeners
$('#saveBtn').addEventListener('click',saveMemory);
$('#clearAllBtn').addEventListener('click',()=>{if(confirm('すべて削除しますか?')){localStorage.removeItem(STORAGE_KEY);renderMemories();}});
$('#exportBtn').addEventListener('click',exportMemories);
$('#importBtn').addEventListener('click',()=>$('#importFile').click());
$('#importFile').addEventListener('change',e=>{if(e.target.files[0])importMemories(e.target.files[0]);e.target.value='';});
$('#darkModeSwitch').addEventListener('change',e=>applyTheme(e.target.checked));
$('#searchInput').addEventListener('input',e=>renderMemories(e.target.value));
$('#memoryText').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.ctrlKey||e.metaKey))saveMemory();});
$('#statsBtn').addEventListener('click',openStats);
$('#voiceBtn').addEventListener('click',startVoice);
});
</script>
</body>
</html>