mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
Added verse-submission infrastructure
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
// Single source of truth for the admin account that may view submitter
|
||||
// attribution on /scheduled-verses. The public game never surfaces submitter
|
||||
// identity; this email is the sole exception (see spec "Admin /scheduled-verses").
|
||||
export const ADMIN_EMAIL = 'geohpowell@gmail.com';
|
||||
|
||||
export function isAdmin(email: string | null | undefined): boolean {
|
||||
return !!email && email.toLowerCase() === ADMIN_EMAIL;
|
||||
}
|
||||
@@ -48,3 +48,20 @@ export const dailyCompletions = sqliteTable('daily_completions', {
|
||||
]);
|
||||
|
||||
export type DailyCompletion = typeof dailyCompletions.$inferSelect;
|
||||
|
||||
export const verseSubmissions = sqliteTable('verse_submissions', {
|
||||
id: text('id').primaryKey(),
|
||||
// Nullable so ON DELETE SET NULL can clear attribution when a user is deleted;
|
||||
// the scheduled verse still plays out (canonical text lives on daily_verses).
|
||||
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
|
||||
scheduledDate: text('scheduled_date').notNull().unique(),
|
||||
selectedBookId: text('selected_book_id').notNull(),
|
||||
selectedChapter: integer('selected_chapter').notNull(),
|
||||
selectedVerse: integer('selected_verse').notNull(),
|
||||
// Raw integer of server UTC milliseconds — drives the 7-day rolling cooldown.
|
||||
submittedAt: integer('submitted_at').notNull(),
|
||||
}, (table) => [
|
||||
index('verse_submissions_user_id_idx').on(table.userId),
|
||||
]);
|
||||
|
||||
export type VerseSubmission = typeof verseSubmissions.$inferSelect;
|
||||
|
||||
+126
-2
@@ -135,7 +135,7 @@ function getGreekChapter(bookNumber: number, chapterNumber: number): ChapterData
|
||||
/**
|
||||
* Get the number of verses in a specific chapter
|
||||
*/
|
||||
function getVerseCount(bookNumber: number, chapterNumber: number): number {
|
||||
export function getVerseCount(bookNumber: number, chapterNumber: number): number {
|
||||
const chapter = getChapter(bookNumber, chapterNumber);
|
||||
return chapter ? chapter.verse.length : 0;
|
||||
}
|
||||
@@ -151,7 +151,7 @@ function getGreekVerseCount(bookNumber: number, chapterNumber: number): number {
|
||||
/**
|
||||
* Get the number of chapters in a specific book
|
||||
*/
|
||||
function getChapterCount(bookNumber: number): number {
|
||||
export function getChapterCount(bookNumber: number): number {
|
||||
const book = getBook(bookNumber);
|
||||
return book ? book.chapter.length : 0;
|
||||
}
|
||||
@@ -439,3 +439,127 @@ export function getAllNKJVVerses(): Array<{ text: string; book: string; chapter:
|
||||
}
|
||||
return verses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a 1-based book-wide verse index to { chapter, verse }.
|
||||
* The index is contiguous across all chapters of the book.
|
||||
*/
|
||||
function resolveBookVerseIndex(
|
||||
bookNumber: number,
|
||||
index: number
|
||||
): { chapter: number; verse: number } | null {
|
||||
const chapterCount = getChapterCount(bookNumber);
|
||||
let cumulative = 0;
|
||||
for (let c = 1; c <= chapterCount; c++) {
|
||||
const vCount = getVerseCount(bookNumber, c);
|
||||
if (index <= cumulative + vCount) {
|
||||
return { chapter: c, verse: index - cumulative };
|
||||
}
|
||||
cumulative += vCount;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface ComposedWindow {
|
||||
bookId: string;
|
||||
bookName: string;
|
||||
verses: string[]; // the 3 verse texts in order
|
||||
startChapter: number;
|
||||
startVerse: number;
|
||||
endChapter: number;
|
||||
endVerse: number;
|
||||
selectedVerse: number; // the anchor verse number within its chapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a 3-verse window anchored on (book, chapter, verse), constrained to
|
||||
* a single book. Default window is [v-2, v-1, v] in book-wide verse indices;
|
||||
* if that would run before the book's start (v is 1 or 2), fall forward to
|
||||
* [v, v+1, v+2]. Cross-chapter windows within the same book are allowed.
|
||||
*
|
||||
* Mirrors the spec's "Verse Windowing" section.
|
||||
*/
|
||||
export function composeVerseWindow(
|
||||
bookNumber: number,
|
||||
chapter: number,
|
||||
verse: number
|
||||
): ComposedWindow | null {
|
||||
const book = getBookByNumber(bookNumber);
|
||||
if (!book) return null;
|
||||
|
||||
const chapterCount = getChapterCount(bookNumber);
|
||||
if (chapter < 1 || chapter > chapterCount) return null;
|
||||
const verseCount = getVerseCount(bookNumber, chapter);
|
||||
if (verse < 1 || verse > verseCount) return null;
|
||||
|
||||
// Book-wide 1-based index of the selected verse.
|
||||
let selectedIndex = 0;
|
||||
for (let c = 1; c < chapter; c++) selectedIndex += getVerseCount(bookNumber, c);
|
||||
selectedIndex += verse;
|
||||
|
||||
let startIdx: number;
|
||||
let endIdx: number;
|
||||
if (selectedIndex - 2 < 1) {
|
||||
// Fall forward: [v, v+1, v+2]
|
||||
startIdx = selectedIndex;
|
||||
endIdx = selectedIndex + 2;
|
||||
} else {
|
||||
startIdx = selectedIndex - 2;
|
||||
endIdx = selectedIndex;
|
||||
}
|
||||
|
||||
const start = resolveBookVerseIndex(bookNumber, startIdx);
|
||||
const end = resolveBookVerseIndex(bookNumber, endIdx);
|
||||
if (!start || !end) return null;
|
||||
|
||||
// Extract the contiguous window, crossing chapter boundaries within the book as needed.
|
||||
const verses: string[] = [];
|
||||
let curChapter = start.chapter;
|
||||
let curVerse = start.verse;
|
||||
while (verses.length < 3 && curChapter <= chapterCount) {
|
||||
const vCount = getVerseCount(bookNumber, curChapter);
|
||||
while (curVerse <= vCount && verses.length < 3) {
|
||||
const ex = extractVerses(bookNumber, curChapter, curVerse, 1);
|
||||
if (ex.length === 1) {
|
||||
verses.push(ex[0]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
curVerse++;
|
||||
}
|
||||
curChapter++;
|
||||
curVerse = 1;
|
||||
}
|
||||
|
||||
if (verses.length < 3) return null;
|
||||
|
||||
return {
|
||||
bookId: book.id,
|
||||
bookName: book.name,
|
||||
verses,
|
||||
startChapter: start.chapter,
|
||||
startVerse: start.verse,
|
||||
endChapter: end.chapter,
|
||||
endVerse: end.verse,
|
||||
selectedVerse: verse
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a reference for a window that may span chapters within a book.
|
||||
* Same-chapter: "Genesis 1:1-3" (hyphen)
|
||||
* Cross-chapter: "Genesis 1:31–2:1" (en-dash)
|
||||
*/
|
||||
export function formatWindowReference(
|
||||
bookName: string,
|
||||
startChapter: number,
|
||||
startVerse: number,
|
||||
endChapter: number,
|
||||
endVerse: number
|
||||
): string {
|
||||
if (startChapter === endChapter) {
|
||||
if (startVerse === endVerse) return `${bookName} ${startChapter}:${startVerse}`;
|
||||
return `${bookName} ${startChapter}:${startVerse}-${endVerse}`;
|
||||
}
|
||||
return `${bookName} ${startChapter}:${startVerse}\u2013${endChapter}:${endVerse}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user