400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
import {
|
|
validateBody,
|
|
validateQuery
|
|
} from "./chunk-5EGA6GHY.js";
|
|
import {
|
|
NotFoundError,
|
|
resolveNotebookPath
|
|
} from "./chunk-ER4KPD22.js";
|
|
import {
|
|
asyncHandler,
|
|
createApiModule,
|
|
defineApiModule,
|
|
defineEndpoints,
|
|
successResponse
|
|
} from "./chunk-74TMTGBG.js";
|
|
|
|
// shared/modules/todo/api.ts
|
|
var TODO_ENDPOINTS = defineEndpoints({
|
|
list: { path: "/", method: "GET" },
|
|
save: { path: "/save", method: "POST" },
|
|
add: { path: "/add", method: "POST" },
|
|
toggle: { path: "/toggle", method: "POST" },
|
|
update: { path: "/update", method: "POST" },
|
|
delete: { path: "/delete", method: "DELETE" }
|
|
});
|
|
|
|
// shared/modules/todo/index.ts
|
|
var TODO_MODULE = defineApiModule({
|
|
id: "todo",
|
|
name: "TODO",
|
|
basePath: "/todo",
|
|
order: 30,
|
|
version: "1.0.0",
|
|
endpoints: TODO_ENDPOINTS
|
|
});
|
|
|
|
// api/modules/todo/service.ts
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
|
|
// api/modules/todo/parser.ts
|
|
var parseTodoContent = (content) => {
|
|
const lines = content.split("\n");
|
|
const result = [];
|
|
let currentDate = null;
|
|
let currentItems = [];
|
|
let itemId = 0;
|
|
for (const line of lines) {
|
|
const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/);
|
|
if (dateMatch) {
|
|
if (currentDate) {
|
|
result.push({ date: currentDate, items: currentItems });
|
|
}
|
|
currentDate = dateMatch[1];
|
|
currentItems = [];
|
|
} else if (currentDate) {
|
|
const todoMatch = line.match(/^- (√|○) (.*)$/);
|
|
if (todoMatch) {
|
|
currentItems.push({
|
|
id: `${currentDate}-${itemId++}`,
|
|
content: todoMatch[2],
|
|
completed: todoMatch[1] === "\u221A"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (currentDate) {
|
|
result.push({ date: currentDate, items: currentItems });
|
|
}
|
|
return result;
|
|
};
|
|
var generateTodoContent = (dayTodos) => {
|
|
const lines = [];
|
|
const sortedDays = [...dayTodos].sort((a, b) => a.date.localeCompare(b.date));
|
|
for (const day of sortedDays) {
|
|
lines.push(`## ${day.date}`);
|
|
for (const item of day.items) {
|
|
const checkbox = item.completed ? "\u221A" : "\u25CB";
|
|
lines.push(`- ${checkbox} ${item.content}`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
return lines.join("\n").trimEnd();
|
|
};
|
|
|
|
// api/modules/todo/service.ts
|
|
var TodoService = class {
|
|
constructor(deps = {}) {
|
|
this.deps = deps;
|
|
}
|
|
getTodoFilePath(year, month) {
|
|
const yearStr = year.toString();
|
|
const monthStr = month.toString().padStart(2, "0");
|
|
const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md`;
|
|
const { fullPath } = resolveNotebookPath(relPath);
|
|
return { relPath, fullPath };
|
|
}
|
|
async ensureTodoFileExists(fullPath) {
|
|
const dir = path.dirname(fullPath);
|
|
await fs.mkdir(dir, { recursive: true });
|
|
try {
|
|
await fs.access(fullPath);
|
|
} catch {
|
|
await fs.writeFile(fullPath, "", "utf-8");
|
|
}
|
|
}
|
|
async loadAndParseTodoFile(year, month) {
|
|
const { fullPath } = this.getTodoFilePath(year, month);
|
|
try {
|
|
await fs.access(fullPath);
|
|
} catch {
|
|
throw new NotFoundError("TODO file not found");
|
|
}
|
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
return { fullPath, dayTodos: parseTodoContent(content) };
|
|
}
|
|
async saveTodoFile(fullPath, dayTodos) {
|
|
const content = generateTodoContent(dayTodos);
|
|
await fs.writeFile(fullPath, content, "utf-8");
|
|
}
|
|
async getTodo(year, month) {
|
|
const { fullPath } = this.getTodoFilePath(year, month);
|
|
let dayTodos = [];
|
|
try {
|
|
await fs.access(fullPath);
|
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
dayTodos = parseTodoContent(content);
|
|
} catch {
|
|
}
|
|
const now = /* @__PURE__ */ new Date();
|
|
const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`;
|
|
const yesterday = new Date(now);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, "0")}-${yesterday.getDate().toString().padStart(2, "0")}`;
|
|
if (year === now.getFullYear() && month === now.getMonth() + 1) {
|
|
const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr);
|
|
if (migrated) {
|
|
const newContent = generateTodoContent(dayTodos);
|
|
await this.ensureTodoFileExists(fullPath);
|
|
await fs.writeFile(fullPath, newContent, "utf-8");
|
|
}
|
|
}
|
|
return { dayTodos, year, month };
|
|
}
|
|
migrateIncompleteItems(dayTodos, todayStr, yesterdayStr) {
|
|
let migrated = false;
|
|
const yesterdayTodo = dayTodos.find((d) => d.date === yesterdayStr);
|
|
if (yesterdayTodo) {
|
|
const incompleteItems = yesterdayTodo.items.filter((item) => !item.completed);
|
|
if (incompleteItems.length > 0) {
|
|
const todayTodo = dayTodos.find((d) => d.date === todayStr);
|
|
if (todayTodo) {
|
|
const existingIds = new Set(todayTodo.items.map((i) => i.id));
|
|
const itemsToAdd = incompleteItems.map((item, idx) => ({
|
|
...item,
|
|
id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id
|
|
}));
|
|
todayTodo.items = [...itemsToAdd, ...todayTodo.items];
|
|
} else {
|
|
dayTodos.push({
|
|
date: todayStr,
|
|
items: incompleteItems.map((item, idx) => ({
|
|
...item,
|
|
id: `${todayStr}-migrated-${idx}`
|
|
}))
|
|
});
|
|
}
|
|
yesterdayTodo.items = yesterdayTodo.items.filter((item) => item.completed);
|
|
if (yesterdayTodo.items.length === 0) {
|
|
const index = dayTodos.findIndex((d) => d.date === yesterdayStr);
|
|
if (index !== -1) {
|
|
dayTodos.splice(index, 1);
|
|
}
|
|
}
|
|
migrated = true;
|
|
}
|
|
}
|
|
return migrated;
|
|
}
|
|
async saveTodo(year, month, dayTodos) {
|
|
const { fullPath } = this.getTodoFilePath(year, month);
|
|
await this.ensureTodoFileExists(fullPath);
|
|
const content = generateTodoContent(dayTodos);
|
|
await fs.writeFile(fullPath, content, "utf-8");
|
|
}
|
|
async addTodo(year, month, date, todoContent) {
|
|
const { fullPath } = this.getTodoFilePath(year, month);
|
|
await this.ensureTodoFileExists(fullPath);
|
|
let fileContent = await fs.readFile(fullPath, "utf-8");
|
|
const dayTodos = parseTodoContent(fileContent);
|
|
const existingDay = dayTodos.find((d) => d.date === date);
|
|
if (existingDay) {
|
|
const newId = `${date}-${existingDay.items.length}`;
|
|
existingDay.items.push({
|
|
id: newId,
|
|
content: todoContent,
|
|
completed: false
|
|
});
|
|
} else {
|
|
dayTodos.push({
|
|
date,
|
|
items: [{
|
|
id: `${date}-0`,
|
|
content: todoContent,
|
|
completed: false
|
|
}]
|
|
});
|
|
}
|
|
fileContent = generateTodoContent(dayTodos);
|
|
await fs.writeFile(fullPath, fileContent, "utf-8");
|
|
return dayTodos;
|
|
}
|
|
async toggleTodo(year, month, date, itemIndex, completed) {
|
|
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month);
|
|
const day = dayTodos.find((d) => d.date === date);
|
|
if (!day || itemIndex >= day.items.length) {
|
|
throw new NotFoundError("TODO item not found");
|
|
}
|
|
day.items[itemIndex].completed = completed;
|
|
await this.saveTodoFile(fullPath, dayTodos);
|
|
return dayTodos;
|
|
}
|
|
async updateTodo(year, month, date, itemIndex, newContent) {
|
|
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month);
|
|
const day = dayTodos.find((d) => d.date === date);
|
|
if (!day || itemIndex >= day.items.length) {
|
|
throw new NotFoundError("TODO item not found");
|
|
}
|
|
day.items[itemIndex].content = newContent;
|
|
await this.saveTodoFile(fullPath, dayTodos);
|
|
return dayTodos;
|
|
}
|
|
async deleteTodo(year, month, date, itemIndex) {
|
|
const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month);
|
|
const dayIndex = dayTodos.findIndex((d) => d.date === date);
|
|
if (dayIndex === -1) {
|
|
throw new NotFoundError("Day not found");
|
|
}
|
|
const day = dayTodos[dayIndex];
|
|
if (itemIndex >= day.items.length) {
|
|
throw new NotFoundError("TODO item not found");
|
|
}
|
|
day.items.splice(itemIndex, 1);
|
|
if (day.items.length === 0) {
|
|
dayTodos.splice(dayIndex, 1);
|
|
}
|
|
await this.saveTodoFile(fullPath, dayTodos);
|
|
return dayTodos;
|
|
}
|
|
};
|
|
var createTodoService = (deps) => {
|
|
return new TodoService(deps);
|
|
};
|
|
|
|
// api/modules/todo/routes.ts
|
|
import express from "express";
|
|
|
|
// api/modules/todo/schemas.ts
|
|
import { z } from "zod";
|
|
var todoItemSchema = z.object({
|
|
id: z.string(),
|
|
content: z.string(),
|
|
completed: z.boolean()
|
|
});
|
|
var dayTodoSchema = z.object({
|
|
date: z.string(),
|
|
items: z.array(todoItemSchema)
|
|
});
|
|
var getTodoQuerySchema = z.object({
|
|
year: z.string().optional(),
|
|
month: z.string().optional()
|
|
});
|
|
var saveTodoSchema = z.object({
|
|
year: z.number().int().positive(),
|
|
month: z.number().int().min(1).max(12),
|
|
dayTodos: z.array(dayTodoSchema)
|
|
});
|
|
var addTodoSchema = z.object({
|
|
year: z.number().int().positive(),
|
|
month: z.number().int().min(1).max(12),
|
|
date: z.string(),
|
|
content: z.string()
|
|
});
|
|
var toggleTodoSchema = z.object({
|
|
year: z.number().int().positive(),
|
|
month: z.number().int().min(1).max(12),
|
|
date: z.string(),
|
|
itemIndex: z.number().int().nonnegative(),
|
|
completed: z.boolean()
|
|
});
|
|
var updateTodoSchema = z.object({
|
|
year: z.number().int().positive(),
|
|
month: z.number().int().min(1).max(12),
|
|
date: z.string(),
|
|
itemIndex: z.number().int().nonnegative(),
|
|
content: z.string()
|
|
});
|
|
var deleteTodoSchema = z.object({
|
|
year: z.number().int().positive(),
|
|
month: z.number().int().min(1).max(12),
|
|
date: z.string(),
|
|
itemIndex: z.number().int().nonnegative()
|
|
});
|
|
|
|
// api/modules/todo/routes.ts
|
|
var createTodoRoutes = (deps) => {
|
|
const router = express.Router();
|
|
const { todoService: todoService2 } = deps;
|
|
router.get(
|
|
"/",
|
|
validateQuery(getTodoQuerySchema),
|
|
asyncHandler(async (req, res) => {
|
|
const year = parseInt(req.query.year) || (/* @__PURE__ */ new Date()).getFullYear();
|
|
const month = parseInt(req.query.month) || (/* @__PURE__ */ new Date()).getMonth() + 1;
|
|
const result = await todoService2.getTodo(year, month);
|
|
successResponse(res, result);
|
|
})
|
|
);
|
|
router.post(
|
|
"/save",
|
|
validateBody(saveTodoSchema),
|
|
asyncHandler(async (req, res) => {
|
|
const { year, month, dayTodos } = req.body;
|
|
await todoService2.saveTodo(year, month, dayTodos);
|
|
successResponse(res, null);
|
|
})
|
|
);
|
|
router.post(
|
|
"/add",
|
|
validateBody(addTodoSchema),
|
|
asyncHandler(async (req, res) => {
|
|
const { year, month, date, content: todoContent } = req.body;
|
|
const dayTodos = await todoService2.addTodo(year, month, date, todoContent);
|
|
successResponse(res, { dayTodos });
|
|
})
|
|
);
|
|
router.post(
|
|
"/toggle",
|
|
validateBody(toggleTodoSchema),
|
|
asyncHandler(async (req, res) => {
|
|
const { year, month, date, itemIndex, completed } = req.body;
|
|
const dayTodos = await todoService2.toggleTodo(year, month, date, itemIndex, completed);
|
|
successResponse(res, { dayTodos });
|
|
})
|
|
);
|
|
router.post(
|
|
"/update",
|
|
validateBody(updateTodoSchema),
|
|
asyncHandler(async (req, res) => {
|
|
const { year, month, date, itemIndex, content: newContent } = req.body;
|
|
const dayTodos = await todoService2.updateTodo(year, month, date, itemIndex, newContent);
|
|
successResponse(res, { dayTodos });
|
|
})
|
|
);
|
|
router.delete(
|
|
"/delete",
|
|
validateBody(deleteTodoSchema),
|
|
asyncHandler(async (req, res) => {
|
|
const { year, month, date, itemIndex } = req.body;
|
|
const dayTodos = await todoService2.deleteTodo(year, month, date, itemIndex);
|
|
successResponse(res, { dayTodos });
|
|
})
|
|
);
|
|
return router;
|
|
};
|
|
var todoService = new TodoService();
|
|
var routes_default = createTodoRoutes({ todoService });
|
|
|
|
// api/modules/todo/index.ts
|
|
var createTodoModule = () => {
|
|
return createApiModule(TODO_MODULE, {
|
|
routes: (container) => {
|
|
const todoService2 = container.getSync("todoService");
|
|
return createTodoRoutes({ todoService: todoService2 });
|
|
},
|
|
lifecycle: {
|
|
onLoad: (container) => {
|
|
container.register("todoService", () => new TodoService());
|
|
}
|
|
}
|
|
});
|
|
};
|
|
var todo_default = createTodoModule;
|
|
|
|
export {
|
|
parseTodoContent,
|
|
generateTodoContent,
|
|
TodoService,
|
|
createTodoService,
|
|
getTodoQuerySchema,
|
|
saveTodoSchema,
|
|
addTodoSchema,
|
|
toggleTodoSchema,
|
|
updateTodoSchema,
|
|
deleteTodoSchema,
|
|
createTodoRoutes,
|
|
createTodoModule,
|
|
todo_default
|
|
};
|