teamshare-bridge 0.3.0 → 0.4.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 (123) hide show
  1. package/dist/bridge/daemon.js +36 -0
  2. package/dist/bridge/daemon.js.map +1 -1
  3. package/dist/bridge/local-server.d.ts +1 -1
  4. package/dist/bridge/spawn.d.ts +2 -0
  5. package/dist/bridge/spawn.js +17 -0
  6. package/dist/bridge/spawn.js.map +1 -1
  7. package/dist/cli/commands/audit.d.ts +1 -0
  8. package/dist/cli/commands/audit.js +75 -0
  9. package/dist/cli/commands/audit.js.map +1 -0
  10. package/dist/cli/commands/build.js +9 -6
  11. package/dist/cli/commands/build.js.map +1 -1
  12. package/dist/cli/commands/doc.js +1 -1
  13. package/dist/cli/commands/doc.js.map +1 -1
  14. package/dist/cli/commands/run.js +3 -2
  15. package/dist/cli/commands/run.js.map +1 -1
  16. package/dist/cli/commands/scaffold.d.ts +1 -0
  17. package/dist/cli/commands/scaffold.js +111 -0
  18. package/dist/cli/commands/scaffold.js.map +1 -0
  19. package/dist/cli/commands/worker.d.ts +2 -0
  20. package/dist/cli/commands/worker.js +110 -0
  21. package/dist/cli/commands/worker.js.map +1 -0
  22. package/dist/cli/index.js +16 -0
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/lib/api.d.ts +22 -3
  25. package/dist/lib/api.js +9 -2
  26. package/dist/lib/api.js.map +1 -1
  27. package/dist/lib/audit/docs.d.ts +5 -0
  28. package/dist/lib/audit/docs.js +52 -0
  29. package/dist/lib/audit/docs.js.map +1 -0
  30. package/dist/lib/audit/errors.d.ts +15 -0
  31. package/dist/lib/audit/errors.js +154 -0
  32. package/dist/lib/audit/errors.js.map +1 -0
  33. package/dist/lib/audit/gates.d.ts +2 -0
  34. package/dist/lib/audit/gates.js +57 -0
  35. package/dist/lib/audit/gates.js.map +1 -0
  36. package/dist/lib/audit/modularity.d.ts +13 -0
  37. package/dist/lib/audit/modularity.js +200 -0
  38. package/dist/lib/audit/modularity.js.map +1 -0
  39. package/dist/lib/audit/quality.d.ts +9 -0
  40. package/dist/lib/audit/quality.js +128 -0
  41. package/dist/lib/audit/quality.js.map +1 -0
  42. package/dist/lib/audit/rbac.d.ts +14 -0
  43. package/dist/lib/audit/rbac.js +139 -0
  44. package/dist/lib/audit/rbac.js.map +1 -0
  45. package/dist/lib/audit/reporter.d.ts +5 -0
  46. package/dist/lib/audit/reporter.js +107 -0
  47. package/dist/lib/audit/reporter.js.map +1 -0
  48. package/dist/lib/audit/runner.d.ts +2 -0
  49. package/dist/lib/audit/runner.js +150 -0
  50. package/dist/lib/audit/runner.js.map +1 -0
  51. package/dist/lib/audit/security.d.ts +9 -0
  52. package/dist/lib/audit/security.js +151 -0
  53. package/dist/lib/audit/security.js.map +1 -0
  54. package/dist/lib/audit/types.d.ts +67 -0
  55. package/dist/lib/audit/types.js +26 -0
  56. package/dist/lib/audit/types.js.map +1 -0
  57. package/dist/lib/audit/ux.d.ts +9 -0
  58. package/dist/lib/audit/ux.js +112 -0
  59. package/dist/lib/audit/ux.js.map +1 -0
  60. package/dist/lib/config.d.ts +1 -1
  61. package/dist/lib/config.js +1 -1
  62. package/dist/lib/llm.js +1 -0
  63. package/dist/lib/llm.js.map +1 -1
  64. package/dist/lib/lock.d.ts +1 -1
  65. package/dist/lib/scaffold/renderer.d.ts +7 -0
  66. package/dist/lib/scaffold/renderer.js +37 -0
  67. package/dist/lib/scaffold/renderer.js.map +1 -0
  68. package/dist/lib/scaffold/structure.d.ts +3 -0
  69. package/dist/lib/scaffold/structure.js +73 -0
  70. package/dist/lib/scaffold/structure.js.map +1 -0
  71. package/dist/lib/scaffold/types.d.ts +34 -0
  72. package/dist/lib/scaffold/types.js +6 -0
  73. package/dist/lib/scaffold/types.js.map +1 -0
  74. package/dist/lib/scaffold/verify.d.ts +9 -0
  75. package/dist/lib/scaffold/verify.js +48 -0
  76. package/dist/lib/scaffold/verify.js.map +1 -0
  77. package/dist/lib/skills.d.ts +2 -2
  78. package/dist/lib/skills.js +2 -1
  79. package/dist/lib/skills.js.map +1 -1
  80. package/package.json +3 -2
  81. package/skills/teamshare-orchestrator-ask/SKILL.md +14 -0
  82. package/skills/teamshare-scaffold/SKILL.md +133 -0
  83. package/templates/backend/.env.example.tpl +13 -0
  84. package/templates/backend/nest-cli.json.tpl +8 -0
  85. package/templates/backend/package.json.tpl +41 -0
  86. package/templates/backend/prisma/schema.prisma.tpl +39 -0
  87. package/templates/backend/src/app.module.ts.tpl +24 -0
  88. package/templates/backend/src/common/filters/all-exceptions.filter.ts.tpl +59 -0
  89. package/templates/backend/src/common/guards/jwt-auth.guard.ts.tpl +27 -0
  90. package/templates/backend/src/common/guards/rbac.guard.ts.tpl +48 -0
  91. package/templates/backend/src/common/interceptors/transform.interceptor.ts.tpl +44 -0
  92. package/templates/backend/src/common/pipes/zod-validation.pipe.ts.tpl +29 -0
  93. package/templates/backend/src/common/swagger/envelope.schemas.ts.tpl +53 -0
  94. package/templates/backend/src/common/swagger/error.schemas.ts.tpl +62 -0
  95. package/templates/backend/src/main.ts.tpl +27 -0
  96. package/templates/backend/src/modules/example/example.controller.ts.tpl +102 -0
  97. package/templates/backend/src/modules/example/example.dto.ts.tpl +14 -0
  98. package/templates/backend/src/modules/example/example.module.ts.tpl +10 -0
  99. package/templates/backend/src/modules/example/example.service.ts.tpl +45 -0
  100. package/templates/backend/tsconfig.json.tpl +26 -0
  101. package/templates/common/.gitignore.tpl +42 -0
  102. package/templates/common/AGENTS.md.tpl +58 -0
  103. package/templates/common/docs/adr/README.md +25 -0
  104. package/templates/common/docs/api-contract.md.tpl +72 -0
  105. package/templates/web/next.config.ts.tpl +7 -0
  106. package/templates/web/package.json.tpl +37 -0
  107. package/templates/web/src/app/globals.css.tpl +48 -0
  108. package/templates/web/src/app/layout.tsx.tpl +25 -0
  109. package/templates/web/src/components/shared/app-button.tsx.tpl +59 -0
  110. package/templates/web/src/components/shared/app-empty-state.tsx.tpl +39 -0
  111. package/templates/web/src/components/shared/app-list-toolbar.tsx.tpl +63 -0
  112. package/templates/web/src/components/shared/app-skeleton.tsx.tpl +14 -0
  113. package/templates/web/src/components/shared/app-table.tsx.tpl +128 -0
  114. package/templates/web/src/components/shared/index.ts.tpl +5 -0
  115. package/templates/web/src/hooks/use-list-state.ts.tpl +72 -0
  116. package/templates/web/src/hooks/use-url-tab.ts.tpl +25 -0
  117. package/templates/web/src/lib/api/client.ts.tpl +123 -0
  118. package/templates/web/src/lib/providers.tsx.tpl +35 -0
  119. package/templates/web/src/lib/query-keys.ts.tpl +21 -0
  120. package/templates/web/src/lib/utils.ts.tpl +7 -0
  121. package/templates/web/src/lib/validation/schemas.ts.tpl +15 -0
  122. package/templates/web/src/modules/example/views/example-list-view.tsx.tpl +98 -0
  123. package/templates/web/tsconfig.json.tpl +23 -0
