@reqord/web 0.1.0

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 (138) hide show
  1. package/LICENSE +661 -0
  2. package/next-env.d.ts +6 -0
  3. package/next.config.ts +7 -0
  4. package/package.json +59 -0
  5. package/postcss.config.mjs +7 -0
  6. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
  7. package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
  8. package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
  9. package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
  10. package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
  11. package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
  12. package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
  13. package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
  14. package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
  15. package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
  16. package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
  17. package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
  18. package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
  19. package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
  20. package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
  21. package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
  22. package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
  23. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
  24. package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
  25. package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
  26. package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
  27. package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
  28. package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
  29. package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
  30. package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
  31. package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
  32. package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
  33. package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
  34. package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
  35. package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
  36. package/src/__tests__/lib/dashboard-data.test.ts +334 -0
  37. package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
  38. package/src/__tests__/lib/gantt-data.test.ts +299 -0
  39. package/src/__tests__/lib/graph-data.test.ts +309 -0
  40. package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
  41. package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
  42. package/src/__tests__/lib/reqord-root.test.ts +31 -0
  43. package/src/__tests__/lib/specification-file.test.ts +63 -0
  44. package/src/__tests__/lib/tasks-data.test.ts +104 -0
  45. package/src/app/dashboard/loading.tsx +21 -0
  46. package/src/app/dashboard/page.tsx +50 -0
  47. package/src/app/error.tsx +22 -0
  48. package/src/app/feedback/loading.tsx +13 -0
  49. package/src/app/feedback/page.tsx +48 -0
  50. package/src/app/globals.css +2 -0
  51. package/src/app/graph/page.tsx +32 -0
  52. package/src/app/layout.tsx +25 -0
  53. package/src/app/page.tsx +5 -0
  54. package/src/app/requirements/[id]/edit/page.tsx +40 -0
  55. package/src/app/requirements/[id]/loading.tsx +14 -0
  56. package/src/app/requirements/[id]/not-found.tsx +18 -0
  57. package/src/app/requirements/[id]/page.tsx +43 -0
  58. package/src/app/requirements/loading.tsx +13 -0
  59. package/src/app/requirements/new/page.tsx +14 -0
  60. package/src/app/requirements/page.tsx +35 -0
  61. package/src/app/specifications/[id]/loading.tsx +14 -0
  62. package/src/app/specifications/[id]/not-found.tsx +18 -0
  63. package/src/app/specifications/[id]/page.tsx +52 -0
  64. package/src/app/specifications/loading.tsx +13 -0
  65. package/src/app/specifications/page.tsx +42 -0
  66. package/src/components/dashboard/critical-path-display.tsx +76 -0
  67. package/src/components/dashboard/progress-bar.tsx +45 -0
  68. package/src/components/dashboard/progress-section.tsx +57 -0
  69. package/src/components/dashboard/project-health.tsx +35 -0
  70. package/src/components/dashboard/status-card.tsx +27 -0
  71. package/src/components/dashboard/status-cards.tsx +28 -0
  72. package/src/components/dashboard/warning-alert.tsx +33 -0
  73. package/src/components/dashboard/warning-alerts.tsx +24 -0
  74. package/src/components/feedback/feedback-badge.tsx +48 -0
  75. package/src/components/feedback/feedback-client-view.tsx +38 -0
  76. package/src/components/feedback/feedback-filters.tsx +86 -0
  77. package/src/components/feedback/feedback-linked-items.tsx +93 -0
  78. package/src/components/feedback/feedback-list.tsx +40 -0
  79. package/src/components/feedback/feedback-table.tsx +115 -0
  80. package/src/components/gantt/gantt-bar.tsx +65 -0
  81. package/src/components/gantt/gantt-chart.tsx +88 -0
  82. package/src/components/gantt/gantt-constants.ts +15 -0
  83. package/src/components/gantt/gantt-critical-path.tsx +38 -0
  84. package/src/components/gantt/gantt-group.tsx +25 -0
  85. package/src/components/gantt/gantt-header.tsx +47 -0
  86. package/src/components/gantt/gantt-legend.tsx +26 -0
  87. package/src/components/graph/dag-layout.ts +131 -0
  88. package/src/components/graph/dependency-graph.tsx +88 -0
  89. package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
  90. package/src/components/graph/drilldown-graph.tsx +45 -0
  91. package/src/components/graph/edge-styles.ts +16 -0
  92. package/src/components/graph/graph-loader.tsx +25 -0
  93. package/src/components/graph/graph-page-client.tsx +98 -0
  94. package/src/components/graph/issue-node.tsx +46 -0
  95. package/src/components/graph/multi-level-graph.tsx +91 -0
  96. package/src/components/graph/requirement-node.tsx +69 -0
  97. package/src/components/graph/specification-node.tsx +39 -0
  98. package/src/components/requirement/delete-button.tsx +46 -0
  99. package/src/components/requirement/dependency-editor.tsx +79 -0
  100. package/src/components/requirement/markdown-editor.tsx +47 -0
  101. package/src/components/requirement/markdown-renderer.tsx +12 -0
  102. package/src/components/requirement/requirement-detail.tsx +228 -0
  103. package/src/components/requirement/requirement-form.tsx +390 -0
  104. package/src/components/requirement/requirement-table.tsx +203 -0
  105. package/src/components/requirement/requirement-tabs.tsx +65 -0
  106. package/src/components/requirement/success-criteria-editor.tsx +53 -0
  107. package/src/components/specification/spec-detail.tsx +103 -0
  108. package/src/components/specification/spec-tabs.tsx +66 -0
  109. package/src/components/specification/specification-table.tsx +193 -0
  110. package/src/components/specification/tab-coverage.tsx +52 -0
  111. package/src/components/specification/tab-design.tsx +16 -0
  112. package/src/components/specification/tab-history.tsx +61 -0
  113. package/src/components/specification/tab-issues.tsx +111 -0
  114. package/src/components/specification/tab-research.tsx +16 -0
  115. package/src/components/ui/badge.tsx +64 -0
  116. package/src/components/ui/nav.tsx +49 -0
  117. package/src/components/ui/tabs.tsx +39 -0
  118. package/src/lib/actions.ts +222 -0
  119. package/src/lib/dashboard-data.ts +224 -0
  120. package/src/lib/data.ts +21 -0
  121. package/src/lib/drilldown-graph-data.ts +98 -0
  122. package/src/lib/feedback-data.ts +33 -0
  123. package/src/lib/feedback-repository.ts +6 -0
  124. package/src/lib/file-system.ts +167 -0
  125. package/src/lib/gantt-data.ts +168 -0
  126. package/src/lib/get-repository.ts +43 -0
  127. package/src/lib/graph-data.ts +161 -0
  128. package/src/lib/id-generator.ts +23 -0
  129. package/src/lib/local-feedback-repository.ts +36 -0
  130. package/src/lib/local-repository.ts +78 -0
  131. package/src/lib/local-specification-repository.ts +61 -0
  132. package/src/lib/repository.ts +11 -0
  133. package/src/lib/reqord-root.ts +33 -0
  134. package/src/lib/specification-data.ts +28 -0
  135. package/src/lib/specification-file.ts +12 -0
  136. package/src/lib/specification-repository.ts +8 -0
  137. package/src/lib/tasks-data.ts +32 -0
  138. package/tsconfig.json +27 -0
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type { TaskEntry, TasksIndex } from "@reqord/shared";
3
+
4
+ // Mock the file-system module
5
+ vi.mock("../../lib/file-system.js", () => ({
6
+ joinPath: vi.fn((...args: string[]) => args.join("/")),
7
+ readYAML: vi.fn(),
8
+ }));
9
+
10
+ // Mock reqord-root
11
+ vi.mock("../../lib/reqord-root.js", () => ({
12
+ getReqordRoot: vi.fn(() => "/mock/root"),
13
+ }));
14
+
15
+ const mockTaskEntry: TaskEntry = {
16
+ number: 1,
17
+ title: "Test Task",
18
+ url: "https://github.com/owner/repo/issues/1",
19
+ status: "open",
20
+ linkedTo: { specifications: ["spec-000001"] },
21
+ syncedAt: "2026-02-22T00:00:00.000Z",
22
+ };
23
+
24
+ describe("tasks-data", () => {
25
+ beforeEach(() => {
26
+ vi.resetModules();
27
+ });
28
+
29
+ describe("loadTasksYaml", () => {
30
+ it("returns tasks index from loaded tasks.yaml", async () => {
31
+ const { readYAML } = await import("../../lib/file-system.js");
32
+
33
+ const mockIndex: TasksIndex = {
34
+ title: "Tasks",
35
+ tasks: [mockTaskEntry],
36
+ };
37
+
38
+ vi.mocked(readYAML).mockResolvedValue(mockIndex);
39
+
40
+ const { loadTasksYaml } = await import("../../lib/tasks-data.js");
41
+ const result = await loadTasksYaml();
42
+
43
+ expect(result.tasks).toHaveLength(1);
44
+ expect(result.tasks[0]).toEqual(mockTaskEntry);
45
+ });
46
+
47
+ it("returns empty tasks index when tasks.yaml is missing or invalid", async () => {
48
+ const { readYAML } = await import("../../lib/file-system.js");
49
+
50
+ vi.mocked(readYAML).mockRejectedValue(new Error("File not found"));
51
+
52
+ const { loadTasksYaml } = await import("../../lib/tasks-data.js");
53
+ const result = await loadTasksYaml();
54
+
55
+ expect(result.tasks).toEqual([]);
56
+ });
57
+ });
58
+
59
+ describe("getAllTasks", () => {
60
+ it("returns tasks array from loaded TasksIndex", async () => {
61
+ const { readYAML } = await import("../../lib/file-system.js");
62
+
63
+ const mockIndex: TasksIndex = {
64
+ title: "Tasks",
65
+ tasks: [mockTaskEntry],
66
+ };
67
+
68
+ vi.mocked(readYAML).mockResolvedValue(mockIndex);
69
+
70
+ const { getAllTasks } = await import("../../lib/tasks-data.js");
71
+ const result = await getAllTasks();
72
+
73
+ expect(result).toHaveLength(1);
74
+ expect(result[0]).toEqual(mockTaskEntry);
75
+ });
76
+
77
+ it("returns empty array when tasks.yaml has no tasks", async () => {
78
+ const { readYAML } = await import("../../lib/file-system.js");
79
+
80
+ const mockIndex: TasksIndex = {
81
+ title: "Tasks",
82
+ tasks: [],
83
+ };
84
+
85
+ vi.mocked(readYAML).mockResolvedValue(mockIndex);
86
+
87
+ const { getAllTasks } = await import("../../lib/tasks-data.js");
88
+ const result = await getAllTasks();
89
+
90
+ expect(result).toEqual([]);
91
+ });
92
+
93
+ it("returns empty array when tasks.yaml is missing or invalid", async () => {
94
+ const { readYAML } = await import("../../lib/file-system.js");
95
+
96
+ vi.mocked(readYAML).mockRejectedValue(new Error("File not found"));
97
+
98
+ const { getAllTasks } = await import("../../lib/tasks-data.js");
99
+ const result = await getAllTasks();
100
+
101
+ expect(result).toEqual([]);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,21 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
4
+ {/* Health Score Skeleton */}
5
+ <div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
6
+
7
+ {/* Progress Section Skeleton */}
8
+ <div className="grid gap-6 md:grid-cols-3">
9
+ <div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
10
+ <div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
11
+ <div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
12
+ </div>
13
+
14
+ {/* Status Cards Skeleton */}
15
+ <div className="grid gap-6 md:grid-cols-2">
16
+ <div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
17
+ <div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
18
+ </div>
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,50 @@
1
+ import { Suspense } from "react";
2
+ import { getDashboardData } from "@/lib/dashboard-data";
3
+ import { ProjectHealth } from "@/components/dashboard/project-health";
4
+ import { ProgressSection } from "@/components/dashboard/progress-section";
5
+ import { StatusCards } from "@/components/dashboard/status-cards";
6
+ import { WarningAlerts } from "@/components/dashboard/warning-alerts";
7
+ import { CriticalPathDisplay } from "@/components/dashboard/critical-path-display";
8
+ import Loading from "./loading";
9
+
10
+ async function DashboardContent() {
11
+ const data = await getDashboardData();
12
+
13
+ return (
14
+ <div className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
15
+ <h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
16
+
17
+ {/* Project Health */}
18
+ <ProjectHealth score={data.healthScore} />
19
+
20
+ {/* Progress Section */}
21
+ <ProgressSection
22
+ requirements={data.requirements}
23
+ specifications={data.specifications}
24
+ issues={data.issues}
25
+ />
26
+
27
+ {/* Status Cards */}
28
+ <StatusCards
29
+ requirements={data.requirements}
30
+ specifications={data.specifications}
31
+ />
32
+
33
+ {/* Warnings */}
34
+ {data.warnings.length > 0 && <WarningAlerts warnings={data.warnings} />}
35
+
36
+ {/* Critical Path */}
37
+ {data.criticalPath !== null && (
38
+ <CriticalPathDisplay items={data.criticalPath} />
39
+ )}
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export default function DashboardPage() {
45
+ return (
46
+ <Suspense fallback={<Loading />}>
47
+ <DashboardContent />
48
+ </Suspense>
49
+ );
50
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ export default function Error({
4
+ error,
5
+ reset,
6
+ }: {
7
+ error: Error & { digest?: string };
8
+ reset: () => void;
9
+ }) {
10
+ return (
11
+ <div className="flex flex-col items-center justify-center py-20">
12
+ <h2 className="text-2xl font-bold text-red-600">Something went wrong</h2>
13
+ <p className="mt-2 text-gray-600">{error.message}</p>
14
+ <button
15
+ onClick={reset}
16
+ className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
17
+ >
18
+ Try again
19
+ </button>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,13 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="animate-pulse space-y-4">
4
+ <div className="h-8 w-48 rounded bg-gray-200" />
5
+ <div className="h-10 w-full rounded bg-gray-200" />
6
+ <div className="space-y-2">
7
+ {Array.from({ length: 8 }, (_, i) => (
8
+ <div key={i} className="h-12 w-full rounded bg-gray-200" />
9
+ ))}
10
+ </div>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,48 @@
1
+ import { Suspense } from "react";
2
+ import { getAllFeedbacks } from "@/lib/feedback-data";
3
+ import { getAllRequirements } from "@/lib/data";
4
+ import { getAllSpecifications } from "@/lib/specification-data";
5
+ import { FeedbackClientView } from "@/components/feedback/feedback-client-view";
6
+ import Loading from "./loading";
7
+
8
+ export const metadata = {
9
+ title: "Feedback - Reqord",
10
+ };
11
+
12
+ export const dynamic = "force-dynamic";
13
+
14
+ async function FeedbackContent() {
15
+ const [feedbacks, requirements, specifications] = await Promise.all([
16
+ getAllFeedbacks(),
17
+ getAllRequirements(),
18
+ getAllSpecifications(),
19
+ ]);
20
+
21
+ const reqTitleMap: Record<string, string> = Object.fromEntries(
22
+ requirements.map((r) => [r.id, r.title]),
23
+ );
24
+ const specTitleMap: Record<string, string> = Object.fromEntries(
25
+ specifications.map((s) => [s.id, s.title ?? s.id]),
26
+ );
27
+
28
+ return (
29
+ <FeedbackClientView
30
+ feedbacks={feedbacks}
31
+ requirementTitles={reqTitleMap}
32
+ specificationTitles={specTitleMap}
33
+ />
34
+ );
35
+ }
36
+
37
+ export default function FeedbackPage() {
38
+ return (
39
+ <div>
40
+ <div className="mb-6">
41
+ <h1 className="text-2xl font-bold">Feedback</h1>
42
+ </div>
43
+ <Suspense fallback={<Loading />}>
44
+ <FeedbackContent />
45
+ </Suspense>
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,2 @@
1
+ @import "tailwindcss";
2
+ @plugin "@tailwindcss/typography";
@@ -0,0 +1,32 @@
1
+ import { getAllRequirements } from "@/lib/data";
2
+ import { getAllSpecifications } from "@/lib/specification-data";
3
+ import { getAllTasks } from "@/lib/tasks-data";
4
+ import { GraphPageClient } from "@/components/graph/graph-page-client";
5
+
6
+ export const metadata = {
7
+ title: "Dependency Graph - Reqord",
8
+ };
9
+
10
+ export const dynamic = "force-dynamic";
11
+
12
+ export default async function GraphPage() {
13
+ const [requirements, specifications, tasks] = await Promise.all([
14
+ getAllRequirements(),
15
+ getAllSpecifications(),
16
+ getAllTasks(),
17
+ ]);
18
+
19
+ const specCountMap: Record<string, number> = {};
20
+ for (const spec of specifications) {
21
+ specCountMap[spec.requirementId] = (specCountMap[spec.requirementId] ?? 0) + 1;
22
+ }
23
+
24
+ return (
25
+ <GraphPageClient
26
+ requirements={requirements}
27
+ specifications={specifications}
28
+ specCountMap={specCountMap}
29
+ tasks={tasks}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,25 @@
1
+ import type { Metadata } from "next";
2
+ import { Nav } from "@/components/ui/nav";
3
+ import "./globals.css";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Reqord",
7
+ description: "Requirements management UI",
8
+ };
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: {
13
+ children: React.ReactNode;
14
+ }) {
15
+ return (
16
+ <html lang="ja">
17
+ <body className="bg-gray-50 text-gray-900 antialiased">
18
+ <Nav />
19
+ <main className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
20
+ {children}
21
+ </main>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function Home() {
4
+ redirect("/requirements");
5
+ }
@@ -0,0 +1,40 @@
1
+ import { notFound } from "next/navigation";
2
+ import {
3
+ getAllRequirements,
4
+ getRequirementById,
5
+ getRequirementDescription,
6
+ } from "@/lib/data";
7
+ import { RequirementForm } from "@/components/requirement/requirement-form";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
12
+ const { id } = await params;
13
+ return { title: `Edit ${id} - Reqord` };
14
+ }
15
+
16
+ export default async function EditPage({
17
+ params,
18
+ }: {
19
+ params: Promise<{ id: string }>;
20
+ }) {
21
+ const { id } = await params;
22
+ const [requirement, description, allRequirements] = await Promise.all([
23
+ getRequirementById(id),
24
+ getRequirementDescription(id),
25
+ getAllRequirements(),
26
+ ]);
27
+
28
+ if (!requirement) {
29
+ notFound();
30
+ }
31
+
32
+ return (
33
+ <RequirementForm
34
+ mode="edit"
35
+ requirement={requirement}
36
+ description={description}
37
+ allRequirements={allRequirements}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,14 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="animate-pulse space-y-4">
4
+ <div className="h-6 w-32 rounded bg-gray-200" />
5
+ <div className="h-8 w-96 rounded bg-gray-200" />
6
+ <div className="flex gap-2">
7
+ <div className="h-6 w-16 rounded-full bg-gray-200" />
8
+ <div className="h-6 w-16 rounded-full bg-gray-200" />
9
+ </div>
10
+ <div className="h-24 w-full rounded bg-gray-200" />
11
+ <div className="h-48 w-full rounded bg-gray-200" />
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,18 @@
1
+ import Link from "next/link";
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <div className="flex flex-col items-center justify-center py-20">
6
+ <h2 className="text-2xl font-bold text-gray-900">Requirement Not Found</h2>
7
+ <p className="mt-2 text-gray-600">
8
+ The requirement you&apos;re looking for does not exist.
9
+ </p>
10
+ <Link
11
+ href="/requirements"
12
+ className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
13
+ >
14
+ Back to Requirements
15
+ </Link>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,43 @@
1
+ import { notFound } from "next/navigation";
2
+ import { getRequirementById, getRequirementDescription } from "@/lib/data";
3
+ import { getSpecificationsByRequirementId } from "@/lib/specification-data";
4
+ import { findUnresolvedByArtifactId } from "@/lib/feedback-data";
5
+ import { RequirementDetail } from "@/components/requirement/requirement-detail";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params;
11
+ const requirement = await getRequirementById(id);
12
+ if (!requirement) {
13
+ return { title: "Not Found - Reqord" };
14
+ }
15
+ return { title: `${requirement.id}: ${requirement.title} - Reqord` };
16
+ }
17
+
18
+ export default async function RequirementPage({
19
+ params,
20
+ }: {
21
+ params: Promise<{ id: string }>;
22
+ }) {
23
+ const { id } = await params;
24
+ const [requirement, description, specifications, feedbacks] = await Promise.all([
25
+ getRequirementById(id),
26
+ getRequirementDescription(id),
27
+ getSpecificationsByRequirementId(id),
28
+ findUnresolvedByArtifactId(id),
29
+ ]);
30
+
31
+ if (!requirement) {
32
+ notFound();
33
+ }
34
+
35
+ return (
36
+ <RequirementDetail
37
+ requirement={requirement}
38
+ description={description}
39
+ specifications={specifications}
40
+ feedbacks={feedbacks}
41
+ />
42
+ );
43
+ }
@@ -0,0 +1,13 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="animate-pulse space-y-4">
4
+ <div className="h-8 w-48 rounded bg-gray-200" />
5
+ <div className="h-10 w-full rounded bg-gray-200" />
6
+ <div className="space-y-2">
7
+ {Array.from({ length: 8 }, (_, i) => (
8
+ <div key={i} className="h-12 w-full rounded bg-gray-200" />
9
+ ))}
10
+ </div>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,14 @@
1
+ import { getAllRequirements } from "@/lib/data";
2
+ import { RequirementForm } from "@/components/requirement/requirement-form";
3
+
4
+ export const metadata = {
5
+ title: "New Requirement - Reqord",
6
+ };
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export default async function NewPage() {
11
+ const allRequirements = await getAllRequirements();
12
+
13
+ return <RequirementForm mode="create" allRequirements={allRequirements} />;
14
+ }
@@ -0,0 +1,35 @@
1
+ import { Suspense } from "react";
2
+ import Link from "next/link";
3
+ import { getAllRequirements } from "@/lib/data";
4
+ import { RequirementTable } from "@/components/requirement/requirement-table";
5
+ import Loading from "./loading";
6
+
7
+ export const metadata = {
8
+ title: "Requirements - Reqord",
9
+ };
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ async function RequirementList() {
14
+ const requirements = await getAllRequirements();
15
+ return <RequirementTable requirements={requirements} />;
16
+ }
17
+
18
+ export default function RequirementsPage() {
19
+ return (
20
+ <div>
21
+ <div className="mb-6 flex items-center justify-between">
22
+ <h1 className="text-2xl font-bold">Requirements</h1>
23
+ <Link
24
+ href="/requirements/new"
25
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
26
+ >
27
+ New Requirement
28
+ </Link>
29
+ </div>
30
+ <Suspense fallback={<Loading />}>
31
+ <RequirementList />
32
+ </Suspense>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,14 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="animate-pulse space-y-4">
4
+ <div className="h-6 w-32 rounded bg-gray-200" />
5
+ <div className="h-8 w-96 rounded bg-gray-200" />
6
+ <div className="flex gap-2">
7
+ <div className="h-6 w-16 rounded-full bg-gray-200" />
8
+ <div className="h-6 w-16 rounded-full bg-gray-200" />
9
+ </div>
10
+ <div className="h-24 w-full rounded bg-gray-200" />
11
+ <div className="h-48 w-full rounded bg-gray-200" />
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,18 @@
1
+ import Link from "next/link";
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <div className="flex flex-col items-center justify-center py-20">
6
+ <h2 className="text-2xl font-bold text-gray-900">Specification Not Found</h2>
7
+ <p className="mt-2 text-gray-600">
8
+ The specification you&apos;re looking for does not exist.
9
+ </p>
10
+ <Link
11
+ href="/specifications"
12
+ className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
13
+ >
14
+ Back to Specifications
15
+ </Link>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,52 @@
1
+ import { notFound } from "next/navigation";
2
+ import { getSpecificationById } from "@/lib/specification-data";
3
+ import { getRequirementById } from "@/lib/data";
4
+ import { loadSpecFile } from "@/lib/specification-file";
5
+ import { findUnresolvedByArtifactId } from "@/lib/feedback-data";
6
+ import { SpecDetail } from "@/components/specification/spec-detail";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params;
12
+ const specification = await getSpecificationById(id);
13
+ if (!specification) {
14
+ return { title: "Not Found - Reqord" };
15
+ }
16
+ const title = specification.title
17
+ ? `${specification.id}: ${specification.title} - Reqord`
18
+ : `${specification.id} - Reqord`;
19
+ return { title };
20
+ }
21
+
22
+ export default async function SpecificationPage({
23
+ params,
24
+ }: {
25
+ params: Promise<{ id: string }>;
26
+ }) {
27
+ const { id } = await params;
28
+ const [specification, design, research] = await Promise.all([
29
+ getSpecificationById(id),
30
+ loadSpecFile(id, "design.md"),
31
+ loadSpecFile(id, "research.md"),
32
+ ]);
33
+
34
+ if (!specification) {
35
+ notFound();
36
+ }
37
+
38
+ const [requirement, feedbacks] = await Promise.all([
39
+ getRequirementById(specification.requirementId),
40
+ findUnresolvedByArtifactId(id),
41
+ ]);
42
+
43
+ return (
44
+ <SpecDetail
45
+ specification={specification}
46
+ design={design}
47
+ research={research}
48
+ requirement={requirement}
49
+ feedbacks={feedbacks}
50
+ />
51
+ );
52
+ }
@@ -0,0 +1,13 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="animate-pulse space-y-4">
4
+ <div className="h-8 w-48 rounded bg-gray-200" />
5
+ <div className="h-10 w-full rounded bg-gray-200" />
6
+ <div className="space-y-2">
7
+ {Array.from({ length: 8 }, (_, i) => (
8
+ <div key={i} className="h-12 w-full rounded bg-gray-200" />
9
+ ))}
10
+ </div>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,42 @@
1
+ import { Suspense } from "react";
2
+ import { getAllSpecifications } from "@/lib/specification-data";
3
+ import { getAllRequirements } from "@/lib/data";
4
+ import { SpecificationTable } from "@/components/specification/specification-table";
5
+ import Loading from "./loading";
6
+
7
+ export const metadata = {
8
+ title: "Specifications - Reqord",
9
+ };
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ async function SpecificationList() {
14
+ const [specifications, requirements] = await Promise.all([
15
+ getAllSpecifications(),
16
+ getAllRequirements(),
17
+ ]);
18
+
19
+ const requirementMap = Object.fromEntries(
20
+ requirements.map((r) => [r.id, r.title]),
21
+ );
22
+
23
+ return (
24
+ <SpecificationTable
25
+ specifications={specifications}
26
+ requirementTitleMap={requirementMap}
27
+ />
28
+ );
29
+ }
30
+
31
+ export default function SpecificationsPage() {
32
+ return (
33
+ <div>
34
+ <div className="mb-6">
35
+ <h1 className="text-2xl font-bold">Specifications</h1>
36
+ </div>
37
+ <Suspense fallback={<Loading />}>
38
+ <SpecificationList />
39
+ </Suspense>
40
+ </div>
41
+ );
42
+ }