Add A to Z Challenge

This commit is contained in:
Evan Fiordeliso 2025-07-12 16:43:18 -04:00
parent ffb8cc8d9c
commit 891041c965
15 changed files with 297 additions and 18 deletions

View File

@ -1,7 +1,7 @@
{
"id": "obsidian-book-tracker",
"name": "Book Tracker",
"version": "1.1.0",
"version": "1.2.0",
"minAppVersion": "0.15.0",
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
"author": "FiFiTiDo",

View File

@ -1,6 +1,6 @@
{
"name": "obsidian-book-tracker",
"version": "1.1.0",
"version": "1.2.0",
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
"main": "main.js",
"scripts": {

View File

@ -25,6 +25,7 @@ import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodr
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
export default class BookTrackerPlugin extends Plugin {
public settings: BookTrackerPluginSettings;
@ -91,6 +92,7 @@ export default class BookTrackerPlugin extends Plugin {
registerReadingStatsCodeBlockProcessor(this);
registerShelfCodeBlockProcessor(this);
registerReadingCalendarCodeBlockProcessor(this);
registerAToZChallengeCodeBlockProcessor(this);
}
onunload() {}

View File

@ -0,0 +1,26 @@
import { registerCodeBlockRenderer } from ".";
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
import AToZChallengeCodeBlockView from "./AToZChallengeCodeBlockView.svelte";
import type BookTrackerPlugin from "@src/main";
import z from "zod/v4";
export function registerAToZChallengeCodeBlockProcessor(
plugin: BookTrackerPlugin
): void {
registerCodeBlockRenderer(
plugin,
"a-to-z-challenge",
(source, el) =>
new SvelteCodeBlockRenderer(
AToZChallengeCodeBlockView,
plugin,
source,
el
)
);
}
export const AToZChallengeSettingsSchema = z.object({
coverProperty: z.string(),
titleProperty: z.string(),
});

View File

@ -0,0 +1,194 @@
<script lang="ts">
import {
createSettings,
setSettingsContext,
} from "@ui/stores/settings.svelte";
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
import { createMetadata } from "@ui/stores/metadata.svelte";
import { STATUS_READ } from "@src/const";
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
import { AToZChallengeSettingsSchema } from "./AToZChallengeCodeBlock";
import { parseYaml, TFile } from "obsidian";
import OpenFileLink from "@ui/components/OpenFileLink.svelte";
import type { Moment } from "moment";
const { plugin, source }: SvelteCodeBlockProps = $props();
const settings = AToZChallengeSettingsSchema.parse(parseYaml(source));
const settingsStore = createSettings(plugin);
setSettingsContext(settingsStore);
const metadataStore = createMetadata(plugin, {
statusFilter: STATUS_READ,
initialYear: true,
disableMonthFilter: true,
});
const getSortValue = (value: string) => {
if (value.startsWith("A ")) {
return value.slice(2) + ", A";
} else if (value.startsWith("An ")) {
return value.slice(3) + ", An";
} else if (value.startsWith("The ")) {
return value.slice(4) + ", The";
} else {
return value;
}
};
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const items = $derived(
metadataStore.metadata.reduce(
(acc, item) => {
const title = item.frontmatter[settings.titleProperty];
const firstLetter = getSortValue(title).charAt(0).toUpperCase();
if (!firstLetter.match(/[A-Z]/)) {
return acc;
}
if (!acc[firstLetter]) {
const coverPath = item.frontmatter[
settings.coverProperty
] as string;
const coverFile = plugin.app.vault.getFileByPath(coverPath);
let coverSrc: string = "";
if (coverFile) {
coverSrc = plugin.app.vault.getResourcePath(coverFile);
}
const coverAlt = item.frontmatter[settings.titleProperty];
acc[firstLetter] = {
file: item.file,
// @ts-expect-error Moment is provided by Obsidian
startDate: moment(
item.frontmatter[
settingsStore.settings.startDateProperty
],
),
// @ts-expect-error Moment is provided by Obsidian
endDate: moment(
item.frontmatter[
settingsStore.settings.endDateProperty
],
),
coverSrc,
coverAlt,
};
}
return acc;
},
{} as Record<
string,
{
file: TFile;
startDate: Moment;
endDate: Moment;
coverSrc: string;
coverAlt: string;
}
>,
),
);
const startDate = $derived(
Object.values(items)
.map((item) => item.startDate)
.sort((a, b) => a.diff(b))[0],
);
const endDate = $derived.by(() => {
const dates = Object.values(items)
.map((item) => item.endDate)
.sort((a, b) => b.diff(a));
if (dates.length !== 26) {
return null;
}
return dates[0];
});
</script>
<div class="reading-bingo">
<div class="top-info">
<select class="year-filter" bind:value={metadataStore.filterYear}>
{#each metadataStore.filterYears as year}
<option value={year}>{year}</option>
{/each}
</select>
<p>Started: {startDate.format("YYYY-MM-DD")}</p>
<p>Ended: {endDate?.format("YYYY-MM-DD") ?? "N/A"}</p>
</div>
<div class="bingo">
{#each alphabet as letter}
<div class="bingo-item">
{#if items[letter]}
{@const item = items[letter]}
<OpenFileLink file={item.file}>
<img src={item.coverSrc} alt={item.coverAlt} />
</OpenFileLink>
{:else}
<div class="placeholder">{letter}</div>
{/if}
</div>
{/each}
</div>
</div>
<style lang="scss">
.reading-bingo {
display: flex;
flex-direction: column;
gap: var(--size-4-6);
.top-info {
display: flex;
align-items: center;
gap: var(--size-4-4);
p {
padding: 0;
margin: 0;
}
}
.bingo {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--size-4-4);
.bingo-item {
min-width: 150px;
max-width: 300px;
width: calc(20% - var(--size-4-4) * 2);
aspect-ratio: 2 / 3;
background-color: var(--background-secondary);
border-radius: var(--radius-l);
&:has(.placeholder) {
display: grid;
place-items: center;
}
.placeholder {
font-size: 2rem;
font-weight: bold;
font-style: italic;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--radius-l);
}
}
}
}
</style>

View File

@ -9,7 +9,7 @@ export function registerReadingCalendarCodeBlockProcessor(
): void {
registerCodeBlockRenderer(
plugin,
"readingcalendar",
"reading-calendar",
(source, el) =>
new SvelteCodeBlockRenderer(
ReadingCalendarCodeBlockView,

View File

@ -24,7 +24,9 @@
const settingsStore = createSettings(plugin);
setSettingsContext(settingsStore);
const metadataStore = createMetadata(plugin, null);
const metadataStore = createMetadata(plugin, {
statusFilter: null,
});
setMetadataContext(metadataStore);
const readingLog = createReadingLog(plugin.readingLog);

View File

@ -8,7 +8,7 @@ export function registerReadingLogCodeBlockProcessor(
): void {
registerCodeBlockRenderer(
plugin,
"readinglog",
"reading-log",
(source, el) =>
new SvelteCodeBlockRenderer(
ReadingLogCodeBlockView,

View File

@ -10,7 +10,7 @@ export function registerReadingStatsCodeBlockProcessor(
): void {
registerCodeBlockRenderer(
plugin,
"readingstats",
"reading-stats",
(source, el) =>
new SvelteCodeBlockRenderer(
ReadingStatsCodeBlockView,

View File

@ -26,7 +26,10 @@
const settingsStore = createSettings(plugin);
setSettingsContext(settingsStore);
const metadataStore = createMetadata(plugin, settings.statusFilter, true);
const metadataStore = createMetadata(plugin, {
statusFilter: settings.statusFilter,
initialMonth: true,
});
setMetadataContext(metadataStore);
let view = $state(settings.defaultView);

View File

@ -23,7 +23,9 @@
const settingsStore = createSettings(plugin);
setSettingsContext(settingsStore);
const metadataStore = createMetadata(plugin, null);
const metadataStore = createMetadata(plugin, {
statusFilter: null,
});
let editMode = $derived(entry !== undefined);
let book = $state(entry?.book ?? "");

View File

@ -9,17 +9,56 @@ export interface DateFilterStore {
get filterMonths(): { label: string; value: number }[];
}
export interface DateFilterStoreOptions {
/**
* If true, the filter month will be set to the current month
* If a number is provided, the filter month will be set to the provided month
*
* @default false
*/
initialMonth?: boolean | number;
/**
* If true, the filter year will be set to the current year
* If a number is provided, the filter year will be set to the provided year
*
* @default true
*/
initialYear?: boolean | number;
/**
* If true, the month filter will be disabled
*
* @default false
*/
disableMonthFilter?: boolean;
}
export function createDateFilter<T>(
data: () => T[],
selector: (item: T) => Moment,
initialMonth?: boolean
{
initialMonth = false,
initialYear = true,
disableMonthFilter = false,
}: DateFilterStoreOptions
): DateFilterStore & {
filteredData: T[];
} {
const today = new Date();
let filterYear: number | typeof ALL_TIME = $state(today.getFullYear());
let filterYear: number | typeof ALL_TIME = $state(
typeof initialYear === "number"
? initialYear
: initialYear
? today.getFullYear()
: ALL_TIME
);
let filterMonth: number | typeof ALL_TIME = $state(
initialMonth ? today.getMonth() + 1 : ALL_TIME
typeof initialMonth === "number"
? initialMonth
: initialMonth
? today.getMonth() + 1
: ALL_TIME
);
const filteredData = $derived.by(() => {
return data()
@ -30,7 +69,7 @@ export function createDateFilter<T>(
return false;
}
if (filterMonth !== ALL_TIME) {
if (filterMonth !== ALL_TIME && !disableMonthFilter) {
if (date.month() !== filterMonth - 1) {
return false;
}

View File

@ -7,7 +7,11 @@ import {
setSettingsContext,
} from "./settings.svelte";
import type BookTrackerPlugin from "@src/main";
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
import {
createDateFilter,
type DateFilterStore,
type DateFilterStoreOptions,
} from "./date-filter.svelte";
import type { ReadingState } from "@src/types";
import type { BookTrackerPluginSettings } from "@ui/settings";
@ -49,10 +53,16 @@ function getMetadata(
return metadata;
}
interface MetadataStoreOptions extends DateFilterStoreOptions {
/**
* The reading state to filter by
*/
statusFilter?: ReadingState | null;
}
export function createMetadata(
plugin: BookTrackerPlugin,
statusFilter: ReadingState | null = STATUS_READ,
initialMonth?: boolean
{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
): MetadataStore {
let settingsStore = getSettingsContext();
if (!settingsStore) {
@ -97,7 +107,7 @@ export function createMetadata(
f.frontmatter[settingsStore.settings.endDateProperty]
);
},
initialMonth
dateFilterOpts
);
return {

View File

@ -18,7 +18,7 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
const dateFilter = createDateFilter(
() => entries,
(entry) => entry.createdAt,
true
{ initialMonth: true }
);
async function addEntry(entry: ReadingLogEntry): Promise<void> {

View File

@ -1,4 +1,5 @@
{
"1.0.0": "0.15.0",
"1.1.0": "0.15.0"
"1.1.0": "0.15.0",
"1.2.0": "0.15.0"
}