@akanjs/cli 2.3.1-rc.2 → 2.3.1-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/index.js +9 -2
  2. package/package.json +2 -2
  3. package/templates/appSample/common/formatters.ts +32 -0
  4. package/templates/appSample/common/validators.ts +32 -0
  5. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +47 -0
  6. package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +38 -0
  7. package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +28 -0
  8. package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
  9. package/templates/appSample/lib/_noti/noti.dictionary.ts +25 -0
  10. package/templates/appSample/lib/_noti/noti.service.ts +31 -0
  11. package/templates/appSample/lib/_noti/noti.signal.ts +30 -0
  12. package/templates/appSample/lib/_noti/noti.store.ts +38 -0
  13. package/templates/appSample/lib/task/Task.Template.tsx +49 -0
  14. package/templates/appSample/lib/task/Task.Unit.tsx +70 -0
  15. package/templates/appSample/lib/task/Task.Util.tsx +103 -0
  16. package/templates/appSample/lib/task/Task.View.tsx +94 -0
  17. package/templates/appSample/lib/task/Task.Zone.tsx +66 -0
  18. package/templates/appSample/lib/task/task.abstract.ts +40 -0
  19. package/templates/appSample/lib/task/task.constant.ts +54 -0
  20. package/templates/appSample/lib/task/task.dictionary.ts +85 -0
  21. package/templates/appSample/lib/task/task.document.ts +54 -0
  22. package/templates/appSample/lib/task/task.service.ts +48 -0
  23. package/templates/appSample/lib/task/task.signal.ts +49 -0
  24. package/templates/appSample/lib/task/task.store.ts +45 -0
  25. package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
  26. package/templates/appSample/page/task/[taskId]/edit.tsx +34 -0
  27. package/templates/appSample/page/task/_index.tsx +38 -0
  28. package/templates/appSample/page/task/_layout.tsx +24 -0
  29. package/templates/appSample/page/task/new.tsx +34 -0
  30. package/templates/appSample/srvkit/AuthGuard.ts +35 -0
  31. package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
  32. package/templates/appSample/ui/GlobalLoading.tsx +30 -0
  33. package/templates/appSample/ui/QuantityControl.tsx +52 -0
  34. package/templates/appSample/webkit/useDebounce.ts +41 -0
  35. package/templates/workspaceRoot/AGENTS.md.template +470 -16
  36. package/templates/workspaceRoot/package.json.template +4 -1
  37. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +62 -0
