Merge pull request #11 from DanielVolz/copilot/remove-duplicate-code

Eliminate duplicate code: centralize database schema and date formatting utilities
This commit is contained in:
Daniel Volz
2026-01-01 19:52:55 +01:00
committed by GitHub
6 changed files with 164 additions and 339 deletions
+3 -91
View File
@@ -3,6 +3,7 @@ import { drizzle } from "drizzle-orm/libsql";
import { existsSync, mkdirSync, accessSync, constants, statSync, writeFileSync } from "fs";
import { resolve } from "path";
import dotenv from "dotenv";
import { getTableCreationSQL } from "./schema-sql";
dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
@@ -43,97 +44,8 @@ export function ensureDataDirectory(dataDir: string): { success: boolean; error?
}
}
/** Get the SQL statements for creating all tables */
export function getTableCreationSQL(): string[] {
return [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
}
/** Get the SQL statements for creating all tables (re-exported from schema-sql) */
export { getTableCreationSQL } from "./schema-sql";
/** Run table creation migrations on a client */
export async function runTableMigrations(client: Client): Promise<{ success: boolean; errors: string[] }> {
+5 -99
View File
@@ -2,6 +2,7 @@ import { createClient, Client } from "@libsql/client";
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
import { getTableCreationSQL } from "./schema-sql";
dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
@@ -9,101 +10,8 @@ dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
// Exported utility functions for testing
// =============================================================================
/** Get the full migration SQL string */
export function getMigrationSQL(): string {
return `
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`;
}
/** Get the full migration SQL string (re-exported from schema-sql) */
export { getTableCreationSQL };
/** Split SQL string into individual statements */
export function splitSQLStatements(sql: string): string[] {
@@ -112,8 +20,7 @@ export function splitSQLStatements(sql: string): string[] {
/** Execute migration statements on a client */
export async function executeMigration(client: Client): Promise<{ success: boolean; executed: number; errors: string[] }> {
const sql = getMigrationSQL();
const statements = splitSQLStatements(sql);
const statements = getTableCreationSQL();
const errors: string[] = [];
let executed = 0;
@@ -150,8 +57,7 @@ async function main() {
const client = createClient({ url });
const sql = getMigrationSQL();
const statements = splitSQLStatements(sql);
const statements = getTableCreationSQL();
for (const stmt of statements) {
console.log("Executing:", getStatementPreview(stmt));
+99
View File
@@ -0,0 +1,99 @@
/**
* Shared SQL table creation statements for database initialization.
* Used by client.ts, migrate.ts, and test setup to avoid duplication.
*/
/**
* Get all SQL table creation statements as an array.
* Each statement creates a table if it doesn't exist.
*/
export function getTableCreationSQL(): string[] {
return [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
}
+16 -16
View File
@@ -17,28 +17,29 @@ import {
// Import the exported utility functions from migrate.ts
import {
getMigrationSQL,
getTableCreationSQL as getTableCreationSQLFromMigrate,
splitSQLStatements,
executeMigration,
getStatementPreview,
} from "../db/migrate.js";
describe("Migration Script Utilities", () => {
describe("getMigrationSQL", () => {
it("should return a non-empty SQL string", () => {
const sql = getMigrationSQL();
expect(typeof sql).toBe("string");
expect(sql.length).toBeGreaterThan(100);
describe("getTableCreationSQL", () => {
it("should return a non-empty array of SQL statements", () => {
const statements = getTableCreationSQL();
expect(Array.isArray(statements)).toBe(true);
expect(statements.length).toBeGreaterThan(0);
});
it("should contain all table definitions", () => {
const sql = getMigrationSQL();
expect(sql).toContain("CREATE TABLE IF NOT EXISTS users");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS medications");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS user_settings");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS share_tokens");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS dose_tracking");
const statements = getTableCreationSQL();
const allSQL = statements.join(" ");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS users");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS medications");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS user_settings");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS share_tokens");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS dose_tracking");
});
});
@@ -61,9 +62,8 @@ describe("Migration Script Utilities", () => {
expect(statements).toHaveLength(2);
});
it("should split migration SQL into 6 statements", () => {
const sql = getMigrationSQL();
const statements = splitSQLStatements(sql);
it("should handle getTableCreationSQL output correctly", () => {
const statements = getTableCreationSQL();
expect(statements).toHaveLength(6);
});
+2 -88
View File
@@ -10,6 +10,7 @@ import fastifyMultipart from "@fastify/multipart";
import { createClient, Client } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { beforeAll, afterAll, beforeEach } from "vitest";
import { getTableCreationSQL } from "../db/schema-sql";
// Type for our test database
export type TestDb = ReturnType<typeof drizzle>;
@@ -63,94 +64,7 @@ export async function buildTestApp(): Promise<TestContext> {
* Create test database schema
*/
async function runTestMigrations(client: Client): Promise<void> {
const tableCreations = [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
const tableCreations = getTableCreationSQL();
for (const sql of tableCreations) {
await client.execute(sql);
+39 -45
View File
@@ -358,15 +358,12 @@ function AppContent() {
setScheduleDays(storedDays ? Number(storedDays) : 30);
// Load manually collapsed/expanded days from localStorage
const storedCollapsed = localStorage.getItem(userStorageKey(user.id, "collapsedDays"));
const storedExpanded = localStorage.getItem(userStorageKey(user.id, "expandedDays"));
try {
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set());
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set());
} catch {
setManuallyCollapsedDays(new Set());
setManuallyExpandedDays(new Set());
}
const { collapsed, expanded } = loadCollapsedDaysFromStorage(
userStorageKey(user.id, "collapsedDays"),
userStorageKey(user.id, "expandedDays")
);
setManuallyCollapsedDays(collapsed);
setManuallyExpandedDays(expanded);
}
}, [user?.id]);
@@ -2895,22 +2892,36 @@ function toIsoString(value: string) {
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
}
function pad2(n: number): string {
return String(n).padStart(2, '0');
}
function loadCollapsedDaysFromStorage(collapsedKey: string, expandedKey: string): { collapsed: Set<string>; expanded: Set<string> } {
const storedCollapsed = localStorage.getItem(collapsedKey);
const storedExpanded = localStorage.getItem(expandedKey);
try {
return {
collapsed: storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set(),
expanded: storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set()
};
} catch {
return {
collapsed: new Set(),
expanded: new Set()
};
}
}
function toDateValue(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
if (Number.isNaN(d.getTime())) {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
return `${fallback.getFullYear()}-${pad2(fallback.getMonth() + 1)}-${pad2(fallback.getDate())}`;
}
function toTimeValue(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
if (Number.isNaN(d.getTime())) {
const now = new Date();
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
}
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
return `${pad2(fallback.getHours())}:${pad2(fallback.getMinutes())}`;
}
function combineDateAndTime(dateStr: string, timeStr: string): string {
@@ -2920,23 +2931,9 @@ function combineDateAndTime(dateStr: string, timeStr: string): string {
function toInputValue(value: string) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
// Return current local time in datetime-local format
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// Convert to local time format for datetime-local input
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
const d = Number.isNaN(date.getTime()) ? new Date() : date;
// Use existing helper functions to avoid duplication
return `${toDateValue(d)}T${toTimeValue(d)}`;
}
function formatDateTime(value: string, locale: string) {
@@ -3449,15 +3446,12 @@ function SharedSchedule() {
// Load collapsed/expanded state from localStorage
useEffect(() => {
if (token && typeof window !== "undefined") {
const storedCollapsed = localStorage.getItem(`share_${token}_collapsedDays`);
const storedExpanded = localStorage.getItem(`share_${token}_expandedDays`);
try {
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set());
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set());
} catch {
setManuallyCollapsedDays(new Set());
setManuallyExpandedDays(new Set());
}
const { collapsed, expanded } = loadCollapsedDaysFromStorage(
`share_${token}_collapsedDays`,
`share_${token}_expandedDays`
);
setManuallyCollapsedDays(collapsed);
setManuallyExpandedDays(expanded);
}
}, [token]);