Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions FAQPage/devwez/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# FAQ Page — devwez

Minimal, fast FAQ page for the [javascript-mini-projects](https://github.com/thinkswell/javascript-mini-projects) collection.

**Fixes:** Closes #1030 — adds starter FAQ page.

### Stack
- Vanilla HTML / CSS / JS (no frameworks)
- Inter + JetBrains Mono
- CSS grid animation for accordion (no height hacks)

### Features
- 6 curated FAQs with tags
- Live search (debounced) — filters by question, answer, tag
- Keyboard shortcut: `/` to focus search, `Esc` to clear
- Expand / collapse all
- Responsive, dark mesh-gradient theme

### Run
Open `index.html` in a browser. No build step.

### Structure
```
FAQPage/devwez/
index.html
style.css
script.js
README.md
```
42 changes: 42 additions & 0 deletions FAQPage/devwez/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FAQ — Quick Answers</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg"></div>
<div class="wrap">
<header class="hero">
<p class="eyebrow">FAQ &middot; Help Center</p>
<h1>Quick answers,<br><span>no fluff.</span></h1>
<p class="sub">Search or browse. Everything we get asked, distilled to the essentials.</p>
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="search" type="text" placeholder="Search — try 'shipping', 'refund', 'account'..." autocomplete="off" />
<span class="kbd">/</span>
</div>
</header>

<div class="meta">
<span id="count">6 questions</span>
<span class="dot"></span>
<span>Updated today</span>
<button id="expandAll">Expand all</button>
</div>

<div class="faq-list" id="faqList">
<!-- items injected by JS -->
</div>

<footer>
<p>Didn't find it? <a href="mailto:help@example.com">Contact support</a> — we reply in &lt; 24h.</p>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>
137 changes: 137 additions & 0 deletions FAQPage/devwez/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const faqs = [
{
q: "How long does shipping take?",
sub: "Delivery timelines & tracking",
tag: "Shipping",
a: `Domestic orders ship in 2–3 business days and arrive in 3–5 days. International is 7–14 days. You'll get a tracking link by email once it leaves the warehouse. If it's been 10 days with no update, ping support and we'll sort it.`
},
{
q: "What’s your refund policy?",
sub: "Returns, exchanges & refunds",
tag: "Billing",
a: `30-day money-back guarantee on unused items. Start a return from your <a href="#">order page</a> — we cover return shipping for defects, otherwise it's on you. Refunds hit your original payment method within 5–7 business days after we receive it.`
},
{
q: "How do I reset my password?",
sub: "Account access in 30 seconds",
tag: "Account",
a: `Go to login → “Forgot password” → enter your email. Check spam if you don't see it in 2 minutes. Link expires in 1 hour. Still stuck? Try an incognito window — extensions sometimes block the reset.`
},
{
q: "Can I change my plan later?",
sub: "Upgrade, downgrade, cancel anytime",
tag: "Billing",
a: `Yep. Upgrade is instant and prorated. Downgrade takes effect next cycle. Cancel anytime from Settings → Billing — no dark patterns, no “call us” hoop. Your data stays for 30 days in case you come back.`
},
{
q: "Is my data secure?",
sub: "Privacy, encryption & compliance",
tag: "Security",
a: `We use AES-256 at rest, TLS 1.3 in transit, and isolated VPCs. SOC 2 Type II audited this year. We never sell data. Export or delete everything from Settings → Privacy — it's your data.`
},
{
q: "Do you offer student discounts?",
sub: "Pricing for learners",
tag: "Pricing",
a: `Students and educators get 40% off with a .edu email or valid ID. Apply at checkout — discount auto-renews each year while you're eligible. Need an invoice for reimbursement? We generate one instantly.`
},
];

const list = document.getElementById('faqList');
const search = document.getElementById('search');
const count = document.getElementById('count');
const expandBtn = document.getElementById('expandAll');

let expandedAll = false;

function render(filter = "") {
const q = filter.toLowerCase().trim();
const filtered = faqs.filter(f =>
!q || f.q.toLowerCase().includes(q) || f.a.toLowerCase().includes(q) || f.tag.toLowerCase().includes(q) || f.sub.toLowerCase().includes(q)
);

count.textContent = `${filtered.length} question${filtered.length !== 1 ? 's' : ''}${q ? ` for “${filter}”` : ''}`;

if (filtered.length === 0) {
list.innerHTML = `<div class="no-result">No results for “${filter}”. Try “shipping” or “refund”.</div>`;
return;
}

list.innerHTML = filtered.map((f, i) => {
const idx = faqs.indexOf(f);
return `
<div class="item" data-idx="${idx}">
<button class="q" aria-expanded="false">
<div class="q-left">
<h3>${f.q}</h3>
<p>${f.sub}</p>
</div>
<span class="icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
</span>
</button>
<div class="a">
<div class="a-inner"><div><span class="tag">${f.tag}</span><br>${f.a}</div></div>
</div>
</div>`;
}).join("");

// wire up toggles - a bit verbose but keeps it readable
list.querySelectorAll('.item').forEach(el => {
const btn = el.querySelector('.q');
btn.addEventListener('click', () => {
const isOpen = el.classList.contains('open');
// close others if you want single-open? we allow multiple
el.classList.toggle('open', !isOpen);
btn.setAttribute('aria-expanded', !isOpen);
});
});

// if expandAll was on, reopen
if (expandedAll) {
list.querySelectorAll('.item').forEach(el => {
el.classList.add('open');
el.querySelector('.q').setAttribute('aria-expanded', 'true');
});
}
}

render();

// search with debounce - tiny helper
let t;
search.addEventListener('input', e => {
clearTimeout(t);
t = setTimeout(() => render(e.target.value), 120);
});

// keyboard shortcut: press "/" to focus
document.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement !== search) {
e.preventDefault();
search.focus();
}
if (e.key === 'Escape') {
search.value = '';
render('');
search.blur();
}
});

expandBtn.addEventListener('click', () => {
expandedAll = !expandedAll;
list.querySelectorAll('.item').forEach(el => {
el.classList.toggle('open', expandedAll);
el.querySelector('.q').setAttribute('aria-expanded', expandedAll);
});
expandBtn.textContent = expandedAll ? 'Collapse all' : 'Expand all';
});

// open first by default - feels friendlier
setTimeout(() => {
const first = list.querySelector('.item');
if (first) {
first.classList.add('open');
first.querySelector('.q').setAttribute('aria-expanded', 'true');
}
}, 80);
155 changes: 155 additions & 0 deletions FAQPage/devwez/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0c0c0f;
--card:#13131a;
--card-2:#1a1a23;
--border:rgba(255,255,255,.08);
--text:#ececf1;
--muted:#9a9aa8;
--accent:#7c5cff;
--accent-2:#22d3ee;
}
html{scroll-behavior:smooth}
body{
font-family:'Inter',system-ui,-apple-system,sans-serif;
background:var(--bg);
color:var(--text);
min-height:100vh;
line-height:1.6;
overflow-x:hidden;
}
.bg{
position:fixed;inset:0;z-index:-1;
background:
radial-gradient(800px 500px at 15% -10%, rgba(124,92,255,.22), transparent 60%),
radial-gradient(700px 400px at 90% 10%, rgba(34,211,238,.14), transparent 60%),
radial-gradient(600px 400px at 50% 100%, rgba(124,92,255,.10), transparent 60%),
var(--bg);
}
.wrap{max-width:720px;margin:0 auto;padding:48px 20px 60px}
.hero{margin-bottom:32px}
.eyebrow{
font-family:'JetBrains Mono',monospace;
font-size:11px;letter-spacing:.14em;text-transform:uppercase;
color:var(--muted);margin-bottom:16px
}
.hero h1{
font-size:clamp(32px,5vw,44px);
font-weight:700;letter-spacing:-.03em;line-height:1.05;margin-bottom:12px
}
.hero h1 span{
font-weight:700;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.sub{color:var(--muted);font-size:15px;max-width:520px}
.search-box{
margin-top:22px;
display:flex;align-items:center;gap:10px;
background:var(--card);
border:1px solid var(--border);
border-radius:12px;
padding:12px 14px;
transition:border-color .2s, box-shadow .2s;
}
.search-box:focus-within{
border-color:rgba(124,92,255,.45);
box-shadow:0 0 0 4px rgba(124,92,255,.12);
}
.search-box svg{color:var(--muted);flex-shrink:0}
.search-box input{
flex:1;background:transparent;border:none;outline:none;
color:var(--text);font-size:14px;font-family:inherit;
}
.search-box input::placeholder{color:#6b6b7a}
.kbd{
font-family:'JetBrains Mono',monospace;
font-size:11px;background:var(--card-2);border:1px solid var(--border);
border-bottom-width:2px;
padding:3px 7px;border-radius:6px;color:var(--muted)
}
.meta{
display:flex;align-items:center;gap:10px;
font-size:12.5px;color:var(--muted);
margin-bottom:14px;
font-family:'JetBrains Mono',monospace;
}
.meta .dot{width:3px;height:3px;background:var(--muted);border-radius:50%}
#expandAll{
margin-left:auto;
background:transparent;border:1px solid var(--border);
color:var(--muted);padding:6px 12px;border-radius:8px;
font-size:12.5px;font-family:inherit;cursor:pointer;
transition:all .15s;
}
#expandAll:hover{border-color:rgba(255,255,255,.16);color:var(--text)}
.faq-list{display:flex;flex-direction:column;gap:10px}
.item{
background:var(--card);
border:1px solid var(--border);
border-radius:14px;
overflow:hidden;
transition:border-color .2s, transform .2s;
}
.item:hover{border-color:rgba(255,255,255,.12)}
.item.open{border-color:rgba(124,92,255,.28);transform:translateY(-1px)}
.q{
width:100%;display:flex;align-items:center;gap:14px;
background:transparent;border:none;text-align:left;
padding:16px 18px;cursor:pointer;
color:var(--text);font-family:inherit;
}
.q-left{flex:1}
.q h3{font-size:14.5px;font-weight:600;letter-spacing:-.01em;line-height:1.4}
.q p{font-size:12.5px;color:var(--muted);margin-top:4px}
.icon{
width:28px;height:28px;border-radius:8px;
background:var(--card-2);border:1px solid var(--border);
display:grid;place-items:center;flex-shrink:0;
transition:transform .25s, background .2s, border-color .2s;
}
.icon svg{transition:transform .25s}
.item.open .icon{
background:var(--accent);border-color:var(--accent);
color:white;transform:rotate(0deg)
}
.item.open .icon svg{transform:rotate(45deg)}
.a{
display:grid;
grid-template-rows:0fr;
transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1);
}
.item.open .a{grid-template-rows:1fr}
.a-inner{
overflow:hidden;
}
.a-inner > div{
padding:0 18px 16px 18px;
color:#c9c9d6;font-size:13.8px;line-height:1.65;
border-top:1px solid var(--border);
padding-top:14px;
margin:0 18px;
padding-left:0;padding-right:0;
}
.a-inner a{color:var(--accent-2);text-decoration:none;border-bottom:1px solid rgba(34,211,238,.25)}
.a-inner a:hover{border-bottom-color:currentColor}
.tag{
display:inline-block;font-size:10px;letter-spacing:.08em;text-transform:uppercase;
background:rgba(124,92,255,.14);border:1px solid rgba(124,92,255,.22);
color:#b8a6ff;padding:3px 7px;border-radius:6px;margin-bottom:8px;
font-family:'JetBrains Mono',monospace;
}
.no-result{
text-align:center;padding:36px 20px;
color:var(--muted);font-size:14px;
background:var(--card);border:1px dashed var(--border);border-radius:14px;
}
footer{
margin-top:28px;text-align:center;
color:var(--muted);font-size:13px;
}
footer a{color:var(--text);text-decoration:underline;text-underline-offset:3px}
@media(max-width:560px){
.wrap{padding:32px 16px}
.search-box .kbd{display:none}
}