@@ -0,0 +1,94 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "Task.View.tsx",
6
+ content: `import { clsx } from "akanjs/client";
7
+
8
+ import * as cnst from "@apps/${dict.appName}/lib/cnst";
9
+ import { usePage } from "@apps/${dict.appName}/client";
10
+
11
+ // ===== Task.View.tsx =====
12
+ // Convention: lib/<module>/ — PascalCase .tsx, View suffix = detail display component.
13
+ // View receives the Full Model (cnst.Task) as props — gives access to all defined fields.
14
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
15
+ // Invoked by Akan's data-loading zone chain: Task.Zone → Load.View → Task.View.
16
+
17
+ interface GeneralProps {
18
+ className?: string;
19
+ task: cnst.Task;
20
+ }
21
+
22
+ export const General = ({ className, task }: GeneralProps) => {
23
+ const { l } = usePage();
24
+ const statusColor = {
25
+ todo: "text-base-content/50",
26
+ inProgress: "text-primary",
27
+ completed: "text-success",
28
+ }[task.status];
29
+
30
+ return (
31
+ <div className={clsx("flex w-full flex-col gap-4", className)}>
32
+ <div>
33
+ <h1 className="font-bold text-2xl text-base-content">{task.title}</h1>
34
+ <div className={clsx("mt-1 font-medium text-sm", statusColor)}>
35
+ {l(\`taskStatus.\${task.status}\`)}
36
+ </div>
37
+ </div>
38
+
39
+ {task.content && (
40
+ <div className="rounded-lg border border-base-content/10 bg-base-100 p-4">
41
+ <p className="whitespace-pre-wrap text-base-content/80 text-sm">
42
+ {task.content}
43
+ </p>
44
+ </div>
45
+ )}
46
+
47
+ <div className="flex items-center gap-4 text-base-content/60 text-sm">
48
+ <div>
49
+ <span className="font-medium">
50
+ {l("task.taskDueLabel")}{" "}
51
+ </span>
52
+ {task.due
53
+ ? task.due.toDate().toLocaleDateString()
54
+ : l("task.taskNoDue")}
55
+ </div>
56
+ </div>
57
+
58
+ {task.workHistory && task.workHistory.length > 0 && (
59
+ <div className="mt-2 border-t border-base-content/10 pt-4">
60
+ <h3 className="font-semibold text-base-content text-sm mb-3">{l("task.taskWorkHistoryTitle")}</h3>
61
+ <ul className="space-y-2">
62
+ {task.workHistory.map((entry, i) => (
63
+ <li key={i} className="flex items-start gap-3 text-sm">
64
+ <span className={clsx("badge badge-xs mt-0.5 shrink-0", {
65
+ "badge-ghost": entry.action === "created",
66
+ "badge-primary": entry.action === "started",
67
+ "badge-success": entry.action === "completed",
68
+ })}>
69
+ {l(\`workHistoryAction.\${entry.action}\`)}
70
+ </span>
71
+ <div>
72
+ <span className="text-base-content/60">{entry.at.toDate().toLocaleString()}</span>
73
+ {entry.note && <span className="text-base-content/50 ml-2">{entry.note}</span>}
74
+ </div>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ </div>
79
+ )}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ // ---- Expandable additional fields: ----
85
+ // Compact: for narrow spaces like sidebars or modals
86
+ // export const Compact = ({ className, task }: CompactProps) => (
87
+ // <div className={clsx("text-sm", className)}>
88
+ // <span className="font-medium">{task.title}</span>
89
+ // {task.due && <span className="text-base-content/50 ml-2">Due: {new Date(task.due).toLocaleDateString()}</span>}
90
+ // </div>
91
+ // );
92
+ `,
93
+ };
94
+ }
@@ -0,0 +1,66 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "Task.Zone.tsx",
6
+ content: `"use client";
7
+
8
+ import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
9
+ import { Load } from "akanjs/ui";
10
+ import type { ClientInit, ClientView } from "akanjs/fetch";
11
+
12
+ import * as cnst from "../cnst";
13
+
14
+ // ===== Task.Zone.tsx =====
15
+ // Convention: lib/<module>/ — PascalCase .tsx, Zone suffix = composition layer between pages and UI.
16
+ // Zone components use Load.Units / Load.View from akanjs/ui — the framework convention for data-bound zones.
17
+ // Takes ClientInit / ClientView props from page loader — data is fetched server-side.
18
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
19
+ // Invoked directly from page components: <Task.Zone.Card init={...} />, <Task.Zone.View view={...} />.
20
+
21
+ interface CardProps {
22
+ className?: string;
23
+ init: ClientInit<"task", cnst.LightTask>;
24
+ }
25
+
26
+ export const Card = ({ className, init }: CardProps) => {
27
+ const { l } = usePage();
28
+ return (
29
+ <Load.Units
30
+ className={className}
31
+ init={init}
32
+ renderEmpty={() => (
33
+ <div className="flex flex-col items-center gap-4 py-16 text-center">
34
+ <div className="text-base-content/40 text-lg">{l("task.taskNoTasks")}</div>
35
+ <button className="btn btn-primary btn-sm">{l("task.taskCreateFirst")}</button>
36
+ </div>
37
+ )}
38
+ renderItem={(task) => (
39
+ <Task.Unit.Card
40
+ key={task.id}
41
+ task={task}
42
+ href={\`/task/\${task.id}\`}
43
+ />
44
+ )}
45
+ />
46
+ );
47
+ };
48
+
49
+ interface ViewProps {
50
+ className?: string;
51
+ view: ClientView<"task", cnst.Task>;
52
+ }
53
+
54
+ export const View = ({ className, view }: ViewProps) => {
55
+ return (
56
+ <Load.View
57
+ className={className}
58
+ view={view}
59
+ renderView={(task) => <Task.View.General task={task} />}
60
+ />
61
+ );
62
+ };
63
+
64
+ `,
65
+ };
66
+ }
@@ -0,0 +1,40 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.abstract.md",
6
+ content: `# Task Abstract
7
+
8
+ ## Akan.js Module Pattern
9
+
10
+ Task is a **database-backed domain module** — the standard Akan.js pattern for persisted entities.
11
+ This module demonstrates the full Akan.js module lifecycle:
12
+ constant -> document -> service -> signal -> store -> UI.
13
+
14
+ Every database module in Akan.js follows this layered architecture:
15
+ - task.constant.ts — data shape (enum, Input, Object, Light, Full layers via via())
16
+ - task.document.ts — DB queries, filters, sorts, indexes, Document chain methods
17
+ - task.dictionary.ts — i18n labels, error messages, UI translations
18
+ - task.service.ts — business logic orchestration bound to DB via serve(db.task, ...)
19
+ - task.signal.ts — API surface (query, mutation, pubsub endpoints bound via endpoint(srv.task, ...))
20
+ - task.store.ts — client state management bound to signal via store(sig.task, ...)
21
+ - Task.*.tsx — UI components (Zone -> Load -> Unit / View / Template / Util)
22
+
23
+ ## Convention: lib/<module>/ directory naming
24
+
25
+ - Database modules live under lib/<module>/ (no underscore prefix)
26
+ - Files follow <module>.<layer>.ts naming — e.g., task.constant.ts, task.service.ts
27
+ - UI files use PascalCase with explicit suffix: Task.Zone.tsx, Task.Unit.tsx, Task.View.tsx
28
+
29
+ ## Convention: scan-registered barrels
30
+
31
+ akan scan auto-discovers each file and registers it in the corresponding barrel:
32
+ cnst.ts, db.ts, dict.ts, srv.ts, sig.ts, st.ts
33
+
34
+ ## Related Modules
35
+
36
+ - lib/__scalar/workHistory/ — standard scalar modules with double-underscore prefix (reusable field sets)
37
+ - lib/_noti/ — pure service module (compare structure differences: no constant/document, serve without DB binding)
38
+ `,
39
+ };
40
+ }
@@ -0,0 +1,54 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.constant.ts",
6
+ content: `import { enumOf } from "akanjs/base";
7
+ import { via } from "akanjs/constant";
8
+
9
+ import { WorkHistoryEntry } from "../__scalar/workHistory/workHistory.constant";
10
+
11
+ // ===== task.constant.ts =====
12
+ // Convention: <module>.constant.ts — the data shape layer of an Akan.js database module.
13
+ // Import scalar primitives from akanjs/base; define model layers with via() from akanjs/constant.
14
+ // Scalars are embedded via field([ScalarType], ...) — see WorkHistoryEntry embedding below.
15
+ // Layer order: enum → Input → Object → Light → Full.
16
+ // Input = user-provided fields; Object = Input + system fields + embedded scalars; Light = subset for list views; Full = Object + Light.
17
+ // Registered by akan scan into cnst.ts barrel.
18
+
19
+ export class TaskStatus extends enumOf("taskStatus", [
20
+ "todo",
21
+ "inProgress",
22
+ "completed",
23
+ ] as const) {}
24
+
25
+ export class TaskInput extends via((field) => ({
26
+ title: field(String),
27
+ content: field(String, { default: "" }),
28
+ })) {}
29
+
30
+ // TaskObject embeds WorkHistoryEntry as a list field — the scalar embedding pattern.
31
+ // field([WorkHistoryEntry], { default: [] }) stores a list of scalar objects in the parent document.
32
+ // Each status change (create, start, complete) pushes a new entry into this list.
33
+ export class TaskObject extends via(TaskInput, (field) => ({
34
+ status: field(TaskStatus, { default: "todo" }),
35
+ due: field(Date).optional(),
36
+ workHistory: field([WorkHistoryEntry], { default: [] }),
37
+ })) {}
38
+
39
+ export class LightTask extends via(TaskObject, ["title", "status", "due"] as const, (resolve) => ({})) {}
40
+
41
+ export class Task extends via(TaskObject, LightTask, (resolve) => ({})) {}
42
+
43
+ // ---- Expandable additional fields: ----
44
+ // ===== Add to TaskInput =====
45
+ // - priority: field(TaskPriority, { default: "medium" })
46
+ // - tags: field(String).list().optional()
47
+ // ===== Add to TaskObject =====
48
+ // - assignee: field(ID).optional()
49
+ // - completedAt: field(Date).optional()
50
+ // ===== Add to Task =====
51
+ // isOverdue(): boolean { return this.due && this.due < new Date() }
52
+ `,
53
+ };
54
+ }
@@ -0,0 +1,85 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.dictionary.ts",
6
+ content: `import { modelDictionary } from "akanjs/dictionary";
7
+
8
+ import type { Task, LightTask } from "./task.constant";
9
+ import type { TaskFilter } from "./task.document";
10
+
11
+ // ===== task.dictionary.ts =====
12
+ // Convention: <module>.dictionary.ts — i18n labels and messages for a database module.
13
+ // Uses modelDictionary(["en", "ko"]) from akanjs/dictionary — the framework convention for bilingual module dictionaries.
14
+ // Sections: .of() module name, .model() field labels, .lightModel() list-view labels, .query() filter labels,
15
+ // .sort() labels, .enum() value labels, .error() messages (thrown via Err()), .translate() UI messages (used in store via msg.xxx).
16
+ // Registered by akan scan into dict.ts barrel.
17
+
18
+ export const dictionary = modelDictionary(["en", "ko"])
19
+ .of((t) =>
20
+ t(["Task", "할 일"])
21
+ .desc(["A unit of work to be completed", "완료해야 할 작업 단위"]),
22
+ )
23
+ .model<Task>((t) => ({
24
+ title: t(["Title", "제목"]).desc(["Task title", "할 일 제목"]),
25
+ content: t(["Content", "내용"]).desc(["Detailed task description", "상세 작업 설명"]),
26
+ status: t(["Status", "상태"]).desc(["Current task status", "현재 작업 상태"]),
27
+ due: t(["Due Date", "마감일"]).desc(["Deadline for completion", "완료 마감일"]),
28
+ workHistory: t(["Work History", "작업 이력"]).desc(["Status change log", "상태 변경 기록"]),
29
+ }))
30
+ .lightModel<LightTask>((t) => ({
31
+ title: t(["Title", "제목"]),
32
+ status: t(["Status", "상태"]),
33
+ due: t(["Due", "마감"]),
34
+ }))
35
+ .query<TaskFilter>((fn) => ({
36
+ byStatus: fn(["By Status", "상태별"]).arg((t) => ({
37
+ status: t(["Status", "상태"]),
38
+ })),
39
+ dueBefore: fn(["Due Before", "이전 마감"]).arg((t) => ({
40
+ before: t(["Before Date", "기준일"]),
41
+ })),
42
+ }))
43
+ .sort<TaskFilter>((t) => ({
44
+ byDue: t(["By Due Date", "마감일순"]),
45
+ newest: t(["Newest First", "최신순"]),
46
+ }))
47
+ .enum<TaskStatus>("taskStatus", (t) => ({
48
+ todo: t(["To Do", "할 일"]),
49
+ inProgress: t(["In Progress", "진행 중"]),
50
+ completed: t(["Completed", "완료"]),
51
+ }))
52
+ .error({
53
+ cannotStartFromNonTodo: [
54
+ "Task can only start from todo status",
55
+ "할 일 상태에서만 시작할 수 있습니다",
56
+ ],
57
+ cannotCompleteFromNonInProgress: [
58
+ "Task can only complete from in-progress status",
59
+ "진행 중 상태에서만 완료할 수 있습니다",
60
+ ],
61
+ })
62
+ .translate({
63
+ createTaskLoading: ["Creating task…", "할 일 생성 중…"],
64
+ createTaskSuccess: ["Task created", "할 일이 생성되었습니다"],
65
+ startTaskLoading: ["Starting task…", "작업 시작 중…"],
66
+ startTaskSuccess: ["Task started", "작업이 시작되었습니다"],
67
+ completeTaskLoading: ["Completing task…", "작업 완료 중…"],
68
+ completeTaskSuccess: ["Task completed", "작업이 완료되었습니다"],
69
+ removeTaskLoading: ["Removing task…", "할 일 삭제 중…"],
70
+ removeTaskSuccess: ["Task removed", "할 일이 삭제되었습니다"],
71
+ taskDueLabel: ["Due:", "마감:"],
72
+ taskNoDue: ["No deadline", "마감 없음"],
73
+ taskWorkHistoryTitle: ["Work History", "작업 이력"],
74
+ taskStart: ["Start", "시작"],
75
+ taskComplete: ["Complete", "완료"],
76
+ taskRemove: ["Remove", "삭제"],
77
+ taskNoTasks: ["No tasks yet", "할 일이 없습니다"],
78
+ taskCreateFirst: ["+ Create your first task", "+ 첫 할 일 만들기"],
79
+ taskBackToTasks: ["← Back to Tasks", "← 할 일 목록으로"],
80
+ taskEdit: ["Edit", "수정"],
81
+ taskNew: ["+ New Task", "+ 새 할 일"],
82
+ });
83
+ `,
84
+ };
85
+ }
@@ -0,0 +1,54 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.document.ts",
6
+ content: `import { dayjs } from "akanjs/base";
7
+ import { by, from, into } from "akanjs/document";
8
+
9
+ import { Err } from "../dict";
10
+ import * as cnst from "../cnst";
11
+
12
+ // ===== task.document.ts =====
13
+ // Convention: <module>.document.ts — the database query and persistence layer.
14
+ // Import from/into/by from akanjs/document — the framework convention for Filter, Document, and Model classes.
15
+ // Filter defines query/sort conditions (auto-generates List/Find/Pick/Exists/Count methods via akan scan).
16
+ // Document defines chainable per-document methods (e.g., task.start().save()).
17
+ // Model = into(Document, Filter, cnst.module, ...) — collection-level operations, schema hooks.
18
+ // Registered by akan scan into db.ts barrel.
19
+
20
+ export class TaskFilter extends from(cnst.Task, (filter) => ({
21
+ query: {
22
+ byStatus: filter()
23
+ .arg("status", cnst.TaskStatus)
24
+ .query((status) => ({ status })),
25
+ dueBefore: filter()
26
+ .arg("before", Date)
27
+ .query((before, q) => ({ due: q.lte(before) })),
28
+ },
29
+ sort: {
30
+ byDue: { due: 1 },
31
+ newest: { createdAt: -1 },
32
+ },
33
+ })) {}
34
+
35
+ export class TaskDocument extends by(cnst.Task) {
36
+ start() {
37
+ if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
38
+ this.status = "inProgress";
39
+ this.workHistory.push({ action: cnst.WorkHistoryAction.started, at: dayjs() } as cnst.WorkHistoryEntry);
40
+ return this;
41
+ }
42
+
43
+ complete() {
44
+ if (this.status !== "inProgress") throw new Err("task.error.cannotCompleteFromNonInProgress");
45
+ this.status = "completed";
46
+ this.workHistory.push({ action: cnst.WorkHistoryAction.completed, at: dayjs() } as cnst.WorkHistoryEntry);
47
+ return this;
48
+ }
49
+ }
50
+
51
+ export class TaskModel extends into(TaskDocument, TaskFilter, cnst.task, () => ({})) {}
52
+ `,
53
+ };
54
+ }
@@ -0,0 +1,48 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.service.ts",
6
+ content: `import { dayjs } from "akanjs/base";
7
+ import { serve } from "akanjs/service";
8
+
9
+ import * as cnst from "../cnst";
10
+ import * as db from "../db";
11
+
12
+ // ===== task.service.ts =====
13
+ // Convention: <module>.service.ts — business logic orchestration for a database module.
14
+ // Extends serve(db.<module>, depsCallback) from akanjs/service — binds to the DB model, receives DI deps.
15
+ // Auto-generated by akan scan (do not write manually):
16
+ // getTask(id), createModel(data), updateModel(id, data), removeModel(id),
17
+ // listByStatus(status), searchDocs(text), and all filter+query methods from document.ts.
18
+ // Manual below: lifecycle hooks, custom business logic methods.
19
+ // Registered by akan scan into srv.ts barrel.
20
+
21
+ export class TaskService extends serve(db.task, () => ({})) {
22
+ // Lifecycle hook: runs before every document creation.
23
+ // Auto-injects the initial workHistory entry for the scalar embedding pattern.
24
+ _preCreate(data: cnst.TaskInput): cnst.TaskInput {
25
+ return {
26
+ ...data,
27
+ workHistory: [{ action: cnst.WorkHistoryAction.created, at: dayjs(), note: "" } as cnst.WorkHistoryEntry],
28
+ };
29
+ }
30
+
31
+ async startTask(taskId: string) {
32
+ const task = await this.getTask(taskId);
33
+ return task.start().save();
34
+ }
35
+
36
+ async completeTask(taskId: string) {
37
+ const task = await this.getTask(taskId);
38
+ return task.complete().save();
39
+ }
40
+ }
41
+
42
+ // ---- Expandable additional fields: ----
43
+ // export class TaskService extends serve(db.task, ({ service, use, signal, plug, env, memory }) => ({
44
+ // notiService: service<srv.NotiService>(),
45
+ // })) {
46
+ `,
47
+ };
48
+ }
@@ -0,0 +1,49 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.signal.ts",
6
+ content: `import { endpoint } from "akanjs/signal";
7
+
8
+ import * as cnst from "../cnst";
9
+ import * as srv from "../srv";
10
+
11
+ // ===== task.signal.ts =====
12
+ // Convention: <module>.signal.ts — the API surface for a database module.
13
+ // Extends endpoint(srv.<module>, ...) from akanjs/signal — binds to the service to define query/mutation/pubsub.
14
+ // endpoint() receives: { query, mutation, pubsub, message } — each typed with the model's return type.
15
+ // .param() = URL path parameter, .body() = request body, .search() = query string.
16
+ //
17
+ // Auto-generated by akan scan (do not write manually):
18
+ // viewTask(id) — fetch single task for detail view
19
+ // editTask(id) — fetch task for edit view
20
+ // mergeTask(id, data) — create/update task
21
+ // Manual endpoints below: only define endpoints that need custom business logic.
22
+ // Registered by akan scan into sig.ts barrel.
23
+
24
+ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
25
+ startTask: mutation(cnst.Task)
26
+ .param("taskId", String)
27
+ .exec(async function (taskId) {
28
+ return await this.taskService.startTask(taskId);
29
+ }),
30
+
31
+ completeTask: mutation(cnst.Task)
32
+ .param("taskId", String)
33
+ .exec(async function (taskId) {
34
+ return await this.taskService.completeTask(taskId);
35
+ }),
36
+ })) {}
37
+
38
+ // ---- Expandable additional fields: ----
39
+ // // message: server→client real-time message (WebSocket)
40
+ // taskCreated: message(cnst.Task).room("taskId", String).exec(...),
41
+ // // pubsub: publish-subscribe pattern
42
+ // taskEvent: pubsub(cnst.Task).exec(...),
43
+ // // search: search parameters (optional, URL query string)
44
+ // searchTask: query(cnst.LightTask).search("keyword", String).exec(...),
45
+ // // guards: authentication/authorization guards
46
+ // // export class TaskEndpoint extends endpoint(srv.task, { guards: { root: SignedIn } }, ({ query, mutation }) => ({})) {}
47
+ `,
48
+ };
49
+ }
@@ -0,0 +1,45 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.store.ts",
6
+ content: `import { store } from "akanjs/store";
7
+
8
+ import { fetch, sig, msg } from "../useClient";
9
+
10
+ // ===== task.store.ts =====
11
+ // Convention: <module>.store.ts — client-side state management for a database module.
12
+ // Extends store(sig.<module>, ...) from akanjs/store — binds to the signal to auto-generate model states/actions.
13
+ // Auto-generated by akan scan (do not write manually):
14
+ // taskForm — form state bound to model fields (create + edit)
15
+ // setTitleOnTask, setContentOnTask, setDueOnTask — auto-setters for form fields
16
+ // createTask(data), updateTask(id, data), removeTask(id) — CRUD actions
17
+ // task (cached model), taskLoading, taskModal — model display states
18
+ // Manual below: custom actions wrapping auto-generated fetch with toast feedback.
19
+ // Registered by akan scan into st.ts barrel.
20
+
21
+ export class TaskStore extends store(sig.task, () => ({})) {
22
+ async startTask(taskId: string) {
23
+ msg.loading("task.startTaskLoading", { key: "startTask" });
24
+ const task = await fetch.startTask(taskId);
25
+ msg.success("task.startTaskSuccess", { key: "startTask" });
26
+ this.setTask(task);
27
+ }
28
+
29
+ async completeTask(taskId: string) {
30
+ msg.loading("task.completeTaskLoading", { key: "completeTask" });
31
+ const task = await fetch.completeTask(taskId);
32
+ msg.success("task.completeTaskSuccess", { key: "completeTask" });
33
+ this.setTask(task);
34
+ }
35
+ }
36
+
37
+ // ---- Expandable additional fields: ----
38
+ // Custom state (if needed):
39
+ // export class TaskStore extends store(sig.task, () => ({
40
+ // filterKeyword: "",
41
+ // })) { ... }
42
+ // The auto-generated taskForm handles title, content, due states automatically.
43
+ `,
44
+ };
45
+ }
@@ -0,0 +1,39 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "_index.tsx",
6
+ content: `
7
+ import { fetch, usePage } from "@apps/${dict.appName}/client";
8
+ import { Task } from "@apps/${dict.appName}/lib/task";
9
+ import { Link } from "akanjs/ui";
10
+
11
+ // ===== page/task/[taskId]/_index.tsx =====
12
+ // Convention: Akan.js file-based routing with dynamic segments.
13
+ // [taskId] folder = URL path parameter extracted via useParams() from akanjs/client.
14
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
15
+
16
+ interface PageProps {
17
+ params: { taskId: string };
18
+ }
19
+ export default async function Page({ params: { taskId } }: PageProps) {
20
+ const { l } = usePage();
21
+ const { taskView } = await fetch.viewTask(taskId);
22
+ return (
23
+ <main className="mx-auto max-w-4xl px-6 py-8">
24
+ <div className="mb-6 flex items-center justify-between">
25
+ <a href="/task" className="btn btn-ghost btn-sm">
26
+ {l("task.taskBackToTasks")}
27
+ </a>
28
+ <Link href={\`/task/\${taskId}/edit\`} className="btn btn-primary btn-sm">
29
+ {l("task.taskEdit")}
30
+ </Link>
31
+ </div>
32
+
33
+ <Task.Zone.View className="max-w-2xl" view={taskView} />
34
+ </main>
35
+ );
36
+ }
37
+ `,
38
+ };
39
+ }
@@ -0,0 +1,34 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "edit.tsx",
6
+ content: `import { fetch, Task } from "@apps/${dict.appName}/client";
7
+ import { Load } from "akanjs/ui";
8
+
9
+ // ===== page/task/[taskId]/edit.tsx =====
10
+ // Convention: Server-side edit form page using Load.Edit from akanjs/ui.
11
+ // async Page() — fetches model data on the server via fetch.editTask(params.taskId).
12
+ // The same Template (Task.Template.General) is reused here — no separate edit template needed.
13
+ // Load.Edit with type="form" provides the submit/cancel wrapper with the pre-loaded model.
14
+
15
+ interface PageProps {
16
+ params: { taskId: string };
17
+ }
18
+ export default async function Page({ params: { taskId } }: PageProps) {
19
+ const taskEdit = await fetch.editTask(taskId);
20
+
21
+ return (
22
+ <Load.Edit
23
+ slice={fetch.slice.task}
24
+ edit={taskEdit}
25
+ type="form"
26
+ onSubmit={\`/task/\${params.taskId}\`}
27
+ >
28
+ <Task.Template.General />
29
+ </Load.Edit>
30
+ );
31
+ }
32
+ `,
33
+ };
34
+ }
@@ -0,0 +1,38 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "_index.tsx",
6
+ content: `"use client";
7
+
8
+ import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
9
+ import { Link } from "akanjs/ui";
10
+
11
+ // ===== page/task/_index.tsx =====
12
+ // Convention: Akan.js file-based routing — _index.tsx is the index page for /task.
13
+ // Server-side data loading via loader() at the page level; passes init/view props to Zone components.
14
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
15
+
16
+ export default function Page() {
17
+ const { l } = usePage();
18
+ return (
19
+ <main className="mx-auto max-w-4xl px-6 py-8">
20
+ <div className="mb-6 flex items-center justify-between">
21
+ <div>
22
+ <h1 className="font-extrabold text-3xl text-base-content">{l("task.modelName")}</h1>
23
+ <p className="mt-1 text-base-content/60 text-sm">
24
+ {l("task.modelDesc")}
25
+ </p>
26
+ </div>
27
+ <Link href="/task/new" className="btn btn-primary btn-sm">
28
+ {l("task.taskNew")}
29
+ </Link>
30
+ </div>
31
+
32
+ <Task.Zone.Card className="flex flex-col gap-3" init={fetch.slice.task} />
33
+ </main>
34
+ );
35
+ }
36
+ `,
37
+ };
38
+ }