mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
creating web components for habit counter
This commit is contained in:
parent
90f087ffe3
commit
944afbd61b
25 changed files with 797 additions and 180 deletions
41
main.ts
41
main.ts
|
|
@ -1,21 +1,21 @@
|
|||
import {
|
||||
App,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
FileSystemAdapter,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
MarkdownPostProcessorContext
|
||||
} from "obsidian";
|
||||
|
||||
import { DBService } from "./src/dbService";
|
||||
import { DBService } from "./src/DBService";
|
||||
import { SQLiteDBSettingTab } from "./src/settingTab";
|
||||
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
|
||||
import { processSqlBlock, processSqlChartBlock, renderDatePicker } from "./src/codeblocks";
|
||||
import { pickTableName } from "./src/helpers";
|
||||
import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types";
|
||||
|
||||
import { injectDatePickerStyles } from "src/styles/datePickerInject";
|
||||
import "./src/webcomponents/habitCounter";
|
||||
|
||||
import { registerHabitCounter } from "./src/webcomponents/HabitCounter/registerHabitCounter";
|
||||
|
||||
export default class SQLiteDBPlugin extends Plugin {
|
||||
settings: SQLiteDBSettings;
|
||||
|
|
@ -29,11 +29,14 @@ export default class SQLiteDBPlugin extends Plugin {
|
|||
|
||||
injectDatePickerStyles();
|
||||
|
||||
this.registerMarkdownPostProcessor((el, ctx) => {
|
||||
registerHabitCounter(el, this.dbService);
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "inspect-table-structure",
|
||||
name: "Inspect table structure",
|
||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||
// ensure DB is loaded (if not, load)
|
||||
await this.openDatabase();
|
||||
await inspectTableStructure(this.dbService, editor, this.app);
|
||||
},
|
||||
|
|
@ -106,29 +109,3 @@ export default class SQLiteDBPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
class SQLiteDBSettingTab extends PluginSettingTab {
|
||||
plugin: SQLiteDBPlugin;
|
||||
|
||||
constructor(app: App, plugin: SQLiteDBPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Database file path")
|
||||
.setDesc("Absolute path to the .db file on disk.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("/home/user/path/to/your.db")
|
||||
.setValue(this.plugin.settings.dbFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dbFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
src/DBService.ts
Normal file
184
src/DBService.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { Notice, App } from "obsidian";
|
||||
import initSqlJs, { Database, SqlJsStatic, Statement } from "sql.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { SQLiteDBSettings } from "./types";
|
||||
|
||||
export class DBService {
|
||||
private db: Database | null = null;
|
||||
private SQL: SqlJsStatic | null = null; // We still need to store the initialized library instance
|
||||
private app: App;
|
||||
private settings: SQLiteDBSettings | null = null; // Store settings for saving path
|
||||
private basePath: string = "";
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the DB is loaded. If it's not loaded yet, load it from disk.
|
||||
* If it is already loaded, do nothing.
|
||||
*
|
||||
* If `forceReload` is true, always reload from disk.
|
||||
*/
|
||||
async ensureDBLoaded(settings: SQLiteDBSettings, basePath: string, forceReload = false): Promise<boolean> { // Added return type promise
|
||||
this.settings = settings;
|
||||
this.basePath = basePath;
|
||||
|
||||
if (!settings.dbFilePath) {
|
||||
new Notice("No DB path set in plugin settings.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only reload if forced or not already loaded
|
||||
if (!this.db || forceReload) {
|
||||
try {
|
||||
console.log("DBService: Initializing SQL.js and loading DB...");
|
||||
this.SQL = await initSqlJs({
|
||||
locateFile: (file) => `${basePath}/${this.app.vault.configDir}/plugins/sqlite-db/${file}`,
|
||||
});
|
||||
|
||||
console.log(`DBService: Reading DB file from: ${settings.dbFilePath}`);
|
||||
const fileBuffer = readFileSync(settings.dbFilePath);
|
||||
this.db = new this.SQL.Database(fileBuffer);
|
||||
console.log("DBService: Database loaded into memory.");
|
||||
|
||||
// Check if we have at least one table
|
||||
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
||||
if (result?.[0]?.values?.[0]?.[0]) {
|
||||
new Notice(`DB Loaded.`);
|
||||
console.log("DBService: DB Loaded successfully.");
|
||||
} else {
|
||||
new Notice("DB Loaded, but found no tables.");
|
||||
console.log("DBService: DB Loaded, but found no tables.");
|
||||
}
|
||||
return true; // Indicate success
|
||||
} catch (err) {
|
||||
this.db = null; // Ensure db is null on error
|
||||
this.SQL = null;
|
||||
console.error("DBService: Error initializing/reading DB:", err);
|
||||
new Notice("Error reading DB: " + (err as Error).message);
|
||||
return false; // Indicate failure
|
||||
}
|
||||
} else {
|
||||
console.log("DBService: Database already loaded.");
|
||||
return true; // Already loaded
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Database instance, or null if not loaded.
|
||||
*/
|
||||
getDB(): Database | null {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query that is expected to return rows (e.g., SELECT).
|
||||
* Uses prepared statements for security.
|
||||
* @param sql SQL query string with placeholders (?)
|
||||
* @param params Array of parameters to bind to placeholders
|
||||
* @returns Promise resolving to an array of result objects
|
||||
*/
|
||||
async getQuery<T extends Record<string, any>>(sql: string, params: any[] = []): Promise<T[]> {
|
||||
if (!this.db) {
|
||||
console.error("DBService.getQuery: Database is not loaded.");
|
||||
throw new Error("Database not loaded.");
|
||||
}
|
||||
|
||||
let stmt: Statement | null = null;
|
||||
try {
|
||||
stmt = this.db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
const results: T[] = [];
|
||||
while (stmt.step()) {
|
||||
results.push(stmt.getAsObject() as T);
|
||||
}
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(`DBService.getQuery: Error executing SQL: ${sql}`, params, error);
|
||||
throw error; // Re-throw
|
||||
} finally {
|
||||
if (stmt) {
|
||||
try { stmt.free(); } catch (freeError) { console.error("DBService.getQuery: Error freeing statement:", freeError); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query that does not return rows (e.g., INSERT, UPDATE, DELETE).
|
||||
* Uses prepared statements for security.
|
||||
* Saves the database to disk after successful execution.
|
||||
* @param sql SQL query string with placeholders (?)
|
||||
* @param params Array of parameters to bind to placeholders
|
||||
* @returns Promise resolving when execution and save are complete
|
||||
*/
|
||||
async runQuery(sql: string, params: any[] = []): Promise<void> {
|
||||
if (!this.db) {
|
||||
console.error("DBService.runQuery: Database is not loaded.");
|
||||
throw new Error("Database not loaded.");
|
||||
}
|
||||
|
||||
let stmt: Statement | null = null;
|
||||
try {
|
||||
stmt = this.db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
stmt.run();
|
||||
|
||||
await this._saveDBInternal();
|
||||
|
||||
} catch (error) {
|
||||
console.error(`DBService.runQuery: Error executing SQL: ${sql}`, params, error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (stmt) {
|
||||
try { stmt.free(); } catch (freeError) { console.error("DBService.runQuery: Error freeing statement:", freeError); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to save the current in-memory database back to the file.
|
||||
*/
|
||||
private async _saveDBInternal(): Promise<void> {
|
||||
if (!this.db) {
|
||||
console.error("DBService._saveDBInternal: Cannot save, database not loaded.");
|
||||
return; // Don't throw, just log and return if DB isn't loaded
|
||||
}
|
||||
if (!this.settings || !this.settings.dbFilePath) {
|
||||
console.error("DBService._saveDBInternal: Cannot save, settings or DB path missing.");
|
||||
new Notice("Cannot save DB: Path missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = this.db.export();
|
||||
await writeFile(this.settings.dbFilePath, data);
|
||||
} catch (error) {
|
||||
console.error(`DBService._saveDBInternal: Error saving database to ${this.settings.dbFilePath}:`, error);
|
||||
new Notice("Error saving database changes!");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly closes the database connection. Important for unloading.
|
||||
*/
|
||||
closeDB() {
|
||||
if (this.db) {
|
||||
try {
|
||||
console.log("DBService: Closing database connection.");
|
||||
this.db.close();
|
||||
} catch(closeError) {
|
||||
console.error("DBService: Error closing database:", closeError);
|
||||
} finally {
|
||||
this.db = null;
|
||||
this.SQL = null;
|
||||
this.settings = null;
|
||||
this.basePath = "";
|
||||
}
|
||||
} else {
|
||||
this.SQL = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { DBService } from "../../dbService";
|
||||
import { DBService } from "../../DBService";
|
||||
import { parseSqlParams, validateTable, buildSqlQuery, renderResults } from "./helpers";
|
||||
import { pluginState } from "../../pluginState";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { DBService } from "../../dbService";
|
||||
import { DBService } from "../../DBService";
|
||||
import { parseChartParams, validateColumns, buildSqlQuery, processChartData } from "./helpers";
|
||||
import { ChartType, createChart } from "./charts";
|
||||
import { pluginState } from "../../pluginState";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Notice, TFile, TFolder, normalizePath, FileSystemAdapter } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { DBService } from "../DBService";
|
||||
import { ColumnPickerModal } from "../components/ColumnPickerModal";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Notice, Editor } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { DBService } from "../DBService";
|
||||
import { TablePickerModal } from "../components/TablePickerModal";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
import { Notice, App } from "obsidian";
|
||||
import initSqlJs, { Database } from "sql.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { SQLiteDBSettings } from "./types";
|
||||
|
||||
/**
|
||||
* This class manages loading/reloading the SQLite DB using sql.js
|
||||
*/
|
||||
export class DBService {
|
||||
private db: Database | null = null;
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the DB is loaded. If it's not loaded yet, load it from disk.
|
||||
* If it is already loaded, do nothing.
|
||||
*
|
||||
* If `forceReload` is true, always reload from disk.
|
||||
*/
|
||||
async ensureDBLoaded(settings: SQLiteDBSettings, basePath: string, forceReload = false) {
|
||||
if (!settings.dbFilePath) {
|
||||
new Notice("No DB path set in plugin settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.db || forceReload) {
|
||||
try {
|
||||
const SQL = await initSqlJs({
|
||||
locateFile: (file) => `${basePath}/${this.app.vault.configDir}/plugins/sqlite-db/${file}`,
|
||||
});
|
||||
const fileBuffer = readFileSync(settings.dbFilePath);
|
||||
const uint8Array = new Uint8Array(fileBuffer);
|
||||
this.db = new SQL.Database(uint8Array);
|
||||
|
||||
// Check if we have at least one table
|
||||
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
||||
if (result?.[0]?.values?.[0]?.[0]) {
|
||||
new Notice(`DB Loaded.`);
|
||||
} else {
|
||||
new Notice("DB Loaded, but found no tables.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice("Error reading DB: " + (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Database instance, or null if not loaded.
|
||||
*/
|
||||
getDB(): Database | null {
|
||||
return this.db;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Notice } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { DBService } from "../DBService";
|
||||
import { TablePickerModal } from "../components/TablePickerModal";
|
||||
|
||||
/**
|
||||
|
|
|
|||
29
src/settingTab.ts
Normal file
29
src/settingTab.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import SQLiteDBPlugin from "main";
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
|
||||
export class SQLiteDBSettingTab extends PluginSettingTab {
|
||||
plugin: SQLiteDBPlugin;
|
||||
|
||||
constructor(app: App, plugin: SQLiteDBPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Database file path")
|
||||
.setDesc("Absolute path to the .db file on disk.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("/home/user/path/to/your.db")
|
||||
.setValue(this.plugin.settings.dbFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dbFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
121
src/webcomponents/HabitCounter/HabitCounter.ts
Normal file
121
src/webcomponents/HabitCounter/HabitCounter.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { DBService } from "../../DBService";
|
||||
import { HabitDataService } from "./services/HabitDataService";
|
||||
import { applyHabitCounterStyles } from "./styles/applyHabitCounterStyles";
|
||||
import { buildHabitCounterDOM } from "./dom/buildHabitCounterDOM";
|
||||
import { createDecrementHandler, createIncrementHandler } from "./eventHandlers/clickHandlers";
|
||||
import { loadHabitData } from "./data/loadHabitData";
|
||||
import { updateHabitData } from "./data/updateHabitData";
|
||||
import {
|
||||
updateDisplayValue,
|
||||
updateStaticUI,
|
||||
setComponentErrorState,
|
||||
clearComponentErrorState,
|
||||
HabitCounterUIElements
|
||||
} from "./dom/uiUpdaters";
|
||||
|
||||
//? Main Web Component class, acts as an orchestrator.
|
||||
export class HabitCounter extends HTMLElement {
|
||||
// --- State ---
|
||||
public habitKey: string = ""; //? Make public if needed by handlers/helpers
|
||||
public initialDate: string = "";
|
||||
public emoji: string = "";
|
||||
public table: string = "";
|
||||
public currentValue: number = 0;
|
||||
private _initialLoadTriggered: boolean = false;
|
||||
private _isInitialized: boolean = false;
|
||||
|
||||
// --- Dependencies ---
|
||||
public habitDataService: HabitDataService | null = null; //? Public for helpers
|
||||
|
||||
// --- Shadow DOM Element References ---
|
||||
//? Store the object returned by the builder
|
||||
private uiElements: HabitCounterUIElements = {
|
||||
wrapper: null, valueDisplay: null, labelElement: null, minusButton: null, plusButton: null
|
||||
};
|
||||
|
||||
// --- Lifecycle ---
|
||||
connectedCallback() {
|
||||
if (this._isInitialized) return;
|
||||
//& console.log("[HabitCounter Component] connectedCallback - Initializing...");
|
||||
|
||||
//~ Create handlers bound to this instance *before* building DOM
|
||||
const handlers = {
|
||||
handleDecrement: createDecrementHandler(this),
|
||||
handleIncrement: createIncrementHandler(this),
|
||||
};
|
||||
|
||||
this.uiElements = buildHabitCounterDOM(this.attachShadow({ mode: "open" }), handlers); //~ Build DOM via helper
|
||||
applyHabitCounterStyles(this.shadowRoot!); //~ Apply styles via helper
|
||||
this._isInitialized = true;
|
||||
//& console.log("[HabitCounter Component] Initialization complete.");
|
||||
}
|
||||
|
||||
// --- Public Methods ---
|
||||
public setDbService(service: DBService): void {
|
||||
//& console.log("[HabitCounter Component] setDbService called.");
|
||||
if (!service) {
|
||||
console.error("[HabitCounter Component] Invalid DBService provided.");
|
||||
this.showErrorState("Setup Error"); //~ Use helper
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.habitDataService = new HabitDataService(service); //~ Initialize service dependency
|
||||
} catch (error) {
|
||||
console.error("[HabitCounter Component] Failed to initialize HabitDataService:", error);
|
||||
this.showErrorState("Setup Error");
|
||||
return;
|
||||
}
|
||||
this._readAttributesAndInitLoad(); //~ Trigger attribute reading and loading
|
||||
}
|
||||
|
||||
// --- Internal Orchestration ---
|
||||
private _readAttributesAndInitLoad(): void {
|
||||
//& console.log("[HabitCounter Component] _readAttributesAndInitLoad running.");
|
||||
this.habitKey = this.getAttribute("habit") ?? "";
|
||||
this.initialDate = this.getAttribute("date") ?? "@date";
|
||||
this.emoji = this.getAttribute("emoji") ?? "❓";
|
||||
this.table = this.getAttribute("table") ?? "";
|
||||
|
||||
updateStaticUI(this.uiElements, this.emoji, this.habitKey); //~ Update static parts of UI
|
||||
|
||||
if (!this.table) {
|
||||
console.warn(`[HabitCounter Component ${this.habitKey}] Table attribute missing.`);
|
||||
this.showErrorState("No table");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._initialLoadTriggered) {
|
||||
this._initialLoadTriggered = true;
|
||||
//& console.log(`[HabitCounter Component ${this.habitKey}] Triggering initial loadHabitData...`);
|
||||
loadHabitData(this).catch(err => console.error("Unhandled error during initial load:", err)); //~ Load data via helper
|
||||
}
|
||||
}
|
||||
|
||||
//? Public method called by click handlers via instance reference
|
||||
public async _updateData(delta: number): Promise<void> {
|
||||
//& console.log(`[HabitCounter Component ${this.habitKey}] _updateData called with delta: ${delta}`);
|
||||
await updateHabitData(this, delta); //~ Update data via helper
|
||||
}
|
||||
|
||||
//? Public method called by data helpers to update display
|
||||
public _updateDisplay(newValue: number): void {
|
||||
//& console.log(`[HabitCounter Component ${this.habitKey}] _updateDisplay called with value: ${newValue}`);
|
||||
this.currentValue = newValue; //? Update internal state first
|
||||
updateDisplayValue(this.uiElements, newValue); //~ Update DOM via helper
|
||||
this.clearErrorState(); //? Clear errors on successful update
|
||||
}
|
||||
|
||||
//? Public methods for UI state helpers
|
||||
public showErrorState(message?: string): void {
|
||||
setComponentErrorState(this.uiElements, message); //~ Set error via helper
|
||||
}
|
||||
|
||||
public clearErrorState(): void {
|
||||
clearComponentErrorState(this.uiElements); //~ Clear error via helper
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom Element Definition ---
|
||||
if (!customElements.get("habit-counter")) {
|
||||
customElements.define("habit-counter", HabitCounter);
|
||||
}
|
||||
16
src/webcomponents/HabitCounter/HabitCounter.types.ts
Normal file
16
src/webcomponents/HabitCounter/HabitCounter.types.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//? Defines the expected structure of a record fetched from the habit table.
|
||||
export interface HabitRecord {
|
||||
value: number; //^ Assuming the column name for the count is 'value'
|
||||
}
|
||||
|
||||
//? Defines the properties needed to identify and update a habit entry.
|
||||
export interface HabitDataArgs {
|
||||
table: string;
|
||||
habitKey: string; //^ Assuming the column name for the habit identifier is 'habitKey'
|
||||
date: string;
|
||||
}
|
||||
|
||||
//? Combined arguments including the new value for updates.
|
||||
export interface UpdateHabitDataArgs extends HabitDataArgs {
|
||||
newValue: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { pluginState } from "../../../pluginState";
|
||||
|
||||
/**
|
||||
* Calculates the effective date to use for data operations.
|
||||
* @param initialDate - The date value from the component's 'date' attribute ("YYYY-MM-DD" or "@date").
|
||||
* @returns The effective date string (YYYY-MM-DD).
|
||||
*/
|
||||
export function calculateEffectiveDate(initialDate: string): string {
|
||||
const effectiveDate = initialDate === "@date" ? pluginState.selectedDate : initialDate;
|
||||
//& console.log(`[CalculateEffectiveDate] Calculated: ${effectiveDate} (Initial: ${initialDate}, Global: ${pluginState.selectedDate})`);
|
||||
return effectiveDate;
|
||||
}
|
||||
40
src/webcomponents/HabitCounter/data/loadHabitData.ts
Normal file
40
src/webcomponents/HabitCounter/data/loadHabitData.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { HabitDataService } from "../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "./calculateEffectiveDate";
|
||||
|
||||
//? Interface describing the necessary parts of the component instance for loading.
|
||||
interface LoadComponentInstance {
|
||||
habitDataService: HabitDataService | null;
|
||||
table: string;
|
||||
habitKey: string;
|
||||
initialDate: string; // Needed for effective date calculation
|
||||
_updateDisplay: (value: number) => void; // Method to update UI
|
||||
showErrorState: (message: string) => void; // Method to show error UI
|
||||
}
|
||||
|
||||
/**
|
||||
* //? Orchestrates loading the habit data using the HabitDataService.
|
||||
* @param instance - The HabitCounter component instance.
|
||||
*/
|
||||
export async function loadHabitData(instance: LoadComponentInstance): Promise<void> {
|
||||
//& console.log(`[LoadHabitData ${instance.habitKey}] loadHabitData running.`);
|
||||
if (!instance.habitDataService || !instance.table || !instance.habitKey) {
|
||||
console.error(`[LoadHabitData ${instance.habitKey}] Cannot load data: Missing service, table, or habitKey.`);
|
||||
instance.showErrorState("Load Error");
|
||||
return;
|
||||
}
|
||||
|
||||
const args = {
|
||||
table: instance.table,
|
||||
habitKey: instance.habitKey,
|
||||
date: calculateEffectiveDate(instance.initialDate), //~ Delegate date calculation
|
||||
};
|
||||
|
||||
try {
|
||||
const value = await instance.habitDataService.fetchHabitValue(args);
|
||||
//& console.log(`[LoadHabitData ${instance.habitKey}] Data loaded successfully: ${value}`);
|
||||
instance._updateDisplay(value);
|
||||
} catch (error) {
|
||||
console.error(`[LoadHabitData ${instance.habitKey}] Failed to load data:`, error);
|
||||
instance.showErrorState("Load Error");
|
||||
}
|
||||
}
|
||||
52
src/webcomponents/HabitCounter/data/updateHabitData.ts
Normal file
52
src/webcomponents/HabitCounter/data/updateHabitData.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { HabitDataService } from "../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "./calculateEffectiveDate";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
//? Interface describing the necessary parts of the component instance for updating.
|
||||
interface UpdateComponentInstance {
|
||||
habitDataService: HabitDataService | null;
|
||||
table: string;
|
||||
habitKey: string;
|
||||
initialDate: string; // Needed for effective date calculation
|
||||
currentValue: number; // The current value held by the component
|
||||
_updateDisplay: (value: number) => void; // Method to update UI
|
||||
showErrorState: (message: string) => void; // Method to show error UI
|
||||
clearErrorState: () => void; // Method to clear error UI
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Orchestrates updating the habit data via the HabitDataService.
|
||||
* @param instance - The HabitCounter component instance.
|
||||
* @param delta - The amount to change the value by (+1 or -1).
|
||||
*/
|
||||
export async function updateHabitData(instance: UpdateComponentInstance, delta: number): Promise<void> {
|
||||
//& console.log(`[UpdateHabitData ${instance.habitKey}] updateHabitData running with delta: ${delta}.`);
|
||||
if (!instance.habitDataService || !instance.table || !instance.habitKey) {
|
||||
console.error(`[UpdateHabitData ${instance.habitKey}] Cannot update data: Missing service, table, or habitKey.`);
|
||||
instance.showErrorState("Save Error");
|
||||
new Notice("Cannot update habit: Setup incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNumericValue = typeof instance.currentValue === 'number' ? instance.currentValue : 0;
|
||||
const newValue = Math.max(0, currentNumericValue + delta); //? Prevent negative values
|
||||
|
||||
const args = {
|
||||
table: instance.table,
|
||||
habitKey: instance.habitKey,
|
||||
date: calculateEffectiveDate(instance.initialDate), //~ Delegate date calculation
|
||||
newValue: newValue,
|
||||
};
|
||||
|
||||
try {
|
||||
await instance.habitDataService.updateHabitValue(args);
|
||||
//& console.log(`[UpdateHabitData ${instance.habitKey}] Data updated successfully to: ${newValue}`);
|
||||
instance._updateDisplay(newValue); //? Update UI only on successful save
|
||||
instance.clearErrorState();
|
||||
} catch (error) {
|
||||
console.error(`[UpdateHabitData ${instance.habitKey}] Failed to update data:`, error);
|
||||
instance.showErrorState("Save Error");
|
||||
new Notice(`Error saving value for ${instance.habitKey}`);
|
||||
}
|
||||
}
|
||||
48
src/webcomponents/HabitCounter/dom/buildHabitCounterDOM.ts
Normal file
48
src/webcomponents/HabitCounter/dom/buildHabitCounterDOM.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { HabitCounterUIElements } from "./uiUpdaters";
|
||||
|
||||
//? Defines the expected structure for event handlers passed to the builder.
|
||||
export interface HabitCounterEventHandlers {
|
||||
handleDecrement: () => void;
|
||||
handleIncrement: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Shadow DOM structure for the habit counter.
|
||||
* @param shadowRoot - The ShadowRoot to append elements to.
|
||||
* @param handlers - Object containing click handler functions.
|
||||
* @returns An object containing references to the created UI elements.
|
||||
*/
|
||||
export function buildHabitCounterDOM(
|
||||
shadowRoot: ShadowRoot,
|
||||
handlers: HabitCounterEventHandlers
|
||||
): HabitCounterUIElements {
|
||||
//& console.log("[HabitCounterDOM] buildHabitCounterDOM called.");
|
||||
if (!shadowRoot) throw new Error("Cannot build DOM: ShadowRoot is null.");
|
||||
if (!handlers || typeof handlers.handleDecrement !== 'function' || typeof handlers.handleIncrement !== 'function') {
|
||||
throw new Error("Cannot build DOM: Invalid event handlers provided.");
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "habit-wrapper";
|
||||
|
||||
const labelElement = document.createElement("span");
|
||||
labelElement.className = "habit-label";
|
||||
|
||||
const minusButton = document.createElement("button");
|
||||
minusButton.textContent = "−";
|
||||
minusButton.addEventListener('click', handlers.handleDecrement);
|
||||
|
||||
const plusButton = document.createElement("button");
|
||||
plusButton.textContent = "+";
|
||||
plusButton.addEventListener('click', handlers.handleIncrement);
|
||||
|
||||
const valueDisplay = document.createElement("span");
|
||||
valueDisplay.className = "habit-value";
|
||||
valueDisplay.textContent = "..."; //? Initial loading state
|
||||
|
||||
wrapper.append(labelElement, minusButton, valueDisplay, plusButton);
|
||||
shadowRoot.append(wrapper);
|
||||
|
||||
//? Return references to the important elements
|
||||
return { wrapper, valueDisplay, labelElement, minusButton, plusButton };
|
||||
}
|
||||
58
src/webcomponents/HabitCounter/dom/uiUpdaters.ts
Normal file
58
src/webcomponents/HabitCounter/dom/uiUpdaters.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// src/webcomponents/habitCounter/dom/uiUpdaters.ts
|
||||
|
||||
//? Interface defining the elements needed by UI updaters.
|
||||
export interface HabitCounterUIElements {
|
||||
wrapper: HTMLElement | null;
|
||||
valueDisplay: HTMLElement | null;
|
||||
labelElement: HTMLElement | null;
|
||||
minusButton: HTMLButtonElement | null;
|
||||
plusButton: HTMLButtonElement | null;
|
||||
}
|
||||
|
||||
/** Updates the text content of the value display. */
|
||||
export function updateDisplayValue(elements: HabitCounterUIElements, newValue: number | string): void {
|
||||
//& console.log(`[HabitCounterUI] updateDisplayValue called with: ${newValue}`);
|
||||
if (elements.valueDisplay) {
|
||||
elements.valueDisplay.textContent = String(newValue);
|
||||
} else {
|
||||
console.error("[HabitCounterUI] valueDisplay element not found for update.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates label text and button aria-labels based on component state. */
|
||||
export function updateStaticUI(elements: HabitCounterUIElements, emoji: string, habitKey: string): void {
|
||||
//& console.log(`[HabitCounterUI] updateStaticUI called for: ${habitKey}`);
|
||||
if (elements.labelElement) {
|
||||
elements.labelElement.textContent = `${emoji} ${habitKey}:`;
|
||||
//& console.log(`[HabitCounterUI] Set label text to: "${elements.labelElement.textContent}"`);
|
||||
}
|
||||
if (elements.minusButton) {
|
||||
elements.minusButton.setAttribute("aria-label", `Decrease ${habitKey} value`);
|
||||
}
|
||||
if (elements.plusButton) {
|
||||
elements.plusButton.setAttribute("aria-label", `Increase ${habitKey} value`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Sets the component to visually indicate an error state. */
|
||||
export function setComponentErrorState(elements: HabitCounterUIElements, message: string = "Error"): void {
|
||||
//& console.warn(`[HabitCounterUI] setComponentErrorState called with message: "${message}"`);
|
||||
updateDisplayValue(elements, "ERR");
|
||||
if (elements.wrapper) {
|
||||
elements.wrapper.style.borderColor = "var(--text-error)";
|
||||
elements.wrapper.style.backgroundColor = "var(--background-secondary-alt)";
|
||||
elements.wrapper.setAttribute("title", message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears any visual error indication. */
|
||||
export function clearComponentErrorState(elements: HabitCounterUIElements): void {
|
||||
//& console.log(`[HabitCounterUI] clearComponentErrorState called.`);
|
||||
//? Only reset if actually in error state (check border color)
|
||||
if (elements.wrapper && elements.wrapper.style.borderColor !== "transparent") {
|
||||
elements.wrapper.style.borderColor = "transparent";
|
||||
elements.wrapper.style.backgroundColor = "";
|
||||
elements.wrapper.removeAttribute("title");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//? Type for the component instance
|
||||
//? It needs access to the method that triggers the data update.
|
||||
interface HabitCounterInstance {
|
||||
_updateData: (delta: number) => Promise<void>;
|
||||
habitKey: string;
|
||||
}
|
||||
|
||||
/** Creates the handler for the decrement button click. */
|
||||
export function createDecrementHandler(instance: HabitCounterInstance): () => void {
|
||||
return () => {
|
||||
//& console.log(`[HabitCounterClickHandler ${instance.habitKey}] Decrement clicked.`);
|
||||
instance._updateData(-1).catch(err => {
|
||||
console.error(`[HabitCounterClickHandler ${instance.habitKey}] Error during decrement update:`, err);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates the handler for the increment button click. */
|
||||
export function createIncrementHandler(instance: HabitCounterInstance): () => void {
|
||||
return () => {
|
||||
//& console.log(`[HabitCounterClickHandler ${instance.habitKey}] Increment clicked.`);
|
||||
instance._updateData(1).catch(err => {
|
||||
console.error(`[HabitCounterClickHandler ${instance.habitKey}] Error during increment update:`, err);
|
||||
});
|
||||
};
|
||||
}
|
||||
62
src/webcomponents/HabitCounter/registerHabitCounter.ts
Normal file
62
src/webcomponents/HabitCounter/registerHabitCounter.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// src/webcomponents/registration.ts
|
||||
import { DBService } from "../../DBService";
|
||||
import { HabitCounter } from "./HabitCounter";
|
||||
|
||||
//? Define element if needed here or ensure it's defined elsewhere before this runs
|
||||
if (!customElements.get("habit-counter")) {
|
||||
customElements.define("habit-counter", HabitCounter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds placeholder elements and replaces them with initialized HabitCounter components.
|
||||
* @param el The container element to search within.
|
||||
* @param dbService The DBService instance for dependency injection.
|
||||
*/
|
||||
export const registerHabitCounter = (el: HTMLElement, dbService: DBService) => {
|
||||
const placeholders = el.querySelectorAll("span.habit-counter-placeholder");
|
||||
|
||||
placeholders.forEach((placeholderEl) => { //~ Removed index as it wasn't used
|
||||
if (!(placeholderEl instanceof HTMLElement)) return;
|
||||
|
||||
const habitKey = placeholderEl.dataset.habit; //^ Read 'habit' data attribute
|
||||
const date = placeholderEl.dataset.date;
|
||||
const emoji = placeholderEl.dataset.emoji;
|
||||
const table = placeholderEl.dataset.table;
|
||||
|
||||
// --- Validation ---
|
||||
if (!habitKey) {
|
||||
console.warn("[HabitCounter Reg] Placeholder missing data-habit:", placeholderEl);
|
||||
placeholderEl.textContent = "[Missing habit name]";
|
||||
return;
|
||||
}
|
||||
if (!table) {
|
||||
console.warn(`[HabitCounter Reg] Placeholder for habit "${habitKey}" missing data-table:`, placeholderEl);
|
||||
placeholderEl.textContent = "[Missing table name]";
|
||||
return;
|
||||
}
|
||||
// --- End Validation ---
|
||||
|
||||
try {
|
||||
//& console.log(`[HabitCounter Reg] Creating component for habit: ${habitKey}, table: ${table}`);
|
||||
const component = document.createElement("habit-counter") as HabitCounter;
|
||||
|
||||
//^ Set attributes that the component will read later
|
||||
component.setAttribute("habit", habitKey); // Set 'habit' attribute
|
||||
component.setAttribute("table", table);
|
||||
if (date) component.setAttribute("date", date);
|
||||
else component.setAttribute("date", "@date");
|
||||
if (emoji) component.setAttribute("emoji", emoji);
|
||||
|
||||
//& console.log(`[HabitCounter Reg] Injecting DBService for habit: ${habitKey}`);
|
||||
//? Inject the DBService dependency - component reads attributes inside this call now
|
||||
component.setDbService(dbService);
|
||||
|
||||
placeholderEl.replaceWith(component);
|
||||
//& console.log(`[HabitCounter Reg] Replacement successful for habit: ${habitKey}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[HabitCounter Reg] Error processing placeholder for habit "${habitKey}":`, error, placeholderEl);
|
||||
placeholderEl.textContent = "[Error Loading Counter]";
|
||||
}
|
||||
});
|
||||
}
|
||||
61
src/webcomponents/HabitCounter/services/HabitDataService.ts
Normal file
61
src/webcomponents/HabitCounter/services/HabitDataService.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { DBService } from "src/dbService";
|
||||
import { HabitRecord, HabitDataArgs, UpdateHabitDataArgs } from "../HabitCounter.types";
|
||||
import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier";
|
||||
import { validateFetchArgs, validateUpdateArgs } from "./utils/validateHabitArgs";
|
||||
|
||||
//? Service for database interactions related to habits.
|
||||
export class HabitDataService {
|
||||
private dbService: DBService;
|
||||
|
||||
constructor(dbService: DBService) {
|
||||
if (!dbService) throw new Error("HabitDataService requires a valid DBService instance.");
|
||||
this.dbService = dbService;
|
||||
//& console.log("[HabitDataService] Initialized.");
|
||||
}
|
||||
|
||||
async fetchHabitValue(args: HabitDataArgs): Promise<number> {
|
||||
//& console.log(`[HabitDataService] fetchHabitValue called with:`, args);
|
||||
try {
|
||||
validateFetchArgs(args); //~ Delegate validation
|
||||
const safeTable = quoteSqlIdentifier(args.table); //~ Delegate quoting
|
||||
const sql = `SELECT value FROM ${safeTable} WHERE habitKey = ? AND date = ?`;
|
||||
const params = [args.habitKey, args.date];
|
||||
|
||||
//& console.log(`[HabitDataService] Executing fetch SQL: ${sql} PARAMS:`, params);
|
||||
const result = await this.dbService.getQuery<HabitRecord>(sql, params);
|
||||
//& console.log(`[HabitDataService] Fetch result:`, result);
|
||||
|
||||
const value = result?.[0]?.value;
|
||||
return (typeof value === 'number') ? value : 0; //? Default to 0 if no record or invalid value
|
||||
} catch (error) {
|
||||
console.error(`[HabitDataService] Error in fetchHabitValue for ${args.habitKey}:`, error);
|
||||
throw error; //? Propagate error
|
||||
}
|
||||
}
|
||||
|
||||
async updateHabitValue(args: UpdateHabitDataArgs): Promise<void> {
|
||||
//& console.log(`[HabitDataService] updateHabitValue called with:`, args);
|
||||
try {
|
||||
validateUpdateArgs(args); //~ Delegate validation
|
||||
const safeTable = quoteSqlIdentifier(args.table); //~ Delegate quoting
|
||||
const sql = `
|
||||
INSERT INTO ${safeTable} (habitKey, date, value) VALUES (?, ?, ?)
|
||||
ON CONFLICT(habitKey, date) DO UPDATE SET value = excluded.value;
|
||||
`;
|
||||
//! Requires UNIQUE constraint on (habitKey, date)
|
||||
const params = [args.habitKey, args.date, args.newValue];
|
||||
|
||||
//& console.log(`[HabitDataService] Executing update SQL: ${sql} PARAMS:`, params);
|
||||
await this.dbService.runQuery(sql, params);
|
||||
//& console.log(`[HabitDataService] Update successful.`);
|
||||
} catch (error) {
|
||||
console.error(`[HabitDataService] Error in updateHabitValue for ${args.habitKey}:`, error);
|
||||
//? Log specific guidance for constraint errors
|
||||
if (error instanceof Error && error.message.includes("ON CONFLICT clause does not match")) {
|
||||
console.error(`//! DB Constraint Error: Table '${args.table}' needs a UNIQUE index/key on (habitKey, date). Run: CREATE UNIQUE INDEX IF NOT EXISTS idx_habit_date ON ${quoteSqlIdentifier(args.table)} (habitKey, date);`);
|
||||
throw new Error(`Missing UNIQUE constraint on (habitKey, date) in table ${args.table}. ${error.message}`);
|
||||
}
|
||||
throw error; //? Propagate other errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Safely quotes an identifier (table or column name) for use in SQL queries.
|
||||
* Prevents basic SQL injection via identifiers and handles reserved words.
|
||||
* Only allows alphanumeric characters and underscores. Wraps in double quotes.
|
||||
* @param identifier - The identifier string to quote.
|
||||
* @returns The quoted identifier string.
|
||||
* @throws Error if the identifier contains invalid characters.
|
||||
*/
|
||||
export function quoteSqlIdentifier(identifier: string): string {
|
||||
//? Basic security check for valid characters
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
|
||||
throw new Error(`Invalid identifier used: "${identifier}". Only alphanumeric and underscores allowed.`);
|
||||
}
|
||||
//? SQLite standard quoting is double quotes
|
||||
return `"${identifier}"`;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { HabitDataArgs, UpdateHabitDataArgs } from "../../HabitCounter.types";
|
||||
|
||||
/** Validates arguments required for fetching habit data. */
|
||||
export function validateFetchArgs(args: HabitDataArgs): void {
|
||||
if (!args.table || !args.habitKey || !args.date) {
|
||||
throw new Error(`Missing required arguments for fetch: table='${args.table}', habitKey='${args.habitKey}', date='${args.date}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Validates arguments required for updating habit data. */
|
||||
export function validateUpdateArgs(args: UpdateHabitDataArgs): void {
|
||||
if (!args.table || !args.habitKey || !args.date || typeof args.newValue !== 'number' || args.newValue < 0) {
|
||||
throw new Error(`Invalid or missing arguments for update: table='${args.table}', habitKey='${args.habitKey}', date='${args.date}', newValue='${args.newValue}'`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { getHabitCounterStyles } from "./getHabitCounterStyles";
|
||||
|
||||
/** Creates a style element and appends it to the shadow root. */
|
||||
export function applyHabitCounterStyles(shadowRoot: ShadowRoot): void {
|
||||
//& console.log("[HabitCounterStyles] applyHabitCounterStyles called.");
|
||||
if (!shadowRoot) {
|
||||
console.error("[HabitCounterStyles] Cannot apply styles: ShadowRoot is null.");
|
||||
return;
|
||||
}
|
||||
const style = document.createElement("style");
|
||||
style.textContent = getHabitCounterStyles();
|
||||
shadowRoot.appendChild(style);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/** Returns the CSS string for the component's Shadow DOM. */
|
||||
export function getHabitCounterStyles(): string {
|
||||
//& console.log("[HabitCounterStyles] getHabitCounterStyles called.");
|
||||
|
||||
return `
|
||||
:host { display: inline-flex; vertical-align: middle; }
|
||||
.habit-wrapper {
|
||||
display: flex; align-items: center; gap: 0.5em;
|
||||
font-family: var(--font-text); padding: 2px 5px;
|
||||
border: 1px solid transparent; border-radius: 4px;
|
||||
transition: border-color 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
||||
}
|
||||
.habit-label { white-space: nowrap; }
|
||||
button {
|
||||
background-color: var(--background-secondary); color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border); padding: 1px 7px;
|
||||
font-size: var(--font-ui-small); cursor: pointer; font-weight: bold;
|
||||
border-radius: 4px; line-height: 1.2; min-width: 20px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--background-modifier-hover); border-color: var(--interactive-accent-hover);
|
||||
color: var(--text-accent-hover);
|
||||
}
|
||||
button:active { background-color: var(--background-modifier-active); color: var(--text-accent); }
|
||||
.habit-value {
|
||||
min-width: 2ch; text-align: center; font-weight: bold;
|
||||
padding: 0 2px; color: var(--text-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { DBService } from "../dbService";
|
||||
|
||||
export class HabitCounter extends HTMLElement {
|
||||
private habit: string;
|
||||
private date: string;
|
||||
private count = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
|
||||
this.habit = this.getAttribute("habit") ?? "";
|
||||
this.date = this.getAttribute("date") ?? new Date().toISOString().split("T")[0];
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "habit-wrapper";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "habit-label";
|
||||
label.textContent = `🚬 ${this.habit}:`;
|
||||
|
||||
const minus = document.createElement("button");
|
||||
minus.textContent = "−";
|
||||
minus.onclick = () => this.updateValue(-1);
|
||||
|
||||
const plus = document.createElement("button");
|
||||
plus.textContent = "+";
|
||||
plus.onclick = () => this.updateValue(1);
|
||||
|
||||
const valueDisplay = document.createElement("span");
|
||||
valueDisplay.className = "habit-value";
|
||||
valueDisplay.textContent = String(this.count);
|
||||
|
||||
wrapper.append(label, minus, valueDisplay, plus);
|
||||
shadow.append(wrapper);
|
||||
|
||||
this.loadValue().then(value => {
|
||||
this.count = value;
|
||||
valueDisplay.textContent = String(this.count);
|
||||
});
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.habit-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
font-family: var(--font-text);
|
||||
}
|
||||
button {
|
||||
background: var(--background-secondary);
|
||||
border: none;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--interactive-accent-hover);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
.habit-value {
|
||||
min-width: 2ch;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
shadow.appendChild(style);
|
||||
}
|
||||
|
||||
private async loadValue(): Promise<number> {
|
||||
// ⚠️ Replace this with actual DBService call
|
||||
// For now, just simulate with 5
|
||||
return 5;
|
||||
}
|
||||
|
||||
private async updateValue(delta: number) {
|
||||
this.count = Math.max(0, this.count + delta);
|
||||
// this.shadowRoot?.querySelector(".habit-value")!.textContent = String(this.count);
|
||||
|
||||
// ⚠️ Write to DB here, for now just console.log
|
||||
console.log(`[HabitCounter] Updated ${this.habit} on ${this.date} to`, this.count);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("habit-counter", HabitCounter);
|
||||
0
src/webcomponents/ji
Normal file
0
src/webcomponents/ji
Normal file
Loading…
Reference in a new issue