163 lines
5.6 KiB
HTML
163 lines
5.6 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Generate · Wordsmith{% endblock %}
|
||
{% block content %}
|
||
<section class="container">
|
||
<div class="page-head">
|
||
<div class="page-head__title">
|
||
<div class="eyebrow">№ 01 · Studio</div>
|
||
<h1>Generate a puzzle</h1>
|
||
</div>
|
||
<div class="page-head__meta">{{ themes|length }} theme{{ '' if themes|length == 1 else 's' }} on file</div>
|
||
</div>
|
||
<hr class="section-rule" />
|
||
|
||
{% if errors %}
|
||
<div class="notice">
|
||
<strong>Hold on</strong>
|
||
<ul>
|
||
{% for e in errors %}<li>{{ e }}</li>{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not themes %}
|
||
<div class="panel">
|
||
<p>No themes yet. <a href="/themes/new">Create one</a> to get started.</p>
|
||
</div>
|
||
{% else %}
|
||
|
||
<form class="studio" method="post" action="/generate">
|
||
<div class="form-grid">
|
||
<label class="field">
|
||
<span class="field__label">Theme</span>
|
||
<select name="theme" id="theme" required>
|
||
{% for t in themes %}
|
||
<option value="{{ t.slug }}" data-words="{{ t.words|join(',') }}">{{ t.name }} ({{ t.words|length }} words)</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
|
||
<div class="row">
|
||
<label class="field">
|
||
<span class="field__label">Grid</span>
|
||
<input type="number" name="grid_size" id="grid-size" value="12" min="5" max="25" required />
|
||
</label>
|
||
<label class="field">
|
||
<span class="field__label">Words</span>
|
||
<input type="number" name="word_count" id="word-count-input" value="10" min="1" max="30" required />
|
||
</label>
|
||
<label class="field">
|
||
<span class="field__label">Min len</span>
|
||
<input type="number" name="min_length" value="3" min="1" required />
|
||
</label>
|
||
<label class="field">
|
||
<span class="field__label">Max len</span>
|
||
<input type="number" name="max_length" value="12" min="1" required />
|
||
</label>
|
||
</div>
|
||
|
||
<label class="field">
|
||
<span class="field__label">Title override</span>
|
||
<input type="text" name="title" placeholder="leave blank to use theme name" />
|
||
</label>
|
||
|
||
<fieldset class="fieldset">
|
||
<legend>Directions</legend>
|
||
<div class="directions-grid">
|
||
<label class="check check--locked">
|
||
<input type="checkbox" checked disabled />
|
||
<span>→ Horizontal</span>
|
||
<span class="lock">REQ</span>
|
||
</label>
|
||
<label class="check check--locked">
|
||
<input type="checkbox" checked disabled />
|
||
<span>↓ Vertical</span>
|
||
<span class="lock">REQ</span>
|
||
</label>
|
||
<label class="check">
|
||
<input type="checkbox" name="diagonal" />
|
||
<span>↘ ↗ Diagonal</span>
|
||
</label>
|
||
<label class="check">
|
||
<input type="checkbox" name="reversed" />
|
||
<span>← ↑ ↖ ↙ Reversed</span>
|
||
</label>
|
||
</div>
|
||
<div class="fieldset__divider">
|
||
<label class="check">
|
||
<input type="checkbox" name="overlap" />
|
||
<span>Allow overlapping words</span>
|
||
</label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="button-row">
|
||
<button class="btn" type="submit">✦ Generate</button>
|
||
<button class="btn btn--ghost" type="reset">Reset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="preview-card" aria-label="Preview">
|
||
<div class="preview-card__head">
|
||
<span class="preview-card__title">Sample preview</span>
|
||
<span class="mono muted" id="preview-stats" style="font-size:11px;">12 × 12 · 10 words</span>
|
||
</div>
|
||
<div class="ws-grid" id="ws-grid" style="grid-template-columns: repeat(12, 1fr);"></div>
|
||
<div class="ws-words" id="ws-words"></div>
|
||
</aside>
|
||
</form>
|
||
{% endif %}
|
||
</section>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
const themeSel = document.getElementById('theme');
|
||
const gridInput = document.getElementById('grid-size');
|
||
const countInput = document.getElementById('word-count-input');
|
||
const grid = document.getElementById('ws-grid');
|
||
const wordsEl = document.getElementById('ws-words');
|
||
const stats = document.getElementById('preview-stats');
|
||
if (!themeSel || !grid) return;
|
||
|
||
function selectedWords() {
|
||
const opt = themeSel.options[themeSel.selectedIndex];
|
||
return (opt.dataset.words || '').split(',').filter(Boolean);
|
||
}
|
||
|
||
function renderPreview() {
|
||
const N = Math.max(5, Math.min(25, parseInt(gridInput.value || '12', 10) || 12));
|
||
const want = Math.max(1, parseInt(countInput.value || '10', 10) || 10);
|
||
grid.style.gridTemplateColumns = `repeat(${N}, 1fr)`;
|
||
|
||
const words = selectedWords();
|
||
const grids = words
|
||
.map(w => w.replace(/^(mr|mrs|ms|miss|dr|sir|dame|lord|lady|master|captain|capt|cpt|professor|prof|saint|st)\.?\s+/gi, '')
|
||
.replace(/[^a-z0-9]/gi, '').toUpperCase())
|
||
.filter(g => g.length > 0);
|
||
|
||
const pool = (grids.join('') || 'WORDSMITH').replace(/[^A-Z]/g, '') || 'WORDSMITH';
|
||
|
||
let html = '';
|
||
for (let i = 0; i < N * N; i++) {
|
||
html += `<span>${pool[(i * 7 + 3) % pool.length]}</span>`;
|
||
}
|
||
grid.innerHTML = html;
|
||
|
||
const sample = grids.slice(0, Math.min(want, grids.length));
|
||
wordsEl.innerHTML = sample.map((g, i) =>
|
||
`<b>${g}</b>${i < sample.length - 1 ? '<span class="sep">·</span>' : ''}`
|
||
).join('');
|
||
|
||
stats.textContent = `${N} × ${N} · ${sample.length} word${sample.length === 1 ? '' : 's'}`;
|
||
}
|
||
|
||
themeSel.addEventListener('change', renderPreview);
|
||
gridInput.addEventListener('input', renderPreview);
|
||
countInput.addEventListener('input', renderPreview);
|
||
renderPreview();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|