@@ -0,0 +1,39 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface AppEmptyStateProps {
5
+ icon?: ReactNode;
6
+ title: string;
7
+ description?: string;
8
+ action?: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function AppEmptyState({
13
+ icon,
14
+ title,
15
+ description,
16
+ action,
17
+ className,
18
+ }: AppEmptyStateProps) {
19
+ return (
20
+ <div
21
+ data-slot="app-empty-state"
22
+ className={cn(
23
+ "flex flex-col items-center justify-center py-12 text-center",
24
+ className
25
+ )}
26
+ >
27
+ {icon && (
28
+ <div className="mb-4 text-tertiary [&_svg]:h-12 [&_svg]:w-12">
29
+ {icon}
30
+ </div>
31
+ )}
32
+ <h3 className="text-lg font-semibold text-primary">{title}</h3>
33
+ {description && (
34
+ <p className="mt-1 max-w-sm text-sm text-secondary">{description}</p>
35
+ )}
36
+ {action && <div className="mt-4">{action}</div>}
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { Search } from "lucide-react";
4
+ import { type ReactNode } from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface AppListToolbarProps {
8
+ search?: string;
9
+ onSearchChange?: (value: string) => void;
10
+ searchPlaceholder?: string;
11
+ filters?: ReactNode;
12
+ actions?: ReactNode;
13
+ resultCount?: number;
14
+ className?: string;
15
+ }
16
+
17
+ export function AppListToolbar({
18
+ search,
19
+ onSearchChange,
20
+ searchPlaceholder = "Search...",
21
+ filters,
22
+ actions,
23
+ resultCount,
24
+ className,
25
+ }: AppListToolbarProps) {
26
+ return (
27
+ <div
28
+ data-slot="app-list-toolbar"
29
+ className={cn(
30
+ "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
31
+ className
32
+ )}
33
+ >
34
+ <div className="flex flex-1 items-center gap-3">
35
+ {onSearchChange && (
36
+ <div className="relative flex-1 max-w-sm">
37
+ <Search
38
+ className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-tertiary"
39
+ variant="Outline"
40
+ color="currentColor"
41
+ />
42
+ <input
43
+ type="text"
44
+ placeholder={searchPlaceholder}
45
+ value={search ?? ""}
46
+ onChange={(e) => onSearchChange(e.target.value)}
47
+ className="h-10 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
48
+ />
49
+ </div>
50
+ )}
51
+ {filters}
52
+ </div>
53
+ <div className="flex items-center gap-3">
54
+ {resultCount !== undefined && (
55
+ <span className="text-sm text-secondary">
56
+ {resultCount} result{resultCount !== 1 ? "s" : ""}
57
+ </span>
58
+ )}
59
+ {actions}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,14 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ export interface AppSkeletonProps {
4
+ className?: string;
5
+ }
6
+
7
+ export function AppSkeleton({ className }: AppSkeletonProps) {
8
+ return (
9
+ <div
10
+ data-slot="app-skeleton"
11
+ className={cn("animate-pulse rounded-md bg-muted", className)}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ColumnDef,
5
+ type PaginationState,
6
+ type SortingState,
7
+ flexRender,
8
+ getCoreRowModel,
9
+ getPaginationRowModel,
10
+ getSortedRowModel,
11
+ useReactTable,
12
+ } from "@tanstack/react-table";
13
+ import { type ReactNode } from "react";
14
+ import { cn } from "@/lib/utils";
15
+ import { AppSkeleton } from "./app-skeleton";
16
+ import { AppEmptyState } from "./app-empty-state";
17
+
18
+ export interface AppTableProps<TData> {
19
+ columns: ColumnDef<TData, unknown>[];
20
+ data: TData[];
21
+ loading?: boolean;
22
+ sorting?: SortingState;
23
+ onSortingChange?: (sorting: SortingState) => void;
24
+ pagination?: PaginationState;
25
+ onPaginationChange?: (pagination: PaginationState) => void;
26
+ pageCount?: number;
27
+ emptyIcon?: ReactNode;
28
+ emptyTitle?: string;
29
+ emptyDescription?: string;
30
+ className?: string;
31
+ }
32
+
33
+ export function AppTable<TData>({
34
+ columns,
35
+ data,
36
+ loading = false,
37
+ sorting,
38
+ onSortingChange,
39
+ pagination,
40
+ onPaginationChange,
41
+ pageCount,
42
+ emptyIcon,
43
+ emptyTitle = "No results",
44
+ emptyDescription,
45
+ className,
46
+ }: AppTableProps<TData>) {
47
+ const table = useReactTable({
48
+ data,
49
+ columns,
50
+ pageCount,
51
+ state: {
52
+ sorting,
53
+ pagination,
54
+ },
55
+ onSortingChange,
56
+ onPaginationChange,
57
+ getCoreRowModel: getCoreRowModel(),
58
+ getSortedRowModel: getSortedRowModel(),
59
+ getPaginationRowModel: getPaginationRowModel(),
60
+ manualPagination: true,
61
+ });
62
+
63
+ if (loading) {
64
+ return (
65
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
66
+ <div className="space-y-2 p-4">
67
+ {Array.from({ length: 5 }).map((_, i) => (
68
+ <AppSkeleton key={i} className="h-12 w-full" />
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ if (data.length === 0) {
76
+ return (
77
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
78
+ <AppEmptyState
79
+ icon={emptyIcon}
80
+ title={emptyTitle}
81
+ description={emptyDescription}
82
+ />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div data-slot="app-table" className={cn("rounded-md border", className)}>
89
+ <div className="overflow-auto">
90
+ <table className="w-full caption-bottom text-sm">
91
+ <thead className="border-b bg-muted/50">
92
+ {table.getHeaderGroups().map((headerGroup) => (
93
+ <tr key={headerGroup.id}>
94
+ {headerGroup.headers.map((header) => (
95
+ <th
96
+ key={header.id}
97
+ className="h-10 px-4 text-left font-medium text-secondary"
98
+ >
99
+ {header.isPlaceholder
100
+ ? null
101
+ : flexRender(
102
+ header.column.columnDef.header,
103
+ header.getContext()
104
+ )}
105
+ </th>
106
+ ))}
107
+ </tr>
108
+ ))}
109
+ </thead>
110
+ <tbody>
111
+ {table.getRowModel().rows.map((row) => (
112
+ <tr
113
+ key={row.id}
114
+ className="border-b transition-colors hover:bg-muted/50"
115
+ >
116
+ {row.getVisibleCells().map((cell) => (
117
+ <td key={cell.id} className="p-4">
118
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
119
+ </td>
120
+ ))}
121
+ </tr>
122
+ ))}
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,5 @@
1
+ export { AppButton, buttonVariants, type AppButtonProps } from "./app-button";
2
+ export { AppEmptyState, type AppEmptyStateProps } from "./app-empty-state";
3
+ export { AppSkeleton, type AppSkeletonProps } from "./app-skeleton";
4
+ export { AppTable, type AppTableProps } from "./app-table";
5
+ export { AppListToolbar, type AppListToolbarProps } from "./app-list-toolbar";
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { useRouter, useSearchParams } from "next/navigation";
4
+ import { useCallback, useTransition } from "react";
5
+
6
+ export interface ListState {
7
+ search: string;
8
+ page: number;
9
+ limit: number;
10
+ sort?: string;
11
+ order?: "asc" | "desc";
12
+ [key: string]: string | number | undefined;
13
+ }
14
+
15
+ export function useListState(defaults: Partial<ListState> = {}) {
16
+ const router = useRouter();
17
+ const searchParams = useSearchParams();
18
+ const [isPending, startTransition] = useTransition();
19
+
20
+ const state: ListState = {
21
+ search: searchParams.get("search") ?? defaults.search ?? "",
22
+ page: Number(searchParams.get("page")) || defaults.page || 1,
23
+ limit: Number(searchParams.get("limit")) || defaults.limit || 20,
24
+ sort: searchParams.get("sort") ?? defaults.sort,
25
+ order: (searchParams.get("order") as "asc" | "desc") ?? defaults.order,
26
+ };
27
+
28
+ const setSearchParams = useCallback(
29
+ (updates: Partial<ListState>) => {
30
+ const params = new URLSearchParams(searchParams.toString());
31
+ Object.entries(updates).forEach(([key, value]) => {
32
+ if (value === undefined || value === null || value === "") {
33
+ params.delete(key);
34
+ } else {
35
+ params.set(key, String(value));
36
+ }
37
+ });
38
+ if (updates.search !== undefined) {
39
+ params.set("page", "1");
40
+ }
41
+ startTransition(() => {
42
+ router.push(`?${params.toString()}`, { scroll: false });
43
+ });
44
+ },
45
+ [router, searchParams, startTransition]
46
+ );
47
+
48
+ const setSearch = useCallback(
49
+ (search: string) => setSearchParams({ search }),
50
+ [setSearchParams]
51
+ );
52
+
53
+ const setPage = useCallback(
54
+ (page: number) => setSearchParams({ page }),
55
+ [setSearchParams]
56
+ );
57
+
58
+ const setSort = useCallback(
59
+ (sort: string, order: "asc" | "desc" = "asc") =>
60
+ setSearchParams({ sort, order }),
61
+ [setSearchParams]
62
+ );
63
+
64
+ return {
65
+ state,
66
+ setSearch,
67
+ setPage,
68
+ setSort,
69
+ setParams: setSearchParams,
70
+ isPending,
71
+ };
72
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { useSearchParams } from "next/navigation";
4
+ import { useCallback } from "react";
5
+
6
+ export function useUrlTab(defaultValue: string = "overview") {
7
+ const searchParams = useSearchParams();
8
+ const tab = searchParams.get("tab") ?? defaultValue;
9
+
10
+ const setTab = useCallback(
11
+ (value: string) => {
12
+ const url = new URL(window.location.href);
13
+ if (value === defaultValue) {
14
+ url.searchParams.delete("tab");
15
+ } else {
16
+ url.searchParams.set("tab", value);
17
+ }
18
+ window.history.replaceState({}, "", url.toString());
19
+ window.dispatchEvent(new Event("popstate"));
20
+ },
21
+ [defaultValue]
22
+ );
23
+
24
+ return { tab, setTab };
25
+ }
@@ -0,0 +1,123 @@
1
+ const BASE_URL =
2
+ typeof window !== "undefined"
3
+ ? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"
4
+ : process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
5
+
6
+ let _onUnauthorized: (() => void) | null = null;
7
+
8
+ export function onUnauthorized(fn: () => void) {
9
+ _onUnauthorized = fn;
10
+ return () => {
11
+ _onUnauthorized = null;
12
+ };
13
+ }
14
+
15
+ export class ApiError extends Error {
16
+ constructor(
17
+ public code: string,
18
+ message: string,
19
+ public status: number,
20
+ public details?: unknown
21
+ ) {
22
+ super(message);
23
+ this.name = "ApiError";
24
+ }
25
+ }
26
+
27
+ export interface PaginatedResponse<T> {
28
+ data: T[];
29
+ pagination: {
30
+ page: number;
31
+ limit: number;
32
+ total: number;
33
+ totalPages: number;
34
+ };
35
+ }
36
+
37
+ async function getToken(): Promise<string | null> {
38
+ try {
39
+ const mod = await import("next-auth/react");
40
+ const session = await mod.getSession();
41
+ return (session as { user?: { accessToken?: string } })?.user
42
+ ?.accessToken ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ export async function apiFetch<T>(
49
+ path: string,
50
+ opts: RequestInit = {}
51
+ ): Promise<T> {
52
+ const token = await getToken();
53
+ const headers: Record<string, string> = {
54
+ "Content-Type": "application/json",
55
+ ...(opts.headers as Record<string, string>),
56
+ };
57
+ if (token) {
58
+ headers.Authorization = `Bearer ${token}`;
59
+ }
60
+
61
+ const res = await fetch(`${BASE_URL}${path}`, {
62
+ ...opts,
63
+ headers,
64
+ });
65
+
66
+ if (res.status === 401) {
67
+ _onUnauthorized?.();
68
+ throw new ApiError("UNAUTHORIZED", "Unauthorized", 401);
69
+ }
70
+
71
+ const body = await res.json().catch(() => ({}));
72
+
73
+ if (!res.ok) {
74
+ throw new ApiError(
75
+ body?.error?.code ?? "UNKNOWN_ERROR",
76
+ body?.error?.message ?? "An error occurred",
77
+ res.status,
78
+ body?.error?.details
79
+ );
80
+ }
81
+
82
+ return (body?.data ?? body) as T;
83
+ }
84
+
85
+ export async function apiFetchEnvelope<T>(
86
+ path: string,
87
+ opts: RequestInit = {}
88
+ ): Promise<{ data: T; pagination?: PaginatedResponse<T>["pagination"] }> {
89
+ const token = await getToken();
90
+ const headers: Record<string, string> = {
91
+ "Content-Type": "application/json",
92
+ ...(opts.headers as Record<string, string>),
93
+ };
94
+ if (token) {
95
+ headers.Authorization = `Bearer ${token}`;
96
+ }
97
+
98
+ const res = await fetch(`${BASE_URL}${path}`, {
99
+ ...opts,
100
+ headers,
101
+ });
102
+
103
+ if (res.status === 401) {
104
+ _onUnauthorized?.();
105
+ throw new ApiError("UNAUTHORIZED", "Unauthorized", 401);
106
+ }
107
+
108
+ const body = await res.json().catch(() => ({}));
109
+
110
+ if (!res.ok) {
111
+ throw new ApiError(
112
+ body?.error?.code ?? "UNKNOWN_ERROR",
113
+ body?.error?.message ?? "An error occurred",
114
+ res.status,
115
+ body?.error?.details
116
+ );
117
+ }
118
+
119
+ return {
120
+ data: (body?.data ?? body) as T,
121
+ pagination: body?.pagination,
122
+ };
123
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { ThemeProvider } from "next-themes";
4
+ import { SessionProvider } from "next-auth/react";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+ import { useState } from "react";
7
+
8
+ export function Providers({ children }: { children: React.ReactNode }) {
9
+ const [queryClient] = useState(
10
+ () =>
11
+ new QueryClient({
12
+ defaultOptions: {
13
+ queries: {
14
+ retry: 1,
15
+ staleTime: 30_000,
16
+ },
17
+ },
18
+ })
19
+ );
20
+
21
+ return (
22
+ <ThemeProvider
23
+ attribute="class"
24
+ defaultTheme="light"
25
+ enableSystem
26
+ disableTransitionOnChange
27
+ >
28
+ <SessionProvider>
29
+ <QueryClientProvider client={queryClient}>
30
+ {children}
31
+ </QueryClientProvider>
32
+ </SessionProvider>
33
+ </ThemeProvider>
34
+ );
35
+ }
@@ -0,0 +1,21 @@
1
+
2
+ export interface ListQuery {
3
+ page?: number;
4
+ limit?: number;
5
+ search?: string;
6
+ sort?: string;
7
+ order?: "asc" | "desc";
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export const qk = {
12
+ all: ["api"] as const,
13
+ users: () => [...qk.all, "users"] as const,
14
+ user: (id: string) => [...qk.users(), id] as const,
15
+ projects: () => [...qk.all, "projects"] as const,
16
+ project: (id: string) => [...qk.projects(), id] as const,
17
+ };
18
+
19
+ export function qkList(base: readonly unknown[], query?: ListQuery) {
20
+ return [...base, { ...(query ?? {}) }] as const;
21
+ }
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /** Merge Tailwind classes with conflict resolution. */
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+
3
+ export const emailSchema = z.string().email("Invalid email address");
4
+
5
+ export const passwordSchema = z
6
+ .string()
7
+ .min(8, "Password must be at least 8 characters")
8
+ .max(128, "Password must be at most 128 characters");
9
+
10
+ export const nameSchema = z
11
+ .string()
12
+ .min(1, "Name is required")
13
+ .max(100, "Name must be at most 100 characters");
14
+
15
+ export const uuidSchema = z.string().uuid("Invalid UUID");
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import type { ColumnDef } from "@tanstack/react-table";
6
+ import { Add, DocumentText1, Refresh, Trash } from "iconsax-react";
7
+ import { AppButton, AppListToolbar, AppTable } from "@/components/shared";
8
+ import { useListState } from "@/hooks/use-list-state";
9
+ import { apiFetchEnvelope } from "@/lib/api/client";
10
+ import { qk, qkList } from "@/lib/query-keys";
11
+
12
+ interface ExampleItem {
13
+ id: string;
14
+ name: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ export default function ExampleListView() {
19
+ const { state, setSearch, isPending } = useListState();
20
+ const queryClient = useQueryClient();
21
+
22
+ const query = useQuery({
23
+ queryKey: qkList(qk.all, state),
24
+ queryFn: () =>
25
+ apiFetchEnvelope<{ items: ExampleItem[]; pagination?: { total: number; page: number; totalPages: number } }>(
26
+ `/examples?page=${state.page}&limit=${state.limit}&search=${encodeURIComponent(state.search)}`
27
+ ),
28
+ });
29
+
30
+ const deleteMutation = useMutation({
31
+ mutationFn: (id: string) =>
32
+ apiFetchEnvelope(`/examples/${id}`, { method: "DELETE" }),
33
+ onSuccess: () => void query.invalidateQueries(),
34
+ });
35
+
36
+ function handleDelete(id: string) {
37
+ deleteMutation.mutate(id);
38
+ }
39
+
40
+ const columns = useMemo<ColumnDef<ExampleItem, unknown>[]>(
41
+ () => [
42
+ { accessorKey: "name", header: "Name" },
43
+ {
44
+ accessorKey: "createdAt",
45
+ header: "Created",
46
+ cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
47
+ },
48
+ {
49
+ id: "actions",
50
+ header: "",
51
+ cell: ({ row }) => (
52
+ <AppButton variant="ghost" onClick={() => handleDelete(row.original.id)}>
53
+ <Trash variant="Outline" color="currentColor" className="h-4 w-4" />
54
+ </AppButton>
55
+ ),
56
+ },
57
+ ],
58
+ [deleteMutation.isPending]
59
+ );
60
+
61
+ return (
62
+ <div className="space-y-6">
63
+ <div className="flex items-center justify-between">
64
+ <h1 className="text-2xl font-bold">Examples</h1>
65
+ <AppButton>
66
+ <Add variant="Outline" color="currentColor" className="h-4 w-4" />
67
+ Create
68
+ </AppButton>
69
+ </div>
70
+
71
+ <AppListToolbar
72
+ search={state.search}
73
+ onSearchChange={setSearch}
74
+ searchPlaceholder="Search examples..."
75
+ resultCount={query.data?.pagination?.total}
76
+ />
77
+
78
+ {query.isError ? (
79
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 p-8 text-center">
80
+ <p className="text-sm text-muted-foreground">Could not load examples.</p>
81
+ <AppButton variant="outline" className="mt-3" onClick={() => void query.refetch()}>
82
+ <Refresh variant="Outline" color="currentColor" className="h-4 w-4" />
83
+ Retry
84
+ </AppButton>
85
+ </div>
86
+ ) : (
87
+ <AppTable
88
+ columns={columns}
89
+ data={query.data?.items ?? []}
90
+ loading={query.isLoading || isPending}
91
+ emptyIcon={<DocumentText1 variant="TwoTone" color="currentColor" />}
92
+ emptyTitle="No examples"
93
+ emptyDescription="Create your first example to get started."
94
+ />
95
+ )}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./src/*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }