@htlkg/astro 0.0.2 → 0.0.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.
- package/README.md +24 -8
- package/dist/chunk-2GML443T.js +273 -0
- package/dist/chunk-2GML443T.js.map +1 -0
- package/dist/chunk-6CW4SVDI.js +155 -0
- package/dist/chunk-6CW4SVDI.js.map +1 -0
- package/dist/{chunk-Z2ZAL7KX.js → chunk-UBF5F2RG.js} +1 -1
- package/dist/{chunk-Z2ZAL7KX.js.map → chunk-UBF5F2RG.js.map} +1 -1
- package/dist/htlkg/config.js +1 -1
- package/dist/htlkg/index.js +1 -1
- package/dist/index.js +126 -14
- package/dist/index.js.map +1 -1
- package/dist/utils/index.js +31 -12
- package/dist/vue-app-setup.js +47 -0
- package/dist/vue-app-setup.js.map +1 -0
- package/package.json +39 -10
- package/src/auth/auth.md +77 -0
- package/src/components/Island.astro +56 -0
- package/src/components/components.md +79 -0
- package/src/factories/createListPage.ts +290 -0
- package/src/factories/index.ts +16 -0
- package/src/htlkg/config.ts +10 -0
- package/src/htlkg/htlkg.md +63 -0
- package/src/htlkg/index.ts +53 -164
- package/src/index.ts +3 -0
- package/src/layouts/layouts.md +87 -0
- package/src/middleware/middleware.md +82 -0
- package/src/patterns/patterns.md +104 -0
- package/src/utils/filters.ts +320 -0
- package/src/utils/index.ts +8 -2
- package/src/utils/params.ts +260 -0
- package/src/utils/utils.md +86 -0
- package/dist/chunk-IWK5QCVD.js +0 -216
- package/dist/chunk-IWK5QCVD.js.map +0 -1
- package/dist/chunk-ZQ4XMJH7.js +0 -1
- package/dist/chunk-ZQ4XMJH7.js.map +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Layouts Module
|
|
2
|
+
|
|
3
|
+
Pre-built page layouts for common page types.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { AdminLayout } from '@htlkg/astro/layouts';
|
|
9
|
+
import { BrandLayout } from '@htlkg/astro/layouts';
|
|
10
|
+
import { AuthLayout } from '@htlkg/astro/layouts';
|
|
11
|
+
import { PublicLayout } from '@htlkg/astro/layouts';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## AdminLayout
|
|
15
|
+
|
|
16
|
+
Admin portal pages with sidebar navigation.
|
|
17
|
+
|
|
18
|
+
```astro
|
|
19
|
+
---
|
|
20
|
+
import { AdminLayout } from '@htlkg/astro/layouts';
|
|
21
|
+
|
|
22
|
+
const user = Astro.locals.user;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<AdminLayout user={user} title="Dashboard">
|
|
26
|
+
<DashboardContent />
|
|
27
|
+
</AdminLayout>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## BrandLayout
|
|
31
|
+
|
|
32
|
+
Brand-specific pages with brand context.
|
|
33
|
+
|
|
34
|
+
```astro
|
|
35
|
+
---
|
|
36
|
+
import { BrandLayout } from '@htlkg/astro/layouts';
|
|
37
|
+
|
|
38
|
+
const { brandId } = Astro.params;
|
|
39
|
+
const brand = await getBrand(brandId);
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<BrandLayout brand={brand} title="Brand Settings">
|
|
43
|
+
<BrandSettings />
|
|
44
|
+
</BrandLayout>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## AuthLayout
|
|
48
|
+
|
|
49
|
+
Authentication pages (login, signup).
|
|
50
|
+
|
|
51
|
+
```astro
|
|
52
|
+
---
|
|
53
|
+
import { AuthLayout } from '@htlkg/astro/layouts';
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<AuthLayout title="Login">
|
|
57
|
+
<LoginForm />
|
|
58
|
+
</AuthLayout>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## PublicLayout
|
|
62
|
+
|
|
63
|
+
Public-facing pages without authentication.
|
|
64
|
+
|
|
65
|
+
```astro
|
|
66
|
+
---
|
|
67
|
+
import { PublicLayout } from '@htlkg/astro/layouts';
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
<PublicLayout title="Welcome">
|
|
71
|
+
<LandingPage />
|
|
72
|
+
</PublicLayout>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## DefaultLayout
|
|
76
|
+
|
|
77
|
+
Base layout with minimal styling.
|
|
78
|
+
|
|
79
|
+
```astro
|
|
80
|
+
---
|
|
81
|
+
import { DefaultLayout } from '@htlkg/astro/layouts';
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
<DefaultLayout title="Page Title">
|
|
85
|
+
<slot />
|
|
86
|
+
</DefaultLayout>
|
|
87
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Middleware Module
|
|
2
|
+
|
|
3
|
+
Authentication middleware and route guards for Astro.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
requireAuth,
|
|
10
|
+
requireAdminAccess,
|
|
11
|
+
requireBrandAccess,
|
|
12
|
+
} from '@htlkg/astro/middleware';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Route Guards
|
|
16
|
+
|
|
17
|
+
### requireAuth
|
|
18
|
+
|
|
19
|
+
Require authenticated user. Redirects to login if not authenticated.
|
|
20
|
+
|
|
21
|
+
```astro
|
|
22
|
+
---
|
|
23
|
+
import { requireAuth } from '@htlkg/astro/middleware';
|
|
24
|
+
|
|
25
|
+
export const prerender = false;
|
|
26
|
+
|
|
27
|
+
const { user } = await requireAuth(Astro);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<h1>Welcome, {user.username}</h1>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### requireAdminAccess
|
|
34
|
+
|
|
35
|
+
Require admin role.
|
|
36
|
+
|
|
37
|
+
```astro
|
|
38
|
+
---
|
|
39
|
+
import { requireAdminAccess } from '@htlkg/astro/middleware';
|
|
40
|
+
|
|
41
|
+
export const prerender = false;
|
|
42
|
+
|
|
43
|
+
const { user } = await requireAdminAccess(Astro);
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<AdminDashboard user={user} />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### requireBrandAccess
|
|
50
|
+
|
|
51
|
+
Require access to a specific brand.
|
|
52
|
+
|
|
53
|
+
```astro
|
|
54
|
+
---
|
|
55
|
+
import { requireBrandAccess } from '@htlkg/astro/middleware';
|
|
56
|
+
|
|
57
|
+
export const prerender = false;
|
|
58
|
+
|
|
59
|
+
const { brandId } = Astro.params;
|
|
60
|
+
const { user, brand } = await requireBrandAccess(Astro, brandId);
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
<BrandDashboard brand={brand} user={user} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Custom Login URL
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
await requireAuth(Astro, '/custom-login');
|
|
70
|
+
await requireAdminAccess(Astro, '/admin/login');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Route Guard Config
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
interface RouteGuardConfig {
|
|
77
|
+
publicRoutes: RoutePattern[];
|
|
78
|
+
protectedRoutes: RoutePattern[];
|
|
79
|
+
loginPage: string;
|
|
80
|
+
brandRoutePattern?: RegExp;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Patterns Module
|
|
2
|
+
|
|
3
|
+
Reusable page patterns for common admin and brand pages.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Admin patterns
|
|
9
|
+
import { ListPage } from '@htlkg/astro/patterns/admin';
|
|
10
|
+
import { DetailPage } from '@htlkg/astro/patterns/admin';
|
|
11
|
+
import { FormPage } from '@htlkg/astro/patterns/admin';
|
|
12
|
+
|
|
13
|
+
// Brand patterns
|
|
14
|
+
import { ConfigPage } from '@htlkg/astro/patterns/brand';
|
|
15
|
+
import { PortalPage } from '@htlkg/astro/patterns/brand';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Admin Patterns
|
|
19
|
+
|
|
20
|
+
### ListPage
|
|
21
|
+
|
|
22
|
+
List/table view with filters and pagination.
|
|
23
|
+
|
|
24
|
+
```astro
|
|
25
|
+
---
|
|
26
|
+
import { ListPage } from '@htlkg/astro/patterns/admin';
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<ListPage
|
|
30
|
+
title="Brands"
|
|
31
|
+
items={brands}
|
|
32
|
+
columns={columns}
|
|
33
|
+
filters={filters}
|
|
34
|
+
onRowClick={(brand) => `/admin/brands/${brand.id}`}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### DetailPage
|
|
39
|
+
|
|
40
|
+
Detail view with tabs and actions.
|
|
41
|
+
|
|
42
|
+
```astro
|
|
43
|
+
---
|
|
44
|
+
import { DetailPage } from '@htlkg/astro/patterns/admin';
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<DetailPage
|
|
48
|
+
title={brand.name}
|
|
49
|
+
tabs={['Overview', 'Settings', 'Users']}
|
|
50
|
+
actions={['Edit', 'Delete']}
|
|
51
|
+
>
|
|
52
|
+
<BrandOverview slot="Overview" brand={brand} />
|
|
53
|
+
<BrandSettings slot="Settings" brand={brand} />
|
|
54
|
+
<BrandUsers slot="Users" brand={brand} />
|
|
55
|
+
</DetailPage>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### FormPage
|
|
59
|
+
|
|
60
|
+
Form view with validation.
|
|
61
|
+
|
|
62
|
+
```astro
|
|
63
|
+
---
|
|
64
|
+
import { FormPage } from '@htlkg/astro/patterns/admin';
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
<FormPage
|
|
68
|
+
title="Create Brand"
|
|
69
|
+
schema={brandSchema}
|
|
70
|
+
onSubmit={handleSubmit}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Brand Patterns
|
|
75
|
+
|
|
76
|
+
### ConfigPage
|
|
77
|
+
|
|
78
|
+
Configuration page for brand settings.
|
|
79
|
+
|
|
80
|
+
```astro
|
|
81
|
+
---
|
|
82
|
+
import { ConfigPage } from '@htlkg/astro/patterns/brand';
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<ConfigPage
|
|
86
|
+
brand={brand}
|
|
87
|
+
product={product}
|
|
88
|
+
config={productConfig}
|
|
89
|
+
/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### PortalPage
|
|
93
|
+
|
|
94
|
+
Public portal page for brand.
|
|
95
|
+
|
|
96
|
+
```astro
|
|
97
|
+
---
|
|
98
|
+
import { PortalPage } from '@htlkg/astro/patterns/brand';
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
<PortalPage brand={brand}>
|
|
102
|
+
<WifiPortal />
|
|
103
|
+
</PortalPage>
|
|
104
|
+
```
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Building Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for building GraphQL filters from parsed URL parameters,
|
|
5
|
+
* and for applying client-side filters to data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FilterFieldConfig, ListParams } from "./params";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL filter operators
|
|
12
|
+
*/
|
|
13
|
+
export type GraphQLOperator = "eq" | "ne" | "le" | "lt" | "ge" | "gt" | "contains" | "notContains" | "beginsWith" | "between";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GraphQL filter condition
|
|
17
|
+
*/
|
|
18
|
+
export interface GraphQLFilterCondition {
|
|
19
|
+
[key: string]: {
|
|
20
|
+
[operator in GraphQLOperator]?: any;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GraphQL combined filter
|
|
26
|
+
*/
|
|
27
|
+
export interface GraphQLFilter {
|
|
28
|
+
and?: GraphQLFilterCondition[];
|
|
29
|
+
or?: GraphQLFilterCondition[];
|
|
30
|
+
not?: GraphQLFilter;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build GraphQL filter from parsed URL parameters
|
|
36
|
+
*
|
|
37
|
+
* Only includes fields marked with `graphql: true` in the field config.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const filter = buildGraphQLFilter(
|
|
42
|
+
* { status: 'active', name: 'test' },
|
|
43
|
+
* [
|
|
44
|
+
* { key: 'status', type: 'select', graphql: true },
|
|
45
|
+
* { key: 'name', type: 'text', graphql: true },
|
|
46
|
+
* ]
|
|
47
|
+
* );
|
|
48
|
+
* // Returns: { and: [{ status: { eq: 'active' } }, { name: { contains: 'test' } }] }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function buildGraphQLFilter(
|
|
52
|
+
filters: Record<string, any>,
|
|
53
|
+
filterableFields: FilterFieldConfig[],
|
|
54
|
+
options: { searchFields?: string[]; search?: string } = {}
|
|
55
|
+
): GraphQLFilter | undefined {
|
|
56
|
+
const conditions: GraphQLFilterCondition[] = [];
|
|
57
|
+
|
|
58
|
+
// Build field filters
|
|
59
|
+
for (const field of filterableFields) {
|
|
60
|
+
if (!field.graphql) continue;
|
|
61
|
+
|
|
62
|
+
const value = filters[field.key];
|
|
63
|
+
if (value === undefined || value === null || value === "") continue;
|
|
64
|
+
|
|
65
|
+
const condition = buildFieldCondition(field, value);
|
|
66
|
+
if (condition) {
|
|
67
|
+
conditions.push(condition);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build search filter (applies to multiple fields with OR)
|
|
72
|
+
if (options.search && options.searchFields && options.searchFields.length > 0) {
|
|
73
|
+
const searchConditions = options.searchFields.map((fieldKey) => ({
|
|
74
|
+
[fieldKey]: { contains: options.search },
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
if (searchConditions.length === 1) {
|
|
78
|
+
conditions.push(searchConditions[0]);
|
|
79
|
+
} else if (searchConditions.length > 1) {
|
|
80
|
+
// Multiple search fields use OR
|
|
81
|
+
conditions.push({ or: searchConditions } as unknown as GraphQLFilterCondition);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return combined filter
|
|
86
|
+
if (conditions.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (conditions.length === 1) {
|
|
91
|
+
return conditions[0] as unknown as GraphQLFilter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { and: conditions };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a single field condition
|
|
99
|
+
*/
|
|
100
|
+
function buildFieldCondition(field: FilterFieldConfig, value: any): GraphQLFilterCondition | undefined {
|
|
101
|
+
// Use custom operator if provided
|
|
102
|
+
const operator = field.graphqlOperator;
|
|
103
|
+
|
|
104
|
+
if (operator) {
|
|
105
|
+
// Use the custom operator specified in config
|
|
106
|
+
return { [field.key]: { [operator]: value } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Default behavior based on field type
|
|
110
|
+
switch (field.type) {
|
|
111
|
+
case "text":
|
|
112
|
+
// Text fields use contains by default (case-sensitive in DynamoDB)
|
|
113
|
+
return { [field.key]: { contains: String(value) } };
|
|
114
|
+
|
|
115
|
+
case "number":
|
|
116
|
+
// Number fields use exact match
|
|
117
|
+
return { [field.key]: { eq: Number(value) } };
|
|
118
|
+
|
|
119
|
+
case "boolean":
|
|
120
|
+
// Boolean fields use exact match
|
|
121
|
+
return { [field.key]: { eq: Boolean(value) } };
|
|
122
|
+
|
|
123
|
+
case "date":
|
|
124
|
+
// Date fields - could be exact or range
|
|
125
|
+
if (value instanceof Date) {
|
|
126
|
+
return { [field.key]: { eq: value.toISOString() } };
|
|
127
|
+
}
|
|
128
|
+
return { [field.key]: { eq: value } };
|
|
129
|
+
|
|
130
|
+
case "select":
|
|
131
|
+
// Select fields use exact match
|
|
132
|
+
return { [field.key]: { eq: value } };
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
return { [field.key]: { eq: value } };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply client-side filters to data array
|
|
141
|
+
*
|
|
142
|
+
* Only applies filters for fields NOT marked with `graphql: true`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const filtered = applyClientFilters(
|
|
147
|
+
* items,
|
|
148
|
+
* { brandCount: 5 },
|
|
149
|
+
* [{ key: 'brandCount', type: 'number', graphql: false }]
|
|
150
|
+
* );
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function applyClientFilters<T extends Record<string, any>>(
|
|
154
|
+
items: T[],
|
|
155
|
+
filters: Record<string, any>,
|
|
156
|
+
filterableFields: FilterFieldConfig[],
|
|
157
|
+
options: { search?: string; searchFields?: string[] } = {}
|
|
158
|
+
): T[] {
|
|
159
|
+
let result = items;
|
|
160
|
+
|
|
161
|
+
// Apply field filters (only non-GraphQL fields)
|
|
162
|
+
for (const field of filterableFields) {
|
|
163
|
+
if (field.graphql) continue; // Skip GraphQL fields
|
|
164
|
+
|
|
165
|
+
const value = filters[field.key];
|
|
166
|
+
if (value === undefined || value === null || value === "") continue;
|
|
167
|
+
|
|
168
|
+
result = result.filter((item) => matchesFieldFilter(item, field, value));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Apply search filter (only if not applied via GraphQL)
|
|
172
|
+
if (options.search && options.searchFields) {
|
|
173
|
+
const searchLower = options.search.toLowerCase();
|
|
174
|
+
result = result.filter((item) =>
|
|
175
|
+
options.searchFields!.some((fieldKey) => {
|
|
176
|
+
const fieldValue = item[fieldKey];
|
|
177
|
+
if (fieldValue === null || fieldValue === undefined) return false;
|
|
178
|
+
return String(fieldValue).toLowerCase().includes(searchLower);
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if an item matches a field filter
|
|
188
|
+
*/
|
|
189
|
+
function matchesFieldFilter<T extends Record<string, any>>(item: T, field: FilterFieldConfig, filterValue: any): boolean {
|
|
190
|
+
const itemValue = item[field.key];
|
|
191
|
+
|
|
192
|
+
switch (field.type) {
|
|
193
|
+
case "text":
|
|
194
|
+
if (itemValue === null || itemValue === undefined) return false;
|
|
195
|
+
return String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());
|
|
196
|
+
|
|
197
|
+
case "number":
|
|
198
|
+
return Number(itemValue) === Number(filterValue);
|
|
199
|
+
|
|
200
|
+
case "boolean":
|
|
201
|
+
return Boolean(itemValue) === Boolean(filterValue);
|
|
202
|
+
|
|
203
|
+
case "date":
|
|
204
|
+
if (!itemValue) return false;
|
|
205
|
+
const itemDate = itemValue instanceof Date ? itemValue : new Date(itemValue);
|
|
206
|
+
const filterDate = filterValue instanceof Date ? filterValue : new Date(filterValue);
|
|
207
|
+
// Compare dates (ignoring time)
|
|
208
|
+
return itemDate.toDateString() === filterDate.toDateString();
|
|
209
|
+
|
|
210
|
+
case "select":
|
|
211
|
+
return itemValue === filterValue;
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return itemValue === filterValue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Sort items by a key
|
|
220
|
+
*/
|
|
221
|
+
export function sortItems<T extends Record<string, any>>(items: T[], sortKey: string, sortOrder: "asc" | "desc"): T[] {
|
|
222
|
+
if (!sortKey) return items;
|
|
223
|
+
|
|
224
|
+
const multiplier = sortOrder === "asc" ? 1 : -1;
|
|
225
|
+
|
|
226
|
+
return [...items].sort((a, b) => {
|
|
227
|
+
const aVal = a[sortKey];
|
|
228
|
+
const bVal = b[sortKey];
|
|
229
|
+
|
|
230
|
+
// Handle null/undefined
|
|
231
|
+
if (aVal === null || aVal === undefined) return 1;
|
|
232
|
+
if (bVal === null || bVal === undefined) return -1;
|
|
233
|
+
if (aVal === bVal) return 0;
|
|
234
|
+
|
|
235
|
+
// Handle different types
|
|
236
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
237
|
+
return aVal.localeCompare(bVal) * multiplier;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
241
|
+
return (aVal - bVal) * multiplier;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (aVal instanceof Date && bVal instanceof Date) {
|
|
245
|
+
return (aVal.getTime() - bVal.getTime()) * multiplier;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback to string comparison
|
|
249
|
+
return String(aVal).localeCompare(String(bVal)) * multiplier;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Paginate items
|
|
255
|
+
*/
|
|
256
|
+
export function paginateItems<T>(
|
|
257
|
+
items: T[],
|
|
258
|
+
page: number,
|
|
259
|
+
pageSize: number
|
|
260
|
+
): {
|
|
261
|
+
paginatedItems: T[];
|
|
262
|
+
totalItems: number;
|
|
263
|
+
totalPages: number;
|
|
264
|
+
currentPage: number;
|
|
265
|
+
pageSize: number;
|
|
266
|
+
} {
|
|
267
|
+
const totalItems = items.length;
|
|
268
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
269
|
+
const currentPage = Math.min(Math.max(1, page), totalPages || 1);
|
|
270
|
+
|
|
271
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
272
|
+
const endIndex = startIndex + pageSize;
|
|
273
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
paginatedItems,
|
|
277
|
+
totalItems,
|
|
278
|
+
totalPages,
|
|
279
|
+
currentPage,
|
|
280
|
+
pageSize,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Process list data with filters, sorting, and pagination
|
|
286
|
+
*
|
|
287
|
+
* Convenience function that combines all processing steps.
|
|
288
|
+
*/
|
|
289
|
+
export function processListData<T extends Record<string, any>>(
|
|
290
|
+
items: T[],
|
|
291
|
+
params: ListParams,
|
|
292
|
+
filterableFields: FilterFieldConfig[],
|
|
293
|
+
options: { searchFields?: string[] } = {}
|
|
294
|
+
): {
|
|
295
|
+
items: T[];
|
|
296
|
+
totalItems: number;
|
|
297
|
+
totalPages: number;
|
|
298
|
+
currentPage: number;
|
|
299
|
+
pageSize: number;
|
|
300
|
+
} {
|
|
301
|
+
// Apply client-side filters
|
|
302
|
+
let result = applyClientFilters(items, params.filters, filterableFields, {
|
|
303
|
+
search: params.search,
|
|
304
|
+
searchFields: options.searchFields,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Sort
|
|
308
|
+
result = sortItems(result, params.sortKey, params.sortOrder);
|
|
309
|
+
|
|
310
|
+
// Paginate
|
|
311
|
+
const paginated = paginateItems(result, params.page, params.pageSize);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
items: paginated.paginatedItems,
|
|
315
|
+
totalItems: paginated.totalItems,
|
|
316
|
+
totalPages: paginated.totalPages,
|
|
317
|
+
currentPage: paginated.currentPage,
|
|
318
|
+
pageSize: paginated.pageSize,
|
|
319
|
+
};
|
|
320
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @htlkg/
|
|
3
|
-
*
|
|
2
|
+
* @htlkg/astro - Utilities
|
|
3
|
+
*
|
|
4
4
|
* Helper functions for Astro pages.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from './ssr';
|
|
8
8
|
export * from './static';
|
|
9
9
|
export * from './hydration';
|
|
10
|
+
|
|
11
|
+
// URL parameter parsing and building
|
|
12
|
+
export * from './params';
|
|
13
|
+
|
|
14
|
+
// Filter building and processing
|
|
15
|
+
export * from './filters';
|