Untitled
Anonymous
html
01/29/2026 4:32 AM
69.3 KB
14
Indexable
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>History Study – Flashcards + Practice Test</title>
<style>
:root{
--bg:#0b1020; --panel:#121a33; --panel2:#0f1630;
--text:#e9eeff; --muted:#aeb8e6; --line:#22305f;
--accent:#7aa2ff; --good:#3ddc97; --bad:#ff6b7a; --warn:#ffd166;
--shadow: 0 12px 30px rgba(0,0,0,.35);
--radius: 18px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
}
*{box-sizing:border-box}
body{
margin:0; font-family:var(--sans);
background: radial-gradient(1200px 800px at 20% -10%, rgba(122,162,255,.22), transparent 55%),
radial-gradient(900px 700px at 100% 0%, rgba(61,220,151,.16), transparent 50%),
var(--bg);
color:var(--text);
min-height:100vh;
}
a{color:inherit}
.wrap{max-width:1100px;margin:0 auto;padding:22px 16px 40px}
header{
display:flex; gap:14px; align-items:flex-end; justify-content:space-between; flex-wrap:wrap;
padding:14px 2px 18px;
}
.brand h1{
margin:0; font-size:26px; letter-spacing:.2px;
}
.brand p{margin:6px 0 0;color:var(--muted); font-size:13px; max-width:700px}
.tabs{
display:flex; gap:10px; align-items:center; flex-wrap:wrap;
background:rgba(18,26,51,.55); border:1px solid var(--line);
padding:8px; border-radius:999px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
}
.tab{
border:1px solid transparent;
background:transparent; color:var(--text);
padding:9px 12px; border-radius:999px;
cursor:pointer; font-weight:650; font-size:13px;
transition: .15s ease;
}
.tab:hover{border-color:rgba(122,162,255,.35)}
.tab.active{background:rgba(122,162,255,.18); border-color:rgba(122,162,255,.45)}
.grid{
display:grid; grid-template-columns: 1.3fr .7fr; gap:16px;
}
@media (max-width: 940px){
.grid{grid-template-columns:1fr}
}
.card{
background: linear-gradient(180deg, rgba(18,26,51,.86), rgba(15,22,48,.86));
border:1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
.card .head{
padding:14px 16px;
border-bottom:1px solid var(--line);
display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
}
.card .head h2{margin:0;font-size:15px}
.card .head .sub{color:var(--muted);font-size:12px}
.body{padding:16px}
.row{display:flex; gap:10px; align-items:center; flex-wrap:wrap}
.pill{
font-family: var(--mono);
font-size:12px; color:var(--muted);
padding:6px 10px; border-radius:999px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.55);
}
.btn{
cursor:pointer;
border:1px solid rgba(122,162,255,.35);
background: rgba(122,162,255,.12);
color:var(--text);
padding:10px 12px;
border-radius: 12px;
font-weight:700;
font-size:13px;
transition:.15s ease;
user-select:none;
}
.btn:hover{transform: translateY(-1px); border-color: rgba(122,162,255,.55)}
.btn:active{transform: translateY(0px)}
.btn.secondary{
border-color: rgba(174,184,230,.22);
background: rgba(174,184,230,.08);
}
.btn.danger{
border-color: rgba(255,107,122,.35);
background: rgba(255,107,122,.12);
}
.btn.good{
border-color: rgba(61,220,151,.35);
background: rgba(61,220,151,.12);
}
.btn.small{padding:7px 10px; border-radius: 10px; font-size:12px}
.inputs{
display:grid; grid-template-columns: 1fr 1fr; gap:10px;
margin-top:10px;
}
@media (max-width: 520px){
.inputs{grid-template-columns:1fr}
}
input[type="text"], select, textarea{
width:100%;
padding:11px 12px;
border-radius: 12px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.55);
color:var(--text);
outline:none;
}
textarea{min-height:110px; resize:vertical; font-family:var(--sans)}
label{font-size:12px; color:var(--muted); display:block; margin:0 0 6px 2px}
.hr{height:1px;background:var(--line);margin:14px 0}
.progress{
height:10px; border-radius:999px;
background: rgba(34,48,95,.5);
overflow:hidden; border:1px solid rgba(34,48,95,.9);
}
.bar{height:100%; width:0%; background: rgba(122,162,255,.75)}
/* Flashcard */
.flashcard{
display:grid;
gap:12px;
}
.fc-stage{
position:relative;
background: rgba(10,16,40,.45);
border:1px solid rgba(34,48,95,.9);
border-radius: var(--radius);
min-height: 230px;
padding:18px 18px 16px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.03);
}
.fc-badge{
position:absolute; top:14px; right:14px;
font-size:12px; color:var(--muted);
padding:6px 10px; border-radius:999px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.55);
font-family: var(--mono);
}
.fc-front{
font-size:20px; font-weight:850; letter-spacing:.2px; margin:10px 0 6px;
line-height:1.18;
}
.fc-back{
margin:12px 0 0;
color:var(--text);
font-size:14px;
line-height:1.45;
display:none;
white-space:pre-wrap;
}
.fc-hint{
margin-top:10px;
color: var(--muted);
font-size:12px;
}
.fc-controls{display:flex; gap:10px; flex-wrap:wrap; align-items:center}
.known{
display:flex; gap:10px; align-items:center; flex-wrap:wrap;
padding:10px 12px;
border-radius: 14px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.4);
}
.toggle{
display:flex; align-items:center; gap:10px;
}
.toggle input{transform: scale(1.1)}
.tag{
font-size:12px; padding:5px 10px; border-radius:999px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.55);
color:var(--muted);
}
.tag.good{border-color: rgba(61,220,151,.35); background: rgba(61,220,151,.10); color: rgba(199,255,231,.95)}
.tag.bad{border-color: rgba(255,107,122,.35); background: rgba(255,107,122,.10); color: rgba(255,220,225,.98)}
/* Test */
.q{
padding:14px 14px;
border-radius: 16px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.45);
margin-bottom:12px;
}
.q .meta{display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; color:var(--muted); font-size:12px}
.q .prompt{margin:10px 0 12px; font-size:14px; line-height:1.35}
.choices{display:grid; gap:10px}
.choice{
cursor:pointer;
padding:10px 12px;
border-radius: 12px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.35);
transition:.12s ease;
user-select:none;
}
.choice:hover{border-color: rgba(122,162,255,.55)}
.choice.selected{border-color: rgba(122,162,255,.75); background: rgba(122,162,255,.14)}
.choice.correct{border-color: rgba(61,220,151,.75); background: rgba(61,220,151,.14)}
.choice.wrong{border-color: rgba(255,107,122,.75); background: rgba(255,107,122,.14)}
.result{
padding:12px 14px;
border-radius: 14px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.45);
display:none;
}
.result strong{font-size:14px}
.muted{color:var(--muted)}
.footer-note{margin-top:12px; font-size:12px; color:var(--muted)}
.split{display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between}
.kpi{
display:grid; grid-template-columns: 1fr 1fr; gap:10px;
}
.kpi .box{
padding:12px; border-radius: 16px;
border:1px solid rgba(34,48,95,.9);
background: rgba(10,16,40,.40);
}
.kpi .box .big{font-size:18px; font-weight:900}
.kpi .box .small{font-size:12px; color:var(--muted)}
.hide{display:none !important}
.mini{font-size:12px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="brand">
<h1>History Study</h1>
<p>Flashcards + Practice Test (auto-saves progress in your browser). Use Search + filters to focus your studying.</p>
</div>
<div class="tabs" role="tablist" aria-label="Modes">
<button class="tab active" data-view="flashcards" role="tab" aria-selected="true">Flashcards</button>
<button class="tab" data-view="test" role="tab" aria-selected="false">Practice Test</button>
<button class="tab" data-view="manage" role="tab" aria-selected="false">Manage Cards</button>
</div>
</header>
<div class="grid">
<!-- Main -->
<section class="card" id="mainCard">
<div class="head">
<div>
<h2 id="mainTitle">Flashcards</h2>
<div class="sub" id="mainSub">Flip, shuffle, and track what you know.</div>
</div>
<div class="row">
<span class="pill" id="deckInfo">Deck: 0 cards</span>
<span class="pill" id="modeInfo">Mode: Flashcards</span>
</div>
</div>
<div class="body">
<!-- Flashcards View -->
<div id="view-flashcards">
<div class="flashcard">
<div class="fc-stage" id="fcStage">
<div class="fc-badge" id="fcBadge">Card 0 / 0</div>
<div class="fc-front" id="fcFront">—</div>
<div class="fc-back" id="fcBack">—</div>
<div class="fc-hint" id="fcHint">Tip: press <span class="pill">Space</span> to flip, <span class="pill">←</span>/<span class="pill">→</span> to move.</div>
</div>
<div class="fc-controls">
<button class="btn secondary" id="prevBtn">← Prev</button>
<button class="btn" id="flipBtn">Flip</button>
<button class="btn secondary" id="nextBtn">Next →</button>
<button class="btn secondary" id="shuffleBtn">Shuffle</button>
<button class="btn danger" id="resetProgressBtn">Reset Progress</button>
</div>
<div class="known">
<div class="toggle">
<input type="checkbox" id="knownToggle" />
<label for="knownToggle" style="margin:0;color:var(--text);font-weight:750">Mark as “Known”</label>
</div>
<span class="tag" id="categoryTag">Category: —</span>
<span class="tag" id="knownTag">Status: —</span>
</div>
<div>
<div class="split">
<div class="mini">Known progress</div>
<div class="mini"><span id="knownCount">0</span>/<span id="totalCount">0</span> known</div>
</div>
<div class="progress" aria-label="Progress">
<div class="bar" id="progressBar"></div>
</div>
</div>
</div>
</div>
<!-- Test View -->
<div id="view-test" class="hide">
<div class="row" style="margin-bottom:12px">
<button class="btn" id="startTestBtn">Start New Test</button>
<button class="btn secondary" id="gradeTestBtn">Grade</button>
<button class="btn secondary" id="newSetBtn">New Questions</button>
</div>
<div class="inputs">
<div>
<label for="testCount"># of Questions</label>
<select id="testCount">
<option>5</option>
<option selected>10</option>
<option>15</option>
<option>20</option>
<option>25</option>
</select>
</div>
<div>
<label for="testType">Question Type</label>
<select id="testType">
<option value="mcq" selected>Multiple Choice (term → definition)</option>
<option value="tf">True / False (facts)</option>
<option value="mixed">Mixed</option>
</select>
</div>
</div>
<div class="hr"></div>
<div id="testArea"></div>
<div class="result" id="testResult">
<div class="split">
<strong id="scoreLine">Score: —</strong>
<span class="pill" id="scoreDetail">—</span>
</div>
<div class="footer-note" id="reviewNote">Review missed questions below (they’ll be highlighted).</div>
</div>
</div>
<!-- Manage View -->
<div id="view-manage" class="hide">
<div class="inputs">
<div>
<label for="searchInput">Search (term/definition)</label>
<input id="searchInput" type="text" placeholder="Search… e.g., ‘Odyssey’ or ‘World War’" />
</div>
<div>
<label for="categoryFilter">Category</label>
<select id="categoryFilter">
<option value="all" selected>All</option>
</select>
</div>
</div>
<div class="inputs">
<div>
<label for="knownFilter">Known filter</label>
<select id="knownFilter">
<option value="all" selected>All</option>
<option value="known">Known only</option>
<option value="unknown">Unknown only</option>
</select>
</div>
<div class="row" style="align-items:flex-end">
<button class="btn secondary" id="exportBtn">Export JSON</button>
<button class="btn secondary" id="importBtn">Import JSON</button>
<input id="importFile" type="file" accept="application/json" class="hide" />
</div>
</div>
<div class="hr"></div>
<div class="kpi">
<div class="box">
<div class="big" id="kpiTotal">0</div>
<div class="small">Total cards</div>
</div>
<div class="box">
<div class="big" id="kpiKnown">0</div>
<div class="small">Known cards</div>
</div>
</div>
<div class="hr"></div>
<div>
<label for="addCategory">Add / Edit Card</label>
<div class="inputs">
<div>
<label for="addCategory">Category</label>
<input id="addCategory" type="text" placeholder="e.g., U.S. History" />
</div>
<div>
<label for="addTerm">Term</label>
<input id="addTerm" type="text" placeholder="e.g., War of 1812" />
</div>
</div>
<div style="margin-top:10px">
<label for="addDef">Definition</label>
<textarea id="addDef" placeholder="Definition…"></textarea>
</div>
<div class="row" style="margin-top:10px">
<button class="btn good" id="saveCardBtn">Save Card</button>
<button class="btn secondary" id="clearFormBtn">Clear</button>
<button class="btn danger" id="deleteCardBtn">Delete (by exact term)</button>
</div>
<div class="footer-note">Tip: To edit an existing card, type the exact Term and hit Save.</div>
</div>
<div class="hr"></div>
<div id="listArea"></div>
</div>
</div>
</section>
<!-- Sidebar -->
<aside class="card">
<div class="head">
<div>
<h2>Study Settings</h2>
<div class="sub">Customize what shows up.</div>
</div>
</div>
<div class="body">
<div>
<label for="studyCategory">Study category</label>
<select id="studyCategory">
<option value="all" selected>All</option>
</select>
</div>
<div style="margin-top:10px">
<label for="studyScope">Study scope</label>
<select id="studyScope">
<option value="all" selected>All cards</option>
<option value="unknown">Unknown only</option>
<option value="known">Known only</option>
</select>
</div>
<div class="hr"></div>
<div class="row">
<button class="btn secondary small" id="jumpRandomBtn">Random Card</button>
<button class="btn secondary small" id="showAnswerBtn">Show Answer</button>
</div>
<div class="hr"></div>
<div class="mini">
Keyboard:
<div class="footer-note">
<span class="pill">Space</span> flip
<span class="pill">←</span>/<span class="pill">→</span> prev/next
<span class="pill">K</span> toggle known
</div>
</div>
<div class="hr"></div>
<div class="mini">
Data is stored locally (your browser). Use Export/Import in “Manage Cards” to move it to another device.
</div>
</div>
</aside>
</div>
</div>
<script>
/**************
* DEFAULT DECK
**************/
const DEFAULT_CARDS = [
// Terms/Definitions
{ category:"U.S. History", term:"War of 1812", definition:"A conflict fought between the United States and Great Britain from 1812 to 1815 over trade restrictions, British impressment of American sailors, and U.S. expansion." },
{ category:"Ancient History", term:"The Odyssey", definition:"An ancient Greek epic poem attributed to Homer that follows the hero Odysseus on his long journey home after the Trojan War." },
{ category:"Soviet History", term:"Glasnost", definition:"A Soviet policy introduced by Mikhail Gorbachev in the 1980s that promoted openness, transparency, and freedom of information in government." },
{ category:"U.S. History", term:"George Washington", definition:"The first president of the United States and commander of the Continental Army during the American Revolutionary War." },
{ category:"Latin American / European", term:"Falklands War", definition:"A brief war fought in 1982 between Argentina and the United Kingdom over control of the Falkland Islands in the South Atlantic." },
{ category:"European History", term:"Bohemia", definition:"A historical region in Central Europe, now part of the Czech Republic, that played an important role in European political and cultural history." },
{ category:"U.S. History", term:"Apollo Program", definition:"A U.S. space program run by NASA that successfully landed the first humans on the Moon in 1969." },
{ category:"Economics", term:"Milton Friedman", definition:"An influential American economist who promoted free-market capitalism and limited government intervention in the economy." },
{ category:"European History", term:"Volodymyr Zelenskyy", definition:"The president of Ukraine, elected in 2019, who has led the country during the Russo-Ukrainian War." },
{ category:"Canadian History", term:"Quiet Revolution", definition:"A period of rapid social, political, and economic change in Quebec during the 1960s that reduced the influence of the Catholic Church." },
{ category:"European History", term:"Battle of Borodino", definition:"A major battle fought in 1812 between Napoleon’s army and Russian forces during Napoleon’s invasion of Russia." },
{ category:"Asian History", term:"Mao Zedong", definition:"The communist leader who founded the People’s Republic of China in 1949 and ruled until his death in 1976." },
{ category:"U.S. History", term:"Five Civilized Tribes", definition:"A term used to describe five Native American tribes (Cherokee, Chickasaw, Choctaw, Creek, and Seminole) that adopted aspects of European-American culture." },
{ category:"Ancient History", term:"Cyrus the Great", definition:"The founder of the Persian Empire, known for his military conquests and policies of religious tolerance." },
{ category:"European / Asian History", term:"Marco Polo", definition:"A Venetian explorer who traveled to Asia in the 13th century and helped increase European knowledge of China." },
{ category:"U.S. History", term:"Iran Hostage Crisis", definition:"A 1979–1981 event in which Iranian militants held 52 American diplomats hostage in Tehran." },
{ category:"European History", term:"René Descartes", definition:"A French philosopher and mathematician often considered the father of modern philosophy, famous for the phrase “I think, therefore I am.”" },
{ category:"U.S. History", term:"Zoot Suit Riots", definition:"A series of violent clashes in Los Angeles in 1943 between U.S. servicemen and Mexican American youths." },
{ category:"European History", term:"Brexit", definition:"The United Kingdom’s withdrawal from the European Union, formally completed in 2020." },
{ category:"Asian History", term:"Lee Kuan Yew", definition:"The first prime minister of Singapore, credited with transforming the country into a major global economic power." },
{ category:"European History", term:"Archduke Franz Ferdinand", definition:"The heir to the Austro-Hungarian throne whose assassination in 1914 sparked World War I." },
{ category:"Oceania History", term:"Musket Wars", definition:"A series of conflicts in New Zealand between Māori tribes during the early 19th century, intensified by the introduction of firearms." },
{ category:"European History", term:"Estado Novo", definition:"An authoritarian political regime that ruled Portugal from the 1930s to the 1970s under António Salazar." },
{ category:"European History", term:"French Revolution", definition:"A major political and social upheaval in France from 1789 to 1799 that led to the overthrow of the monarchy." },
{ category:"World History", term:"Great Depression", definition:"A worldwide economic downturn beginning in 1929 that caused massive unemployment and poverty." },
{ category:"Latin American History", term:"Juan Ponce de León", definition:"A Spanish explorer who explored Florida and is often associated with the legend of the Fountain of Youth." },
{ category:"World History", term:"NATO", definition:"The North Atlantic Treaty Organization, a military alliance formed in 1949 for collective defense." },
{ category:"U.S. History", term:"Watergate Scandal", definition:"A political scandal in the 1970s that led to the resignation of U.S. President Richard Nixon." },
{ category:"Ancient History", term:"Peloponnesian War", definition:"A long conflict between Athens and Sparta fought from 431 to 404 BCE in ancient Greece." },
{ category:"World Religions", term:"Buddhism", definition:"A major world religion founded by Siddhartha Gautama (the Buddha) that teaches the path to enlightenment through ethical living and meditation." },
// Fact statements (for True/False pool)
{ category:"African History (Fact)", term:"FACT: Johannesburg gold discovery", definition:"The discovery of gold in the Witwatersrand Basin in South Africa sparked the growth of Johannesburg.", fact:true },
{ category:"African History (Fact)", term:"FACT: Menelik II Ethiopia", definition:"The Ethiopian Empire was extended to its greatest extent under Menelik II.", fact:true },
{ category:"Asian History (Fact)", term:"FACT: Hammurabi code", definition:"While ruling Babylonia, Hammurabi established the earliest and most complete written legal code.", fact:true },
{ category:"Asian History (Fact)", term:"FACT: Indira Gandhi", definition:"Indira Gandhi was the first female prime minister of India.", fact:true },
{ category:"Asian History (Fact)", term:"FACT: Bongbong Marcos", definition:"The current president of the Philippines, Bongbong Marcos, is the son of the country’s former dictator, Ferdinand Marcos.", fact:true },
{ category:"Asian History (Fact)", term:"FACT: Silk Road", definition:"An ancient trade network between China and the West was defined by the trading of silk.", fact:true },
{ category:"Asian History (Fact)", term:"FACT: Baghdad Abbasids", definition:"The city of Baghdad served as the capital of the Abbasid Caliphate.", fact:true },
{ category:"Ancient History (Fact)", term:"FACT: Sparta training", definition:"Sparta was a powerful city-state in Greece where boys began military training at age seven.", fact:true },
{ category:"Ancient History (Fact)", term:"FACT: Alexander succeeds Philip", definition:"In 336 BCE, Alexander the Great succeeded his father, Philip II, as King of Macedon.", fact:true },
{ category:"European History (Fact)", term:"FACT: Constantine converts", definition:"Constantine the Great was the first Roman emperor to convert to Christianity.", fact:true },
{ category:"European History (Fact)", term:"FACT: Henry VIII break", definition:"Henry VIII broke from the Catholic Church to annul his marriage to Catherine of Aragon in order to marry Anne Boleyn.", fact:true },
{ category:"European History (Fact)", term:"FACT: Battle of the Bulge", definition:"During World War II, the final major German offensive in the West occurred at the Battle of the Bulge, fought mostly in Belgium and Luxembourg in December 1944.", fact:true },
{ category:"European History (Fact)", term:"FACT: Russo-Ukrainian War", definition:"The Russo-Ukrainian War has been the deadliest war in Europe since World War II.", fact:true },
{ category:"European History (Fact)", term:"FACT: Leonardo works", definition:"Leonardo da Vinci created notable Italian Renaissance artworks such as Mona Lisa and The Last Supper.", fact:true },
{ category:"European History (Fact)", term:"FACT: Churchill leads UK", definition:"During World War II, the United Kingdom was led by Prime Minister Winston Churchill.", fact:true },
{ category:"European History (Fact)", term:"FACT: Frederick the Great", definition:"Frederick the Great’s nearly 50-year reign in the 18th century led to the emergence of Prussia as a major European power.", fact:true },
{ category:"European History (Fact)", term:"FACT: Martin Luther theses", definition:"Martin Luther sparked the Protestant Reformation after criticizing the sale of indulgences in his Ninety-five Theses.", fact:true },
{ category:"Latin American History (Fact)", term:"FACT: Juan Perón", definition:"Juan Perón was the president of Argentina from 1946 until his overthrow in 1955 and again from 1973 until his death in 1974.", fact:true },
{ category:"Latin American History (Fact)", term:"FACT: Haiti revolt", definition:"Haiti was the site of a successful slave revolt against French colonial rule.", fact:true },
{ category:"U.S. History (Fact)", term:"FACT: Cuba & Spanish-American War", definition:"Cuba’s struggle for independence sparked the Spanish-American War.", fact:true },
{ category:"U.S. History (Fact)", term:"FACT: Jefferson authored DOI", definition:"The Declaration of Independence was principally authored by Thomas Jefferson.", fact:true },
{ category:"U.S. History (Fact)", term:"FACT: Obama slogans", definition:"Barack Obama ran with the primary slogans “Hope” and “Forward” during his two presidential campaigns.", fact:true },
{ category:"U.S. History (Fact)", term:"FACT: Hamilton musical", definition:"Lin-Manuel Miranda’s 2015 musical, Hamilton, covers the life of Alexander Hamilton during the American Revolution; Hamilton was also the first Secretary of the Treasury.", fact:true },
];
/*****************
* STORAGE + STATE
*****************/
const STORAGE_KEY = "history_study_deck_v1";
const PROGRESS_KEY = "history_study_known_v1";
let cards = [];
let knownSet = new Set();
// study filters
let studyCategory = "all";
let studyScope = "all"; // all/known/unknown
let filteredIndexes = [];
let currentPos = 0;
let showingBack = false;
// test state
let testQuestions = []; // {type, term, correct, choices[], selectedIndex, cardIndex}
let testGraded = false;
/************
* UTILITIES
************/
const $ = (id) => document.getElementById(id);
const uniq = (arr) => Array.from(new Set(arr));
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
function saveDeck(){
localStorage.setItem(STORAGE_KEY, JSON.stringify(cards));
}
function saveProgress(){
localStorage.setItem(PROGRESS_KEY, JSON.stringify(Array.from(knownSet)));
}
function loadAll(){
const stored = localStorage.getItem(STORAGE_KEY);
cards = stored ? JSON.parse(stored) : structuredClone(DEFAULT_CARDS);
const storedKnown = localStorage.getItem(PROGRESS_KEY);
knownSet = new Set(storedKnown ? JSON.parse(storedKnown) : []);
}
function cardKey(card){
// stable key: category + term (term should be unique in a typical study set)
return `${card.category}::${card.term}`;
}
function isKnown(card){
return knownSet.has(cardKey(card));
}
function setKnown(card, val){
const k = cardKey(card);
if(val) knownSet.add(k);
else knownSet.delete(k);
saveProgress();
}
function getCategories(){
return uniq(cards.map(c => c.category)).sort((a,b)=>a.localeCompare(b));
}
function computeFiltered(){
filteredIndexes = cards
.map((c, idx) => ({c, idx}))
.filter(({c}) => (studyCategory === "all" ? true : c.category === studyCategory))
.filter(({c}) => {
if(studyScope === "all") return true;
return studyScope === "known" ? isKnown(c) : !isKnown(c);
})
.map(x => x.idx);
if(filteredIndexes.length === 0){
currentPos = 0;
} else {
currentPos = clamp(currentPos, 0, filteredIndexes.length - 1);
}
}
function currentCard(){
if(filteredIndexes.length === 0) return null;
return cards[ filteredIndexes[currentPos] ];
}
function updateTopBadges(){
$("deckInfo").textContent = `Deck: ${cards.length} cards`;
const mode = document.querySelector(".tab.active")?.textContent?.trim() ?? "—";
$("modeInfo").textContent = `Mode: ${mode}`;
}
/************
* VIEW TABS
************/
const views = ["flashcards","test","manage"];
function setView(view){
for(const v of views){
$("view-" + v).classList.toggle("hide", v !== view);
}
document.querySelectorAll(".tab").forEach(btn => {
const isActive = btn.dataset.view === view;
btn.classList.toggle("active", isActive);
btn.setAttribute("aria-selected", isActive ? "true" : "false");
});
if(view === "flashcards"){
$("mainTitle").textContent = "Flashcards";
$("mainSub").textContent = "Flip, shuffle, and track what you know.";
renderFlashcard();
} else if(view === "test"){
$("mainTitle").textContent = "Practice Test";
$("mainSub").textContent = "Multiple choice and/or True/False from your deck.";
renderTestShell();
} else {
$("mainTitle").textContent = "Manage Cards";
$("mainSub").textContent = "Search, filter, edit, export, and import.";
renderManage();
}
updateTopBadges();
}
/****************
* FLASHCARDS UI
****************/
function renderFlashcard(){
computeFiltered();
const total = filteredIndexes.length;
const card = currentCard();
// reset display
showingBack = false;
$("fcBack").style.display = "none";
$("fcFront").style.display = "block";
$("totalCount").textContent = String(total);
if(!card){
$("fcFront").textContent = "No cards match your filters.";
$("fcBack").textContent = "";
$("fcBadge").textContent = `Card 0 / 0`;
$("knownToggle").checked = false;
$("categoryTag").textContent = "Category: —";
$("knownTag").textContent = "Status: —";
updateProgressBar();
return;
}
$("fcFront").textContent = card.term;
$("fcBack").textContent = card.definition;
$("fcBadge").textContent = `Card ${currentPos + 1} / ${total}`;
$("categoryTag").textContent = `Category: ${card.category}`;
const k = isKnown(card);
$("knownToggle").checked = k;
$("knownTag").textContent = `Status: ${k ? "Known" : "Unknown"}`;
$("knownTag").className = "tag " + (k ? "good" : "bad");
updateProgressBar();
}
function flipCard(forceShowBack = null){
const card = currentCard();
if(!card) return;
if(forceShowBack === null){
showingBack = !showingBack;
} else {
showingBack = !!forceShowBack;
}
$("fcBack").style.display = showingBack ? "block" : "none";
$("fcFront").style.display = showingBack ? "none" : "block";
}
function nextCard(){
if(filteredIndexes.length === 0) return;
currentPos = (currentPos + 1) % filteredIndexes.length;
renderFlashcard();
}
function prevCard(){
if(filteredIndexes.length === 0) return;
currentPos = (currentPos - 1 + filteredIndexes.length) % filteredIndexes.length;
renderFlashcard();
}
function shuffleFiltered(){
// Fisher-Yates shuffle on filteredIndexes
for(let i = filteredIndexes.length - 1; i > 0; i--){
const j = Math.floor(Math.random() * (i + 1));
[filteredIndexes[i], filteredIndexes[j]] = [filteredIndexes[j], filteredIndexes[i]];
}
currentPos = 0;
renderFlashcard();
}
function updateProgressBar(){
const total = filteredIndexes.length;
if(total === 0){
$("knownCount").textContent = "0";
$("progressBar").style.width = "0%";
return;
}
const known = filteredIndexes.reduce((acc, idx) => acc + (isKnown(cards[idx]) ? 1 : 0), 0);
$("knownCount").textContent = String(known);
$("progressBar").style.width = `${Math.round((known/total)*100)}%`;
}
/***************
* PRACTICE TEST
***************/
function renderTestShell(){
$("testArea").innerHTML = "";
$("testResult").style.display = "none";
testQuestions = [];
testGraded = false;
}
function getDeckForTest(){
// use same filters as flashcards (studyCategory + studyScope)
computeFiltered();
return filteredIndexes.map(i => ({ card: cards[i], idx: i }));
}
function pickRandom(arr, n){
const copy = arr.slice();
// shuffle
for(let i = copy.length - 1; i > 0; i--){
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy.slice(0, Math.min(n, copy.length));
}
function buildMCQQuestions(deckItems, count){
// Only non-fact cards or include all? We'll allow all cards, but MCQ works best with "term -> definition"
const pool = deckItems.filter(x => !!x.card.term && !!x.card.definition);
const picked = pickRandom(pool, count);
const allDefs = pool.map(x => x.card.definition);
return picked.map(({card, idx}, qIndex) => {
const correct = card.definition;
// create 3 distractors
const distractors = pickRandom(allDefs.filter(d => d !== correct), 3);
const choices = pickRandom([correct, ...distractors], 4);
return {
type: "mcq",
prompt: `What is the best definition of: "${card.term}"?`,
choices,
correctIndex: choices.indexOf(correct),
selectedIndex: null,
cardIndex: idx,
label: `Q${qIndex+1}`
};
});
}
function buildTFQuestions(deckItems, count){
// Prefer explicit fact cards, otherwise use definitions as "statements".
const factPool = deckItems.filter(x => x.card.fact === true);
const fallbackPool = deckItems;
const pool = (factPool.length >= 4) ? factPool : fallbackPool;
const picked = pickRandom(pool, count);
return picked.map(({card, idx}, qIndex) => {
// Make half false by swapping with another random statement (or negating)
const makeFalse = Math.random() < 0.5;
let statement = card.definition;
let answer = true;
if(makeFalse && pool.length > 1){
const other = pickRandom(pool.filter(x => x.idx !== idx), 1)[0];
// false statement: pair term context with mismatched statement
// If it's an explicit fact, we can just use another fact statement.
statement = other.card.definition;
answer = false;
}
return {
type: "tf",
prompt: statement,
choices: ["True", "False"],
correctIndex: answer ? 0 : 1,
selectedIndex: null,
cardIndex: idx,
label: `Q${qIndex+1}`
};
});
}
function startTest(){
const count = parseInt($("testCount").value, 10);
const kind = $("testType").value;
const deckItems = getDeckForTest();
$("testArea").innerHTML = "";
$("testResult").style.display = "none";
testGraded = false;
if(deckItems.length === 0){
$("testArea").innerHTML = `<div class="q"><div class="prompt">No cards match your filters. Change Study Settings.</div></div>`;
testQuestions = [];
return;
}
let qs = [];
if(kind === "mcq"){
qs = buildMCQQuestions(deckItems, count);
} else if(kind === "tf"){
qs = buildTFQuestions(deckItems, count);
} else {
// mixed
const half = Math.floor(count/2);
const mcq = buildMCQQuestions(deckItems, count - half);
const tf = buildTFQuestions(deckItems, half);
qs = pickRandom([...mcq, ...tf], count);
// relabel
qs.forEach((q,i)=> q.label = `Q${i+1}`);
}
testQuestions = qs;
renderTestQuestions();
}
function renderTestQuestions(){
const container = $("testArea");
container.innerHTML = "";
testQuestions.forEach((q, i) => {
const el = document.createElement("div");
el.className = "q";
el.dataset.qindex = String(i);
const meta = document.createElement("div");
meta.className = "meta";
meta.innerHTML = `<span>${q.label} • ${q.type.toUpperCase()}</span><span class="muted">Card #${q.cardIndex+1}</span>`;
const prompt = document.createElement("div");
prompt.className = "prompt";
prompt.textContent = q.prompt;
const choices = document.createElement("div");
choices.className = "choices";
q.choices.forEach((c, ci) => {
const ch = document.createElement("div");
ch.className = "choice";
ch.textContent = c;
ch.addEventListener("click", () => {
if(testGraded) return;
q.selectedIndex = ci;
// update classes
choices.querySelectorAll(".choice").forEach((node, idx) => {
node.classList.toggle("selected", idx === ci);
});
});
choices.appendChild(ch);
});
el.appendChild(meta);
el.appendChild(prompt);
el.appendChild(choices);
container.appendChild(el);
});
}
function gradeTest(){
if(testQuestions.length === 0) return;
testGraded = true;
let correct = 0;
testQuestions.forEach((q, i) => {
const qEl = document.querySelector(`.q[data-qindex="${i}"]`);
const nodes = Array.from(qEl.querySelectorAll(".choice"));
nodes.forEach((node, idx) => {
node.classList.remove("correct","wrong");
if(idx === q.correctIndex) node.classList.add("correct");
if(q.selectedIndex !== null && idx === q.selectedIndex && q.selectedIndex !== q.correctIndex){
node.classList.add("wrong");
}
});
if(q.selectedIndex === q.correctIndex) correct++;
});
const total = testQuestions.length;
const pct = Math.round((correct/total)*100);
$("scoreLine").textContent = `Score: ${correct} / ${total} (${pct}%)`;
$("scoreDetail").textContent = pct >= 80 ? "Nice work" : pct >= 60 ? "Keep going" : "More review needed";
$("testResult").style.display = "block";
$("testResult").scrollIntoView({behavior:"smooth", block:"start"});
}
/*****************
* MANAGE / SEARCH
*****************/
function populateCategorySelects(){
const cats = getCategories();
// sidebar selects
const sc = $("studyCategory");
const cf = $("categoryFilter");
// preserve current selections where possible
const prevSC = sc.value || "all";
const prevCF = cf.value || "all";
sc.innerHTML = `<option value="all">All</option>` + cats.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join("");
cf.innerHTML = `<option value="all">All</option>` + cats.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join("");
sc.value = cats.includes(prevSC) ? prevSC : "all";
cf.value = cats.includes(prevCF) ? prevCF : "all";
}
function renderManage(){
populateCategorySelects();
// KPI
$("kpiTotal").textContent = String(cards.length);
const knownCount = cards.reduce((a,c)=>a+(isKnown(c)?1:0),0);
$("kpiKnown").textContent = String(knownCount);
// list
const q = ($("searchInput").value || "").trim().toLowerCase();
const cat = $("categoryFilter").value;
const kf = $("knownFilter").value;
const items = cards
.map((c, idx) => ({c, idx}))
.filter(({c}) => cat === "all" ? true : c.category === cat)
.filter(({c}) => {
if(kf === "all") return true;
return kf === "known" ? isKnown(c) : !isKnown(c);
})
.filter(({c}) => {
if(!q) return true;
return (c.term + " " + c.definition).toLowerCase().includes(q);
});
const list = $("listArea");
list.innerHTML = "";
if(items.length === 0){
list.innerHTML = `<div class="q"><div class="prompt">No matches.</div></div>`;
return;
}
items.slice(0, 80).forEach(({c, idx}) => {
const box = document.createElement("div");
box.className = "q";
const meta = document.createElement("div");
meta.className = "meta";
meta.innerHTML = `<span>${escapeHtml(c.category)}</span><span class="muted">${isKnown(c) ? "Known" : "Unknown"}</span>`;
const prompt = document.createElement("div");
prompt.className = "prompt";
prompt.innerHTML = `<strong>${escapeHtml(c.term)}</strong><div class="muted" style="margin-top:6px;white-space:pre-wrap">${escapeHtml(c.definition)}</div>`;
const actions = document.createElement("div");
actions.className = "row";
actions.style.marginTop = "10px";
actions.innerHTML = `
<button class="btn secondary small">Study This</button>
<button class="btn secondary small">${isKnown(c) ? "Mark Unknown" : "Mark Known"}</button>
`;
actions.children[0].addEventListener("click", () => {
// set filters to this category and jump to this card in flashcards
studyCategory = c.category;
$("studyCategory").value = studyCategory;
studyScope = "all";
$("studyScope").value = studyScope;
computeFiltered();
const pos = filteredIndexes.indexOf(idx);
currentPos = pos >= 0 ? pos : 0;
setView("flashcards");
renderFlashcard();
});
actions.children[1].addEventListener("click", () => {
setKnown(c, !isKnown(c));
renderManage();
renderFlashcard();
});
box.appendChild(meta);
box.appendChild(prompt);
box.appendChild(actions);
list.appendChild(box);
});
if(items.length > 80){
const more = document.createElement("div");
more.className = "footer-note";
more.textContent = `Showing first 80 of ${items.length} matches. Use search/filter to narrow.`;
list.appendChild(more);
}
}
function saveCardFromForm(){
const cat = $("addCategory").value.trim();
const term = $("addTerm").value.trim();
const def = $("addDef").value.trim();
if(!cat || !term || !def){
alert("Please fill Category, Term, and Definition.");
return;
}
const existingIndex = cards.findIndex(c => c.term === term);
if(existingIndex >= 0){
// keep known key consistent (category might change)
const old = cards[existingIndex];
const wasKnown = isKnown(old);
cards[existingIndex] = { category: cat, term, definition: def };
// migrate known key if needed
if(wasKnown){
knownSet.delete(cardKey(old));
knownSet.add(cardKey(cards[existingIndex]));
saveProgress();
}
} else {
cards.push({ category: cat, term, definition: def });
}
saveDeck();
populateCategorySelects();
renderManage();
renderFlashcard();
clearForm();
}
function deleteCardByTerm(){
const term = $("addTerm").value.trim();
if(!term){
alert("Type the exact Term to delete.");
return;
}
const idx = cards.findIndex(c => c.term === term);
if(idx < 0){
alert("No card found with that exact Term.");
return;
}
const c = cards[idx];
knownSet.delete(cardKey(c));
cards.splice(idx, 1);
saveDeck(); saveProgress();
populateCategorySelects();
renderManage();
renderFlashcard();
clearForm();
}
function clearForm(){
$("addCategory").value = "";
$("addTerm").value = "";
$("addDef").value = "";
}
function exportJSON(){
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
cards,
knownKeys: Array.from(knownSet),
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "history-study-deck.json";
a.click();
URL.revokeObjectURL(url);
}
function importJSONFile(file){
const reader = new FileReader();
reader.onload = () => {
try{
const data = JSON.parse(reader.result);
if(!data || !Array.isArray(data.cards)) throw new Error("Invalid JSON format.");
cards = data.cards;
knownSet = new Set(Array.isArray(data.knownKeys) ? data.knownKeys : []);
saveDeck(); saveProgress();
populateCategorySelects();
renderManage();
renderFlashcard();
alert("Imported successfully!");
} catch(e){
alert("Import failed: " + e.message);
}
};
reader.readAsText(file);
}
function escapeHtml(str){
return String(str)
.replaceAll("&","&")
.replaceAll("<","<")
.replaceAll(">",">")
.replaceAll('"',""")
.replaceAll("'","'");
}
/************
* WIRING UP
************/
function bindEvents(){
// tabs
document.querySelectorAll(".tab").forEach(btn => {
btn.addEventListener("click", () => setView(btn.dataset.view));
});
// flashcards controls
$("flipBtn").addEventListener("click", () => flipCard());
$("prevBtn").addEventListener("click", prevCard);
$("nextBtn").addEventListener("click", nextCard);
$("shuffleBtn").addEventListener("click", shuffleFiltered);
$("resetProgressBtn").addEventListener("click", () => {
if(confirm("Reset known/unknown progress?")){
knownSet = new Set();
saveProgress();
renderFlashcard();
renderManage();
}
});
$("knownToggle").addEventListener("change", (e) => {
const c = currentCard();
if(!c) return;
setKnown(c, e.target.checked);
renderFlashcard();
renderManage();
});
$("fcStage").addEventListener("click", () => flipCard());
// sidebar settings
$("studyCategory").addEventListener("change", (e) => {
studyCategory = e.target.value;
currentPos = 0;
renderFlashcard();
});
$("studyScope").addEventListener("change", (e) => {
studyScope = e.target.value;
currentPos = 0;
renderFlashcard();
});
$("jumpRandomBtn").addEventListener("click", () => {
computeFiltered();
if(filteredIndexes.length === 0) return;
currentPos = Math.floor(Math.random() * filteredIndexes.length);
renderFlashcard();
});
$("showAnswerBtn").addEventListener("click", () => flipCard(true));
// test controls
$("startTestBtn").addEventListener("click", startTest);
$("newSetBtn").addEventListener("click", startTest);
$("gradeTestBtn").addEventListener("click", gradeTest);
// manage controls
$("searchInput").addEventListener("input", renderManage);
$("categoryFilter").addEventListener("change", renderManage);
$("knownFilter").addEventListener("change", renderManage);
$("saveCardBtn").addEventListener("click", saveCardFromForm);
$("clearFormBtn").addEventListener("click", clearForm);
$("deleteCardBtn").addEventListener("click", deleteCardByTerm);
$("exportBtn").addEventListener("click", exportJSON);
$("importBtn").addEventListener("click", () => $("importFile").click());
$("importFile").addEventListener("change", (e) => {
const f = e.target.files?.[0];
if(f) importJSONFile(f);
e.target.value = "";
});
// keyboard shortcuts
window.addEventListener("keydown", (e) => {
const activeView = document.querySelector(".tab.active")?.dataset.view;
if(activeView !== "flashcards") return;
if(e.code === "Space"){
e.preventDefault();
flipCard();
} else if(e.code === "ArrowRight"){
nextCard();
} else if(e.code === "ArrowLeft"){
prevCard();
} else if(e.key.toLowerCase() === "k"){
const c = currentCard();
if(!c) return;
const newVal = !isKnown(c);
setKnown(c, newVal);
$("knownToggle").checked = newVal;
renderFlashcard();
renderManage();
}
});
}
/************
* INIT
************/
(function init(){
loadAll();
populateCategorySelects();
studyCategory = "all";
studyScope = "all";
$("studyCategory").value = "all";
$("studyScope").value = "all";
updateTopBadges();
bindEvents();
setView("flashcards");
renderManage(); // keep KPI/list ready
})();
</script>
</body>
</html>
Editor is loading...
Leave a Comment