Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
- Admin: sign-up Open / Invite only / Closed with single-use invite links - Present mode for scripts (auto-scroll) and outlines (step through, pace vs planned minutes) - Series: group studies on the dashboard, filter by series, calendar file for a study's sessions - Lesson handout: printable page per session (passage, outline, questions, writing lines) - Save a chunk's notes as your own study template (synced) - Voice dictation for notes and scripts (browser speech recognition) - Reading progress: map of the 66 books and a calendar of days read, synced and merged across devices - Export highlights and bookmarks as Markdown or Word, grouped by named colour - Verse of the day push to the ntfy topic (needs IQBIBLE_API_KEY) - Sharing: co-teacher editing via single-use invite links; read-only links can expire and take a passcode Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88bc59bac0
commit
54cb1c217d
73 files changed
+2739
-134
No files matched your search
@@ -0,0 +1,45 @@
|
||||
// The rules for a read-only link: when it stops working, and an optional passcode. Shown under the link.
|
||||
import { useState } from 'react';
|
||||
|
||||
const OPTIONS = [['', 'Never'], ['1', 'In 1 day'], ['7', 'In 7 days'], ['30', 'In 30 days']];
|
||||
const field = 'rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
|
||||
export const expiryText = (expiresAt, now = Date.now()) => {
|
||||
if (!expiresAt) return 'Never expires';
|
||||
if (expiresAt <= now) return 'Expired';
|
||||
const days = Math.ceil((expiresAt - now) / 86_400_000);
|
||||
return `Expires ${new Date(expiresAt).toLocaleDateString()} (${days} day${days === 1 ? '' : 's'} left)`;
|
||||
};
|
||||
|
||||
export default function LinkRules({ expiresAt, hasPasscode, busy, onSave }) {
|
||||
const [days, setDays] = useState('keep'); // 'keep' | '' | '1' | '7' | '30'
|
||||
const [passcode, setPasscode] = useState('');
|
||||
const dirty = days !== 'keep' || passcode.trim() !== '';
|
||||
|
||||
const save = async () => {
|
||||
const options = {};
|
||||
if (days !== 'keep') options.expiresInDays = days === '' ? null : Number(days);
|
||||
if (passcode.trim()) options.passcode = passcode.trim();
|
||||
if (await onSave(options)) { setDays('keep'); setPasscode(''); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-[var(--r-line)] p-3" role="group" aria-label="Link settings">
|
||||
<p className="text-xs text-[var(--r-sub)]">{expiryText(expiresAt)} · {hasPasscode ? 'Needs a passcode' : 'No passcode'}</p>
|
||||
<div className="mt-2 flex flex-wrap items-end gap-3">
|
||||
<label className="text-xs text-[var(--r-sub)]">Stop working
|
||||
<select value={days} onChange={(e) => setDays(e.target.value)} className={`${field} mt-1 block`}>
|
||||
<option value="keep">{expiresAt ? 'Keep as is' : 'Never (as is)'}</option>
|
||||
{OPTIONS.map(([v, label]) => <option key={v || 'never'} value={v}>{label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-[var(--r-sub)]">{hasPasscode ? 'New passcode' : 'Passcode (optional)'}
|
||||
<input type="text" value={passcode} onChange={(e) => setPasscode(e.target.value)} maxLength={100} autoComplete="off" placeholder={hasPasscode ? 'Type to change' : 'None'} className={`${field} mt-1 block w-44`} />
|
||||
</label>
|
||||
<button type="button" onClick={save} disabled={!dirty || busy} className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-sm hover:bg-black/5 disabled:opacity-50">Save settings</button>
|
||||
{hasPasscode && <button type="button" onClick={() => onSave({ passcode: null })} disabled={busy} className="text-xs text-[var(--r-danger,#b42318)] hover:underline">Remove passcode</button>}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-[var(--r-sub)]">Send the passcode separately from the link. Anyone with both can read it until it expires or you turn it off.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user