@nlabs/metropolisjs 0.1.2 → 0.2.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.
- package/.github/copilot-instructions.md +167 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +112 -0
- package/GRAPHQL_VALIDATION_RESULTS.md +158 -0
- package/PERMISSION_GRAPHQL_INTEGRATION.md +293 -0
- package/PERMISSION_IMPLEMENTATION_SUMMARY.md +243 -0
- package/README.md +1123 -112
- package/SIGNIN_GRAPHQL_VALIDATION.md +125 -0
- package/VAULT_GRAPHQL_API_FINDINGS.md +312 -0
- package/docs/ACTIONS.md +63 -0
- package/docs/COLLECTIONS.md +1038 -0
- package/docs/CONNECTIONS.md +1367 -0
- package/docs/CRUD_INTEGRATION.md +702 -0
- package/examples/connections-usage.tsx +909 -0
- package/examples/content-actions-usage.ts +144 -0
- package/examples/crud-usage.tsx +847 -0
- package/examples/extensibility-usage.tsx +889 -0
- package/examples/i18n-import-examples.ts +86 -0
- package/examples/i18n-namespace-usage.ts +118 -0
- package/examples/i18next-singleton-demo.ts +187 -0
- package/examples/permission-system-usage.tsx +415 -0
- package/examples/project-details.tsx +397 -0
- package/examples/shared-i18n-usage.tsx +203 -0
- package/examples/signup-error-handling.ts +100 -0
- package/examples/translation-types.ts +86 -0
- package/examples/translation-usage.tsx +336 -0
- package/index.js +1 -0
- package/jest.config.mjs +28 -0
- package/lex.config.mjs +11 -2
- package/lib/__tests__/e2e/helpers/testGraphQLServer.d.ts +22 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServer.d.ts.map +1 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServer.js +197 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.d.ts +3 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.d.ts.map +1 -0
- package/lib/__tests__/e2e/helpers/testGraphQLServerSimple.js +52 -0
- package/lib/actions/appActions/appActions.d.ts +29 -0
- package/lib/actions/appActions/appActions.d.ts.map +1 -0
- package/lib/actions/appActions/appActions.js +159 -0
- package/lib/actions/connectionActions/connectionActions.d.ts +19 -0
- package/lib/actions/connectionActions/connectionActions.d.ts.map +1 -0
- package/lib/actions/connectionActions/connectionActions.js +131 -0
- package/lib/actions/contentActions/contentActions.d.ts +34 -0
- package/lib/actions/contentActions/contentActions.d.ts.map +1 -0
- package/lib/actions/contentActions/contentActions.js +314 -0
- package/lib/actions/conversationActions/conversationActions.d.ts +30 -0
- package/lib/actions/conversationActions/conversationActions.d.ts.map +1 -0
- package/lib/actions/conversationActions/conversationActions.js +225 -0
- package/lib/actions/eventActions/eventActions.d.ts +27 -7
- package/lib/actions/eventActions/eventActions.d.ts.map +1 -0
- package/lib/actions/eventActions/eventActions.js +436 -2
- package/lib/actions/groupActions/groupActions.d.ts +30 -0
- package/lib/actions/groupActions/groupActions.d.ts.map +1 -0
- package/lib/actions/groupActions/groupActions.js +229 -0
- package/lib/actions/imageActions/imageActions.d.ts +16 -10
- package/lib/actions/imageActions/imageActions.d.ts.map +1 -0
- package/lib/actions/imageActions/imageActions.js +593 -2
- package/lib/actions/index.d.ts +21 -9
- package/lib/actions/index.d.ts.map +1 -0
- package/lib/actions/index.js +22 -2
- package/lib/actions/locationActions/locationActions.d.ts +11 -9
- package/lib/actions/locationActions/locationActions.d.ts.map +1 -0
- package/lib/actions/locationActions/locationActions.js +388 -2
- package/lib/actions/messageActions/messageActions.d.ts +16 -32
- package/lib/actions/messageActions/messageActions.d.ts.map +1 -0
- package/lib/actions/messageActions/messageActions.js +235 -2
- package/lib/actions/permissionActions/permissionActions.d.ts +34 -0
- package/lib/actions/permissionActions/permissionActions.d.ts.map +1 -0
- package/lib/actions/permissionActions/permissionActions.js +330 -0
- package/lib/actions/personaActions/personaActions.d.ts +65 -0
- package/lib/actions/personaActions/personaActions.d.ts.map +1 -0
- package/lib/actions/personaActions/personaActions.js +572 -0
- package/lib/actions/postActions/postActions.d.ts +12 -32
- package/lib/actions/postActions/postActions.d.ts.map +1 -0
- package/lib/actions/postActions/postActions.js +447 -2
- package/lib/actions/reactionActions/reactionActions.d.ts +13 -27
- package/lib/actions/reactionActions/reactionActions.d.ts.map +1 -0
- package/lib/actions/reactionActions/reactionActions.js +413 -2
- package/lib/actions/sseActions/sseActions.d.ts +1 -0
- package/lib/actions/sseActions/sseActions.d.ts.map +1 -0
- package/lib/actions/sseActions/sseActions.js +148 -2
- package/lib/actions/subscriptionActions/subscriptionActions.d.ts +24 -0
- package/lib/actions/subscriptionActions/subscriptionActions.d.ts.map +1 -0
- package/lib/actions/subscriptionActions/subscriptionActions.js +329 -0
- package/lib/actions/tagActions/tagActions.d.ts +13 -31
- package/lib/actions/tagActions/tagActions.d.ts.map +1 -0
- package/lib/actions/tagActions/tagActions.js +347 -2
- package/lib/actions/translationActions/translationActions.d.ts +38 -0
- package/lib/actions/translationActions/translationActions.d.ts.map +1 -0
- package/lib/actions/translationActions/translationActions.js +189 -0
- package/lib/actions/userActions/userActions.d.ts +42 -95
- package/lib/actions/userActions/userActions.d.ts.map +1 -0
- package/lib/actions/userActions/userActions.js +844 -2
- package/lib/actions/videoActions/videoActions.d.ts +41 -0
- package/lib/actions/videoActions/videoActions.d.ts.map +1 -0
- package/lib/actions/videoActions/videoActions.js +532 -0
- package/lib/actions/websocketActions/websocketActions.d.ts +20 -21
- package/lib/actions/websocketActions/websocketActions.d.ts.map +1 -0
- package/lib/actions/websocketActions/websocketActions.js +377 -2
- package/lib/adapters/appAdapter/appAdapter.d.ts +8 -0
- package/lib/adapters/appAdapter/appAdapter.d.ts.map +1 -0
- package/lib/adapters/appAdapter/appAdapter.js +90 -0
- package/lib/adapters/arangoAdapter/arangoAdapter.d.ts +3 -2
- package/lib/adapters/arangoAdapter/arangoAdapter.d.ts.map +1 -0
- package/lib/adapters/arangoAdapter/arangoAdapter.js +75 -2
- package/lib/adapters/contentAdapter/contentAdapter.d.ts +3 -2
- package/lib/adapters/contentAdapter/contentAdapter.d.ts.map +1 -0
- package/lib/adapters/contentAdapter/contentAdapter.js +107 -2
- package/lib/adapters/conversationAdapter/conversationAdapter.d.ts +3 -2
- package/lib/adapters/conversationAdapter/conversationAdapter.d.ts.map +1 -0
- package/lib/adapters/conversationAdapter/conversationAdapter.js +75 -2
- package/lib/adapters/eventAdapter/eventAdapter.d.ts +5 -2
- package/lib/adapters/eventAdapter/eventAdapter.d.ts.map +1 -0
- package/lib/adapters/eventAdapter/eventAdapter.js +162 -2
- package/lib/adapters/fileAdapter/fileAdapter.d.ts +3 -2
- package/lib/adapters/fileAdapter/fileAdapter.d.ts.map +1 -0
- package/lib/adapters/fileAdapter/fileAdapter.js +118 -2
- package/lib/adapters/groupAdapter/groupAdapter.d.ts +28 -0
- package/lib/adapters/groupAdapter/groupAdapter.d.ts.map +1 -0
- package/lib/adapters/groupAdapter/groupAdapter.js +66 -0
- package/lib/adapters/imageAdapter/imageAdapter.d.ts +9 -2
- package/lib/adapters/imageAdapter/imageAdapter.d.ts.map +1 -0
- package/lib/adapters/imageAdapter/imageAdapter.js +134 -2
- package/lib/adapters/index.d.ts +34 -16
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +19 -2
- package/lib/adapters/locationAdapter/locationAdapter.d.ts +3 -2
- package/lib/adapters/locationAdapter/locationAdapter.d.ts.map +1 -0
- package/lib/adapters/locationAdapter/locationAdapter.js +95 -2
- package/lib/adapters/messageAdapter/messageAdapter.d.ts +23 -19
- package/lib/adapters/messageAdapter/messageAdapter.d.ts.map +1 -0
- package/lib/adapters/messageAdapter/messageAdapter.js +82 -2
- package/lib/adapters/permissionAdapter/permissionAdapter.d.ts +41 -0
- package/lib/adapters/permissionAdapter/permissionAdapter.d.ts.map +1 -0
- package/lib/adapters/permissionAdapter/permissionAdapter.js +120 -0
- package/lib/adapters/personaAdapter/personaAdapter.d.ts +28 -2
- package/lib/adapters/personaAdapter/personaAdapter.d.ts.map +1 -0
- package/lib/adapters/personaAdapter/personaAdapter.js +161 -2
- package/lib/adapters/postAdapter/postAdapter.d.ts +3 -2
- package/lib/adapters/postAdapter/postAdapter.d.ts.map +1 -0
- package/lib/adapters/postAdapter/postAdapter.js +115 -2
- package/lib/adapters/reactionAdapter/reactionAdapter.d.ts +5 -2
- package/lib/adapters/reactionAdapter/reactionAdapter.d.ts.map +1 -0
- package/lib/adapters/reactionAdapter/reactionAdapter.js +81 -2
- package/lib/adapters/reaktorAdapter/reaktorAdapter.d.ts +3 -2
- package/lib/adapters/reaktorAdapter/reaktorAdapter.d.ts.map +1 -0
- package/lib/adapters/reaktorAdapter/reaktorAdapter.js +58 -2
- package/lib/adapters/sessionAdapter/sessionAdapter.d.ts +3 -2
- package/lib/adapters/sessionAdapter/sessionAdapter.d.ts.map +1 -0
- package/lib/adapters/sessionAdapter/sessionAdapter.js +80 -2
- package/lib/adapters/tagAdapter/tagAdapter.d.ts +3 -2
- package/lib/adapters/tagAdapter/tagAdapter.d.ts.map +1 -0
- package/lib/adapters/tagAdapter/tagAdapter.js +98 -2
- package/lib/adapters/translationAdapter/translationAdapter.d.ts +21 -0
- package/lib/adapters/translationAdapter/translationAdapter.d.ts.map +1 -0
- package/lib/adapters/translationAdapter/translationAdapter.js +55 -0
- package/lib/adapters/userAdapter/userAdapter.d.ts +18 -6
- package/lib/adapters/userAdapter/userAdapter.d.ts.map +1 -0
- package/lib/adapters/userAdapter/userAdapter.js +302 -2
- package/lib/adapters/videoAdapter/videoAdapter.d.ts +8 -0
- package/lib/adapters/videoAdapter/videoAdapter.d.ts.map +1 -0
- package/lib/adapters/videoAdapter/videoAdapter.js +194 -0
- package/lib/config/index.d.ts +19 -6
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/index.js +97 -2
- package/lib/constants/Collections.d.ts +44 -0
- package/lib/constants/Collections.d.ts.map +1 -0
- package/lib/constants/Collections.js +40 -0
- package/lib/constants/MetropolisConstants.d.ts +1 -0
- package/lib/constants/MetropolisConstants.d.ts.map +1 -0
- package/lib/constants/MetropolisConstants.js +15 -2
- package/lib/graphql/message.d.ts +1 -0
- package/lib/graphql/message.d.ts.map +1 -0
- package/lib/graphql/message.js +6 -4
- package/lib/graphql/notification.d.ts +1 -0
- package/lib/graphql/notification.d.ts.map +1 -0
- package/lib/graphql/notification.js +6 -4
- package/lib/graphql/session.d.ts +1 -0
- package/lib/graphql/session.d.ts.map +1 -0
- package/lib/graphql/session.js +4 -3
- package/lib/index.d.ts +45 -22
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +248 -2
- package/lib/stores/appStore.d.ts +1 -0
- package/lib/stores/appStore.d.ts.map +1 -0
- package/lib/stores/appStore.js +23 -2
- package/lib/stores/contentStore.d.ts +36 -0
- package/lib/stores/contentStore.d.ts.map +1 -0
- package/lib/stores/contentStore.js +139 -0
- package/lib/stores/conversationStore.d.ts +38 -0
- package/lib/stores/conversationStore.d.ts.map +1 -0
- package/lib/stores/conversationStore.js +117 -0
- package/lib/stores/eventStore.d.ts +4 -1
- package/lib/stores/eventStore.d.ts.map +1 -0
- package/lib/stores/eventStore.js +47 -2
- package/lib/stores/groupStore.d.ts +37 -0
- package/lib/stores/groupStore.d.ts.map +1 -0
- package/lib/stores/groupStore.js +68 -0
- package/lib/stores/imageStore.d.ts +6 -1
- package/lib/stores/imageStore.d.ts.map +1 -0
- package/lib/stores/imageStore.js +53 -2
- package/lib/stores/index.d.ts +20 -11
- package/lib/stores/index.d.ts.map +1 -0
- package/lib/stores/index.js +24 -2
- package/lib/stores/locationStore.d.ts +2 -1
- package/lib/stores/locationStore.d.ts.map +1 -0
- package/lib/stores/locationStore.js +42 -2
- package/lib/stores/messageStore.d.ts +19 -4
- package/lib/stores/messageStore.d.ts.map +1 -0
- package/lib/stores/messageStore.js +137 -2
- package/lib/stores/notificationStore.d.ts +33 -0
- package/lib/stores/notificationStore.d.ts.map +1 -0
- package/lib/stores/notificationStore.js +55 -0
- package/lib/stores/permissionStore.d.ts +37 -0
- package/lib/stores/permissionStore.d.ts.map +1 -0
- package/lib/stores/permissionStore.js +117 -0
- package/lib/stores/personaStore.d.ts +26 -0
- package/lib/stores/personaStore.d.ts.map +1 -0
- package/lib/stores/personaStore.js +118 -0
- package/lib/stores/postStore.d.ts +2 -1
- package/lib/stores/postStore.d.ts.map +1 -0
- package/lib/stores/postStore.js +53 -2
- package/lib/stores/reactionStore.d.ts +1 -0
- package/lib/stores/reactionStore.d.ts.map +1 -0
- package/lib/stores/reactionStore.js +28 -2
- package/lib/stores/subscriptionStore.d.ts +41 -0
- package/lib/stores/subscriptionStore.d.ts.map +1 -0
- package/lib/stores/subscriptionStore.js +85 -0
- package/lib/stores/tagStore.d.ts +8 -5
- package/lib/stores/tagStore.d.ts.map +1 -0
- package/lib/stores/tagStore.js +87 -2
- package/lib/stores/translationStore.d.ts +42 -0
- package/lib/stores/translationStore.d.ts.map +1 -0
- package/lib/stores/translationStore.js +122 -0
- package/lib/stores/userStore.d.ts +5 -4
- package/lib/stores/userStore.d.ts.map +1 -0
- package/lib/stores/userStore.js +319 -2
- package/lib/stores/videoStore.d.ts +39 -0
- package/lib/stores/videoStore.d.ts.map +1 -0
- package/lib/stores/videoStore.js +132 -0
- package/lib/stores/websocketStore.d.ts +1 -0
- package/lib/stores/websocketStore.d.ts.map +1 -0
- package/lib/stores/websocketStore.js +51 -2
- package/lib/types/apps.types.d.ts +17 -0
- package/lib/types/apps.types.d.ts.map +1 -0
- package/lib/types/apps.types.js +6 -0
- package/lib/types/common.types.d.ts +31 -0
- package/lib/types/common.types.d.ts.map +1 -0
- package/lib/types/common.types.js +6 -0
- package/lib/types/conversations.types.d.ts +17 -0
- package/lib/types/conversations.types.d.ts.map +1 -0
- package/lib/types/conversations.types.js +6 -0
- package/lib/types/edges.types.d.ts +38 -0
- package/lib/types/edges.types.d.ts.map +1 -0
- package/lib/types/edges.types.js +6 -0
- package/lib/types/groups.types.d.ts +19 -0
- package/lib/types/groups.types.d.ts.map +1 -0
- package/lib/types/groups.types.js +6 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +12 -0
- package/lib/types/subscriptions.types.d.ts +32 -0
- package/lib/types/subscriptions.types.d.ts.map +1 -0
- package/lib/types/subscriptions.types.js +6 -0
- package/lib/types/videos.types.d.ts +61 -0
- package/lib/types/videos.types.d.ts.map +1 -0
- package/lib/types/videos.types.js +6 -0
- package/lib/utils/MetropolisProvider.d.ts +39 -15
- package/lib/utils/MetropolisProvider.d.ts.map +1 -0
- package/lib/utils/MetropolisProvider.js +31 -2
- package/lib/utils/actionFactory.d.ts +21 -19
- package/lib/utils/actionFactory.d.ts.map +1 -0
- package/lib/utils/actionFactory.js +95 -2
- package/lib/utils/api.d.ts +5 -4
- package/lib/utils/api.d.ts.map +1 -0
- package/lib/utils/api.js +252 -6
- package/lib/utils/app.d.ts +1 -0
- package/lib/utils/app.d.ts.map +1 -0
- package/lib/utils/app.js +15 -2
- package/lib/utils/baseActionFactory.d.ts +5 -23
- package/lib/utils/baseActionFactory.d.ts.map +1 -0
- package/lib/utils/baseActionFactory.js +72 -2
- package/lib/utils/configUtils.d.ts +11 -0
- package/lib/utils/configUtils.d.ts.map +1 -0
- package/lib/utils/configUtils.js +20 -0
- package/lib/utils/dateUtils.d.ts +1 -0
- package/lib/utils/dateUtils.d.ts.map +1 -0
- package/lib/utils/dateUtils.js +11 -2
- package/lib/utils/file.d.ts +3 -1
- package/lib/utils/file.d.ts.map +1 -0
- package/lib/utils/file.js +156 -2
- package/lib/utils/i18n.d.ts +7 -0
- package/lib/utils/i18n.d.ts.map +1 -0
- package/lib/utils/i18n.js +96 -0
- package/lib/utils/index.d.ts +20 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +26 -0
- package/lib/utils/location.d.ts +4 -3
- package/lib/utils/location.d.ts.map +1 -0
- package/lib/utils/location.js +82 -2
- package/lib/utils/permissionUtils.d.ts +22 -0
- package/lib/utils/permissionUtils.d.ts.map +1 -0
- package/lib/utils/permissionUtils.js +72 -0
- package/lib/utils/requestCache.d.ts +8 -0
- package/lib/utils/requestCache.d.ts.map +1 -0
- package/lib/utils/requestCache.js +53 -0
- package/lib/utils/session.d.ts +14 -0
- package/lib/utils/session.d.ts.map +1 -0
- package/lib/utils/session.js +107 -0
- package/lib/utils/useConversationTyping.d.ts +26 -0
- package/lib/utils/useConversationTyping.d.ts.map +1 -0
- package/lib/utils/useConversationTyping.js +117 -0
- package/lib/utils/useMetropolis.d.ts +67 -12
- package/lib/utils/useMetropolis.d.ts.map +1 -0
- package/lib/utils/useMetropolis.js +311 -2
- package/lib/utils/useTranslations.d.ts +16 -0
- package/lib/utils/useTranslations.d.ts.map +1 -0
- package/lib/utils/useTranslations.js +105 -0
- package/lib/utils/validatorFactory.d.ts +3 -2
- package/lib/utils/validatorFactory.d.ts.map +1 -0
- package/lib/utils/validatorFactory.js +95 -2
- package/package.json +58 -17
- package/temp_signin.ts +73 -0
- package/lib/actions/profileActions/profileActions.d.ts +0 -50
- package/lib/actions/profileActions/profileActions.js +0 -2
- package/lib/adapters/profileAdapter/profileAdapter.d.ts +0 -32
- package/lib/adapters/profileAdapter/profileAdapter.js +0 -2
- package/lib/stores/profileStore.d.ts +0 -24
- package/lib/stores/profileStore.js +0 -2
- /package/{FACTORY_PATTERN_GUIDE.md → factoryPatternGuide.md} +0 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2019-Present, Nitrogen Labs, Inc.
|
|
3
|
+
* Copyrights licensed under the MIT License. See the accompanying LICENSE file for terms.
|
|
4
|
+
*
|
|
5
|
+
* Extensibility and Custom Field Examples for MetropolisJS
|
|
6
|
+
*
|
|
7
|
+
* This file demonstrates how to use extensible fields and custom adapters
|
|
8
|
+
* to extend MetropolisJS functionality with:
|
|
9
|
+
* - Custom data validation and transformation
|
|
10
|
+
* - Type-safe extensible fields
|
|
11
|
+
* - Custom business logic in adapters
|
|
12
|
+
* - Advanced validation patterns
|
|
13
|
+
* - Metadata and custom field management
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, {useEffect, useState} from 'react';
|
|
17
|
+
import {useFlux} from '@nlabs/arkhamjs-utils-react';
|
|
18
|
+
import {
|
|
19
|
+
createPostActions,
|
|
20
|
+
createEventActions,
|
|
21
|
+
createGroupActions,
|
|
22
|
+
createUserActions,
|
|
23
|
+
type PostType,
|
|
24
|
+
type EventType,
|
|
25
|
+
type GroupType,
|
|
26
|
+
type PostActionsOptions,
|
|
27
|
+
type EventActionsOptions,
|
|
28
|
+
type GroupActionsOptions,
|
|
29
|
+
type BaseAdapterOptions
|
|
30
|
+
} from '@nlabs/metropolisjs';
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Custom Type Extensions
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
interface ExtendedPostType extends PostType {
|
|
37
|
+
metadata?: {
|
|
38
|
+
isSponsored?: boolean;
|
|
39
|
+
sponsorName?: string;
|
|
40
|
+
priority?: 'low' | 'medium' | 'high';
|
|
41
|
+
views?: number;
|
|
42
|
+
engagement?: {
|
|
43
|
+
likes?: number;
|
|
44
|
+
comments?: number;
|
|
45
|
+
shares?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
customFields?: {
|
|
49
|
+
campaign?: string;
|
|
50
|
+
targetAudience?: string[];
|
|
51
|
+
expiresAt?: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ExtendedEventType extends EventType {
|
|
56
|
+
metadata?: {
|
|
57
|
+
venue?: {
|
|
58
|
+
name: string;
|
|
59
|
+
address: string;
|
|
60
|
+
capacity: number;
|
|
61
|
+
};
|
|
62
|
+
ticketing?: {
|
|
63
|
+
price: number;
|
|
64
|
+
currency: string;
|
|
65
|
+
available: number;
|
|
66
|
+
};
|
|
67
|
+
categories?: string[];
|
|
68
|
+
};
|
|
69
|
+
customFields?: {
|
|
70
|
+
ageRestriction?: number;
|
|
71
|
+
dresscode?: string;
|
|
72
|
+
accessibility?: string[];
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ExtendedGroupType extends GroupType {
|
|
77
|
+
metadata?: {
|
|
78
|
+
settings?: {
|
|
79
|
+
requiresApproval?: boolean;
|
|
80
|
+
allowInvites?: boolean;
|
|
81
|
+
isVerified?: boolean;
|
|
82
|
+
};
|
|
83
|
+
stats?: {
|
|
84
|
+
totalPosts?: number;
|
|
85
|
+
activeMembers?: number;
|
|
86
|
+
created?: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
customFields?: {
|
|
90
|
+
industryTags?: string[];
|
|
91
|
+
companySize?: string;
|
|
92
|
+
website?: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Custom Adapter Examples
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
export const SponsoredPostAdapterExample = () => {
|
|
101
|
+
const flux = useFlux();
|
|
102
|
+
|
|
103
|
+
const customPostAdapter = (input: unknown, options?: BaseAdapterOptions): ExtendedPostType => {
|
|
104
|
+
const post = input as ExtendedPostType;
|
|
105
|
+
|
|
106
|
+
if(post.metadata?.isSponsored) {
|
|
107
|
+
if(!post.metadata.sponsorName) {
|
|
108
|
+
throw new Error('Sponsored posts must include sponsorName in metadata');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if(!post.customFields?.campaign) {
|
|
112
|
+
throw new Error('Sponsored posts must include a campaign ID');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if(!post.metadata.priority) {
|
|
116
|
+
post.metadata.priority = 'medium';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if(post.customFields?.expiresAt) {
|
|
121
|
+
if(post.customFields.expiresAt < Date.now()) {
|
|
122
|
+
throw new Error('Expiration date must be in the future');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return post;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const postActions = createPostActions(flux, {
|
|
130
|
+
postAdapter: customPostAdapter
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const [formData, setFormData] = useState<ExtendedPostType>({
|
|
134
|
+
content: '',
|
|
135
|
+
metadata: {
|
|
136
|
+
isSponsored: false,
|
|
137
|
+
sponsorName: '',
|
|
138
|
+
priority: 'medium'
|
|
139
|
+
},
|
|
140
|
+
customFields: {
|
|
141
|
+
campaign: '',
|
|
142
|
+
targetAudience: [],
|
|
143
|
+
expiresAt: undefined
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const [error, setError] = useState<string | null>(null);
|
|
148
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
149
|
+
|
|
150
|
+
const handleSubmit = async () => {
|
|
151
|
+
try {
|
|
152
|
+
setIsSubmitting(true);
|
|
153
|
+
setError(null);
|
|
154
|
+
|
|
155
|
+
const newPost = await postActions.add(formData, [
|
|
156
|
+
'postId',
|
|
157
|
+
'content',
|
|
158
|
+
'metadata',
|
|
159
|
+
'customFields',
|
|
160
|
+
'createdAt'
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
console.log('Sponsored post created:', newPost);
|
|
164
|
+
setFormData({
|
|
165
|
+
content: '',
|
|
166
|
+
metadata: {isSponsored: false, sponsorName: '', priority: 'medium'},
|
|
167
|
+
customFields: {campaign: '', targetAudience: [], expiresAt: undefined}
|
|
168
|
+
});
|
|
169
|
+
} catch(err: any) {
|
|
170
|
+
setError(err.message || 'Failed to create sponsored post');
|
|
171
|
+
console.error('Error creating sponsored post:', err);
|
|
172
|
+
} finally {
|
|
173
|
+
setIsSubmitting(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="sponsored-post-creator">
|
|
179
|
+
<h2>Create Sponsored Post</h2>
|
|
180
|
+
{error && <div className="error">{error}</div>}
|
|
181
|
+
|
|
182
|
+
<textarea
|
|
183
|
+
placeholder="Post content"
|
|
184
|
+
value={formData.content}
|
|
185
|
+
onChange={(e) => setFormData({...formData, content: e.target.value})}
|
|
186
|
+
rows={4}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
<label>
|
|
190
|
+
<input
|
|
191
|
+
type="checkbox"
|
|
192
|
+
checked={formData.metadata?.isSponsored}
|
|
193
|
+
onChange={(e) => setFormData({
|
|
194
|
+
...formData,
|
|
195
|
+
metadata: {...formData.metadata, isSponsored: e.target.checked}
|
|
196
|
+
})}
|
|
197
|
+
/>
|
|
198
|
+
This is a sponsored post
|
|
199
|
+
</label>
|
|
200
|
+
|
|
201
|
+
{formData.metadata?.isSponsored && (
|
|
202
|
+
<>
|
|
203
|
+
<input
|
|
204
|
+
type="text"
|
|
205
|
+
placeholder="Sponsor name (required)"
|
|
206
|
+
value={formData.metadata.sponsorName}
|
|
207
|
+
onChange={(e) => setFormData({
|
|
208
|
+
...formData,
|
|
209
|
+
metadata: {...formData.metadata, sponsorName: e.target.value}
|
|
210
|
+
})}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
<input
|
|
214
|
+
type="text"
|
|
215
|
+
placeholder="Campaign ID (required)"
|
|
216
|
+
value={formData.customFields?.campaign}
|
|
217
|
+
onChange={(e) => setFormData({
|
|
218
|
+
...formData,
|
|
219
|
+
customFields: {...formData.customFields, campaign: e.target.value}
|
|
220
|
+
})}
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<select
|
|
224
|
+
value={formData.metadata.priority}
|
|
225
|
+
onChange={(e) => setFormData({
|
|
226
|
+
...formData,
|
|
227
|
+
metadata: {...formData.metadata, priority: e.target.value as any}
|
|
228
|
+
})}
|
|
229
|
+
>
|
|
230
|
+
<option value="low">Low Priority</option>
|
|
231
|
+
<option value="medium">Medium Priority</option>
|
|
232
|
+
<option value="high">High Priority</option>
|
|
233
|
+
</select>
|
|
234
|
+
|
|
235
|
+
<input
|
|
236
|
+
type="datetime-local"
|
|
237
|
+
placeholder="Expires at"
|
|
238
|
+
onChange={(e) => setFormData({
|
|
239
|
+
...formData,
|
|
240
|
+
customFields: {
|
|
241
|
+
...formData.customFields,
|
|
242
|
+
expiresAt: new Date(e.target.value).getTime()
|
|
243
|
+
}
|
|
244
|
+
})}
|
|
245
|
+
/>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
<button onClick={handleSubmit} disabled={isSubmitting || !formData.content}>
|
|
250
|
+
{isSubmitting ? 'Creating...' : 'Create Post'}
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// =============================================================================
|
|
257
|
+
// Event Ticketing Adapter Example
|
|
258
|
+
// =============================================================================
|
|
259
|
+
|
|
260
|
+
export const EventTicketingAdapterExample = () => {
|
|
261
|
+
const flux = useFlux();
|
|
262
|
+
|
|
263
|
+
const customEventAdapter = (input: unknown, options?: BaseAdapterOptions): ExtendedEventType => {
|
|
264
|
+
const event = input as ExtendedEventType;
|
|
265
|
+
|
|
266
|
+
if(event.metadata?.ticketing) {
|
|
267
|
+
const {price, available} = event.metadata.ticketing;
|
|
268
|
+
|
|
269
|
+
if(price < 0) {
|
|
270
|
+
throw new Error('Ticket price cannot be negative');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if(available < 0) {
|
|
274
|
+
throw new Error('Available tickets cannot be negative');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if(event.metadata.venue?.capacity && available > event.metadata.venue.capacity) {
|
|
278
|
+
throw new Error('Available tickets cannot exceed venue capacity');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if(!event.metadata.ticketing.currency) {
|
|
282
|
+
event.metadata.ticketing.currency = 'USD';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if(event.customFields?.ageRestriction) {
|
|
287
|
+
if(event.customFields.ageRestriction < 0 || event.customFields.ageRestriction > 100) {
|
|
288
|
+
throw new Error('Age restriction must be between 0 and 100');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if(event.startDate && event.endDate && event.startDate > event.endDate) {
|
|
293
|
+
throw new Error('Event start date must be before end date');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return event;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const eventActions = createEventActions(flux, {
|
|
300
|
+
eventAdapter: customEventAdapter
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const [formData, setFormData] = useState<ExtendedEventType>({
|
|
304
|
+
name: '',
|
|
305
|
+
content: '',
|
|
306
|
+
startDate: undefined,
|
|
307
|
+
endDate: undefined,
|
|
308
|
+
metadata: {
|
|
309
|
+
venue: {
|
|
310
|
+
name: '',
|
|
311
|
+
address: '',
|
|
312
|
+
capacity: 0
|
|
313
|
+
},
|
|
314
|
+
ticketing: {
|
|
315
|
+
price: 0,
|
|
316
|
+
currency: 'USD',
|
|
317
|
+
available: 0
|
|
318
|
+
},
|
|
319
|
+
categories: []
|
|
320
|
+
},
|
|
321
|
+
customFields: {
|
|
322
|
+
ageRestriction: undefined,
|
|
323
|
+
dresscode: '',
|
|
324
|
+
accessibility: []
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const [error, setError] = useState<string | null>(null);
|
|
329
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
330
|
+
|
|
331
|
+
const handleSubmit = async () => {
|
|
332
|
+
try {
|
|
333
|
+
setIsSubmitting(true);
|
|
334
|
+
setError(null);
|
|
335
|
+
|
|
336
|
+
const newEvent = await eventActions.addEvent(formData, [
|
|
337
|
+
'eventId',
|
|
338
|
+
'name',
|
|
339
|
+
'content',
|
|
340
|
+
'startDate',
|
|
341
|
+
'endDate',
|
|
342
|
+
'metadata',
|
|
343
|
+
'customFields',
|
|
344
|
+
'createdAt'
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
console.log('Ticketed event created:', newEvent);
|
|
348
|
+
} catch(err: any) {
|
|
349
|
+
setError(err.message || 'Failed to create event');
|
|
350
|
+
console.error('Error creating event:', err);
|
|
351
|
+
} finally {
|
|
352
|
+
setIsSubmitting(false);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div className="ticketed-event-creator">
|
|
358
|
+
<h2>Create Ticketed Event</h2>
|
|
359
|
+
{error && <div className="error">{error}</div>}
|
|
360
|
+
|
|
361
|
+
<input
|
|
362
|
+
type="text"
|
|
363
|
+
placeholder="Event name"
|
|
364
|
+
value={formData.name}
|
|
365
|
+
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
366
|
+
/>
|
|
367
|
+
|
|
368
|
+
<textarea
|
|
369
|
+
placeholder="Event description"
|
|
370
|
+
value={formData.content}
|
|
371
|
+
onChange={(e) => setFormData({...formData, content: e.target.value})}
|
|
372
|
+
rows={4}
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
<div className="dates">
|
|
376
|
+
<input
|
|
377
|
+
type="datetime-local"
|
|
378
|
+
onChange={(e) => setFormData({
|
|
379
|
+
...formData,
|
|
380
|
+
startDate: new Date(e.target.value).getTime()
|
|
381
|
+
})}
|
|
382
|
+
/>
|
|
383
|
+
<input
|
|
384
|
+
type="datetime-local"
|
|
385
|
+
onChange={(e) => setFormData({
|
|
386
|
+
...formData,
|
|
387
|
+
endDate: new Date(e.target.value).getTime()
|
|
388
|
+
})}
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<h3>Venue Information</h3>
|
|
393
|
+
<input
|
|
394
|
+
type="text"
|
|
395
|
+
placeholder="Venue name"
|
|
396
|
+
value={formData.metadata?.venue?.name}
|
|
397
|
+
onChange={(e) => setFormData({
|
|
398
|
+
...formData,
|
|
399
|
+
metadata: {
|
|
400
|
+
...formData.metadata,
|
|
401
|
+
venue: {...formData.metadata?.venue, name: e.target.value}
|
|
402
|
+
}
|
|
403
|
+
})}
|
|
404
|
+
/>
|
|
405
|
+
|
|
406
|
+
<input
|
|
407
|
+
type="text"
|
|
408
|
+
placeholder="Venue address"
|
|
409
|
+
value={formData.metadata?.venue?.address}
|
|
410
|
+
onChange={(e) => setFormData({
|
|
411
|
+
...formData,
|
|
412
|
+
metadata: {
|
|
413
|
+
...formData.metadata,
|
|
414
|
+
venue: {...formData.metadata?.venue, address: e.target.value}
|
|
415
|
+
}
|
|
416
|
+
})}
|
|
417
|
+
/>
|
|
418
|
+
|
|
419
|
+
<input
|
|
420
|
+
type="number"
|
|
421
|
+
placeholder="Venue capacity"
|
|
422
|
+
value={formData.metadata?.venue?.capacity}
|
|
423
|
+
onChange={(e) => setFormData({
|
|
424
|
+
...formData,
|
|
425
|
+
metadata: {
|
|
426
|
+
...formData.metadata,
|
|
427
|
+
venue: {...formData.metadata?.venue, capacity: parseInt(e.target.value)}
|
|
428
|
+
}
|
|
429
|
+
})}
|
|
430
|
+
/>
|
|
431
|
+
|
|
432
|
+
<h3>Ticketing</h3>
|
|
433
|
+
<input
|
|
434
|
+
type="number"
|
|
435
|
+
placeholder="Ticket price"
|
|
436
|
+
value={formData.metadata?.ticketing?.price}
|
|
437
|
+
onChange={(e) => setFormData({
|
|
438
|
+
...formData,
|
|
439
|
+
metadata: {
|
|
440
|
+
...formData.metadata,
|
|
441
|
+
ticketing: {...formData.metadata?.ticketing, price: parseFloat(e.target.value)}
|
|
442
|
+
}
|
|
443
|
+
})}
|
|
444
|
+
step="0.01"
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
<select
|
|
448
|
+
value={formData.metadata?.ticketing?.currency}
|
|
449
|
+
onChange={(e) => setFormData({
|
|
450
|
+
...formData,
|
|
451
|
+
metadata: {
|
|
452
|
+
...formData.metadata,
|
|
453
|
+
ticketing: {...formData.metadata?.ticketing, currency: e.target.value}
|
|
454
|
+
}
|
|
455
|
+
})}
|
|
456
|
+
>
|
|
457
|
+
<option value="USD">USD</option>
|
|
458
|
+
<option value="EUR">EUR</option>
|
|
459
|
+
<option value="GBP">GBP</option>
|
|
460
|
+
<option value="JPY">JPY</option>
|
|
461
|
+
</select>
|
|
462
|
+
|
|
463
|
+
<input
|
|
464
|
+
type="number"
|
|
465
|
+
placeholder="Available tickets"
|
|
466
|
+
value={formData.metadata?.ticketing?.available}
|
|
467
|
+
onChange={(e) => setFormData({
|
|
468
|
+
...formData,
|
|
469
|
+
metadata: {
|
|
470
|
+
...formData.metadata,
|
|
471
|
+
ticketing: {...formData.metadata?.ticketing, available: parseInt(e.target.value)}
|
|
472
|
+
}
|
|
473
|
+
})}
|
|
474
|
+
/>
|
|
475
|
+
|
|
476
|
+
<h3>Additional Information</h3>
|
|
477
|
+
<input
|
|
478
|
+
type="number"
|
|
479
|
+
placeholder="Age restriction (optional)"
|
|
480
|
+
value={formData.customFields?.ageRestriction || ''}
|
|
481
|
+
onChange={(e) => setFormData({
|
|
482
|
+
...formData,
|
|
483
|
+
customFields: {...formData.customFields, ageRestriction: parseInt(e.target.value)}
|
|
484
|
+
})}
|
|
485
|
+
/>
|
|
486
|
+
|
|
487
|
+
<input
|
|
488
|
+
type="text"
|
|
489
|
+
placeholder="Dress code (optional)"
|
|
490
|
+
value={formData.customFields?.dresscode}
|
|
491
|
+
onChange={(e) => setFormData({
|
|
492
|
+
...formData,
|
|
493
|
+
customFields: {...formData.customFields, dresscode: e.target.value}
|
|
494
|
+
})}
|
|
495
|
+
/>
|
|
496
|
+
|
|
497
|
+
<button onClick={handleSubmit} disabled={isSubmitting || !formData.name}>
|
|
498
|
+
{isSubmitting ? 'Creating...' : 'Create Event'}
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// =============================================================================
|
|
505
|
+
// Group Settings Adapter Example
|
|
506
|
+
// =============================================================================
|
|
507
|
+
|
|
508
|
+
export const GroupSettingsAdapterExample = () => {
|
|
509
|
+
const flux = useFlux();
|
|
510
|
+
|
|
511
|
+
const customGroupAdapter = (input: unknown, options?: BaseAdapterOptions): ExtendedGroupType => {
|
|
512
|
+
const group = input as ExtendedGroupType;
|
|
513
|
+
|
|
514
|
+
if(group.metadata?.settings) {
|
|
515
|
+
const {requiresApproval, allowInvites, isVerified} = group.metadata.settings;
|
|
516
|
+
|
|
517
|
+
if(isVerified && group.privacy !== 'public') {
|
|
518
|
+
throw new Error('Verified groups must be public');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if(!allowInvites && requiresApproval) {
|
|
522
|
+
console.warn('Group does not allow invites but requires approval - may limit growth');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if(group.customFields?.industryTags) {
|
|
527
|
+
if(group.customFields.industryTags.length > 10) {
|
|
528
|
+
throw new Error('Cannot have more than 10 industry tags');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if(group.customFields?.website) {
|
|
533
|
+
try {
|
|
534
|
+
new URL(group.customFields.website);
|
|
535
|
+
} catch {
|
|
536
|
+
throw new Error('Invalid website URL format');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if(!group.metadata) {
|
|
541
|
+
group.metadata = {
|
|
542
|
+
settings: {
|
|
543
|
+
requiresApproval: false,
|
|
544
|
+
allowInvites: true,
|
|
545
|
+
isVerified: false
|
|
546
|
+
},
|
|
547
|
+
stats: {
|
|
548
|
+
totalPosts: 0,
|
|
549
|
+
activeMembers: 0,
|
|
550
|
+
created: Date.now()
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return group;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const groupActions = createGroupActions(flux, {
|
|
559
|
+
groupAdapter: customGroupAdapter
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const [formData, setFormData] = useState<ExtendedGroupType>({
|
|
563
|
+
name: '',
|
|
564
|
+
description: '',
|
|
565
|
+
privacy: 'public',
|
|
566
|
+
metadata: {
|
|
567
|
+
settings: {
|
|
568
|
+
requiresApproval: false,
|
|
569
|
+
allowInvites: true,
|
|
570
|
+
isVerified: false
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
customFields: {
|
|
574
|
+
industryTags: [],
|
|
575
|
+
companySize: '',
|
|
576
|
+
website: ''
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const [error, setError] = useState<string | null>(null);
|
|
581
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
582
|
+
|
|
583
|
+
const handleSubmit = async () => {
|
|
584
|
+
try {
|
|
585
|
+
setIsSubmitting(true);
|
|
586
|
+
setError(null);
|
|
587
|
+
|
|
588
|
+
const newGroup = await groupActions.add(formData, [
|
|
589
|
+
'groupId',
|
|
590
|
+
'name',
|
|
591
|
+
'description',
|
|
592
|
+
'privacy',
|
|
593
|
+
'metadata',
|
|
594
|
+
'customFields',
|
|
595
|
+
'createdAt'
|
|
596
|
+
]);
|
|
597
|
+
|
|
598
|
+
console.log('Group with custom settings created:', newGroup);
|
|
599
|
+
} catch(err: any) {
|
|
600
|
+
setError(err.message || 'Failed to create group');
|
|
601
|
+
console.error('Error creating group:', err);
|
|
602
|
+
} finally {
|
|
603
|
+
setIsSubmitting(false);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
<div className="group-settings-creator">
|
|
609
|
+
<h2>Create Professional Group</h2>
|
|
610
|
+
{error && <div className="error">{error}</div>}
|
|
611
|
+
|
|
612
|
+
<input
|
|
613
|
+
type="text"
|
|
614
|
+
placeholder="Group name"
|
|
615
|
+
value={formData.name}
|
|
616
|
+
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
617
|
+
/>
|
|
618
|
+
|
|
619
|
+
<textarea
|
|
620
|
+
placeholder="Group description"
|
|
621
|
+
value={formData.description}
|
|
622
|
+
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
623
|
+
rows={3}
|
|
624
|
+
/>
|
|
625
|
+
|
|
626
|
+
<select
|
|
627
|
+
value={formData.privacy}
|
|
628
|
+
onChange={(e) => setFormData({...formData, privacy: e.target.value})}
|
|
629
|
+
>
|
|
630
|
+
<option value="public">Public</option>
|
|
631
|
+
<option value="private">Private</option>
|
|
632
|
+
</select>
|
|
633
|
+
|
|
634
|
+
<h3>Group Settings</h3>
|
|
635
|
+
<label>
|
|
636
|
+
<input
|
|
637
|
+
type="checkbox"
|
|
638
|
+
checked={formData.metadata?.settings?.requiresApproval}
|
|
639
|
+
onChange={(e) => setFormData({
|
|
640
|
+
...formData,
|
|
641
|
+
metadata: {
|
|
642
|
+
...formData.metadata,
|
|
643
|
+
settings: {...formData.metadata?.settings, requiresApproval: e.target.checked}
|
|
644
|
+
}
|
|
645
|
+
})}
|
|
646
|
+
/>
|
|
647
|
+
Requires approval to join
|
|
648
|
+
</label>
|
|
649
|
+
|
|
650
|
+
<label>
|
|
651
|
+
<input
|
|
652
|
+
type="checkbox"
|
|
653
|
+
checked={formData.metadata?.settings?.allowInvites}
|
|
654
|
+
onChange={(e) => setFormData({
|
|
655
|
+
...formData,
|
|
656
|
+
metadata: {
|
|
657
|
+
...formData.metadata,
|
|
658
|
+
settings: {...formData.metadata?.settings, allowInvites: e.target.checked}
|
|
659
|
+
}
|
|
660
|
+
})}
|
|
661
|
+
/>
|
|
662
|
+
Allow member invites
|
|
663
|
+
</label>
|
|
664
|
+
|
|
665
|
+
<label>
|
|
666
|
+
<input
|
|
667
|
+
type="checkbox"
|
|
668
|
+
checked={formData.metadata?.settings?.isVerified}
|
|
669
|
+
onChange={(e) => setFormData({
|
|
670
|
+
...formData,
|
|
671
|
+
metadata: {
|
|
672
|
+
...formData.metadata,
|
|
673
|
+
settings: {...formData.metadata?.settings, isVerified: e.target.checked}
|
|
674
|
+
}
|
|
675
|
+
})}
|
|
676
|
+
disabled={formData.privacy !== 'public'}
|
|
677
|
+
/>
|
|
678
|
+
Verified group (public only)
|
|
679
|
+
</label>
|
|
680
|
+
|
|
681
|
+
<h3>Professional Information</h3>
|
|
682
|
+
<input
|
|
683
|
+
type="text"
|
|
684
|
+
placeholder="Industry tags (comma-separated)"
|
|
685
|
+
onChange={(e) => setFormData({
|
|
686
|
+
...formData,
|
|
687
|
+
customFields: {
|
|
688
|
+
...formData.customFields,
|
|
689
|
+
industryTags: e.target.value.split(',').map(t => t.trim()).filter(t => t)
|
|
690
|
+
}
|
|
691
|
+
})}
|
|
692
|
+
/>
|
|
693
|
+
|
|
694
|
+
<select
|
|
695
|
+
value={formData.customFields?.companySize}
|
|
696
|
+
onChange={(e) => setFormData({
|
|
697
|
+
...formData,
|
|
698
|
+
customFields: {...formData.customFields, companySize: e.target.value}
|
|
699
|
+
})}
|
|
700
|
+
>
|
|
701
|
+
<option value="">Select company size</option>
|
|
702
|
+
<option value="1-10">1-10 employees</option>
|
|
703
|
+
<option value="11-50">11-50 employees</option>
|
|
704
|
+
<option value="51-200">51-200 employees</option>
|
|
705
|
+
<option value="201-500">201-500 employees</option>
|
|
706
|
+
<option value="501+">501+ employees</option>
|
|
707
|
+
</select>
|
|
708
|
+
|
|
709
|
+
<input
|
|
710
|
+
type="url"
|
|
711
|
+
placeholder="Website URL (optional)"
|
|
712
|
+
value={formData.customFields?.website}
|
|
713
|
+
onChange={(e) => setFormData({
|
|
714
|
+
...formData,
|
|
715
|
+
customFields: {...formData.customFields, website: e.target.value}
|
|
716
|
+
})}
|
|
717
|
+
/>
|
|
718
|
+
|
|
719
|
+
<button onClick={handleSubmit} disabled={isSubmitting || !formData.name}>
|
|
720
|
+
{isSubmitting ? 'Creating...' : 'Create Group'}
|
|
721
|
+
</button>
|
|
722
|
+
</div>
|
|
723
|
+
);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// =============================================================================
|
|
727
|
+
// Dynamic Adapter Switching Example
|
|
728
|
+
// =============================================================================
|
|
729
|
+
|
|
730
|
+
export const DynamicAdapterExample = () => {
|
|
731
|
+
const flux = useFlux();
|
|
732
|
+
|
|
733
|
+
const [adapterMode, setAdapterMode] = useState<'strict' | 'lenient'>('strict');
|
|
734
|
+
|
|
735
|
+
const strictAdapter = (input: unknown): PostType => {
|
|
736
|
+
const post = input as PostType;
|
|
737
|
+
|
|
738
|
+
if(!post.content || post.content.trim().length === 0) {
|
|
739
|
+
throw new Error('Content is required and cannot be empty');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if(post.content.length > 5000) {
|
|
743
|
+
throw new Error('Content cannot exceed 5000 characters');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if(post.content.includes('<script>')) {
|
|
747
|
+
throw new Error('Content contains disallowed HTML');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return post;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const lenientAdapter = (input: unknown): PostType => {
|
|
754
|
+
const post = input as PostType;
|
|
755
|
+
|
|
756
|
+
if(!post.content) {
|
|
757
|
+
post.content = '[No content]';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if(post.content.length > 5000) {
|
|
761
|
+
post.content = post.content.substring(0, 5000) + '...';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return post;
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const postActions = createPostActions(flux, {
|
|
768
|
+
postAdapter: adapterMode === 'strict' ? strictAdapter : lenientAdapter
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const [content, setContent] = useState('');
|
|
772
|
+
const [error, setError] = useState<string | null>(null);
|
|
773
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
774
|
+
|
|
775
|
+
const handleSubmit = async () => {
|
|
776
|
+
try {
|
|
777
|
+
setIsSubmitting(true);
|
|
778
|
+
setError(null);
|
|
779
|
+
|
|
780
|
+
const newPost = await postActions.add({content}, ['postId', 'content', 'createdAt']);
|
|
781
|
+
|
|
782
|
+
console.log('Post created with', adapterMode, 'validation:', newPost);
|
|
783
|
+
setContent('');
|
|
784
|
+
} catch(err: any) {
|
|
785
|
+
setError(err.message || 'Failed to create post');
|
|
786
|
+
console.error('Error creating post:', err);
|
|
787
|
+
} finally {
|
|
788
|
+
setIsSubmitting(false);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
<div className="dynamic-adapter-example">
|
|
794
|
+
<h2>Dynamic Adapter Switching</h2>
|
|
795
|
+
|
|
796
|
+
<div className="mode-selector">
|
|
797
|
+
<label>
|
|
798
|
+
<input
|
|
799
|
+
type="radio"
|
|
800
|
+
value="strict"
|
|
801
|
+
checked={adapterMode === 'strict'}
|
|
802
|
+
onChange={() => setAdapterMode('strict')}
|
|
803
|
+
/>
|
|
804
|
+
Strict Mode (Throws errors)
|
|
805
|
+
</label>
|
|
806
|
+
<label>
|
|
807
|
+
<input
|
|
808
|
+
type="radio"
|
|
809
|
+
value="lenient"
|
|
810
|
+
checked={adapterMode === 'lenient'}
|
|
811
|
+
onChange={() => setAdapterMode('lenient')}
|
|
812
|
+
/>
|
|
813
|
+
Lenient Mode (Auto-corrects)
|
|
814
|
+
</label>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
{error && <div className="error">{error}</div>}
|
|
818
|
+
|
|
819
|
+
<textarea
|
|
820
|
+
placeholder="Post content"
|
|
821
|
+
value={content}
|
|
822
|
+
onChange={(e) => setContent(e.target.value)}
|
|
823
|
+
rows={4}
|
|
824
|
+
/>
|
|
825
|
+
|
|
826
|
+
<div className="info">
|
|
827
|
+
<small>
|
|
828
|
+
Current mode: {adapterMode === 'strict' ? 'Strict validation' : 'Auto-correction enabled'}
|
|
829
|
+
</small>
|
|
830
|
+
<small>Characters: {content.length}</small>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<button onClick={handleSubmit} disabled={isSubmitting}>
|
|
834
|
+
{isSubmitting ? 'Creating...' : 'Create Post'}
|
|
835
|
+
</button>
|
|
836
|
+
</div>
|
|
837
|
+
);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// =============================================================================
|
|
841
|
+
// Complete Extensibility Example
|
|
842
|
+
// =============================================================================
|
|
843
|
+
|
|
844
|
+
export const CompleteExtensibilityExample = () => {
|
|
845
|
+
const [activeExample, setActiveExample] = useState<'sponsored' | 'ticketing' | 'settings' | 'dynamic'>('sponsored');
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className="extensibility-examples">
|
|
849
|
+
<h1>MetropolisJS Extensibility Examples</h1>
|
|
850
|
+
|
|
851
|
+
<nav>
|
|
852
|
+
<button onClick={() => setActiveExample('sponsored')}>Sponsored Posts</button>
|
|
853
|
+
<button onClick={() => setActiveExample('ticketing')}>Event Ticketing</button>
|
|
854
|
+
<button onClick={() => setActiveExample('settings')}>Group Settings</button>
|
|
855
|
+
<button onClick={() => setActiveExample('dynamic')}>Dynamic Adapters</button>
|
|
856
|
+
</nav>
|
|
857
|
+
|
|
858
|
+
<main>
|
|
859
|
+
{activeExample === 'sponsored' && <SponsoredPostAdapterExample />}
|
|
860
|
+
{activeExample === 'ticketing' && <EventTicketingAdapterExample />}
|
|
861
|
+
{activeExample === 'settings' && <GroupSettingsAdapterExample />}
|
|
862
|
+
{activeExample === 'dynamic' && <DynamicAdapterExample />}
|
|
863
|
+
</main>
|
|
864
|
+
|
|
865
|
+
<section className="documentation">
|
|
866
|
+
<h2>Key Concepts</h2>
|
|
867
|
+
<ul>
|
|
868
|
+
<li>
|
|
869
|
+
<strong>Custom Adapters:</strong> Transform and validate data before API calls
|
|
870
|
+
</li>
|
|
871
|
+
<li>
|
|
872
|
+
<strong>Extensible Fields:</strong> Use metadata and customFields for domain-specific data
|
|
873
|
+
</li>
|
|
874
|
+
<li>
|
|
875
|
+
<strong>Type Safety:</strong> Extend base types for compile-time validation
|
|
876
|
+
</li>
|
|
877
|
+
<li>
|
|
878
|
+
<strong>Business Logic:</strong> Implement complex validation rules in adapters
|
|
879
|
+
</li>
|
|
880
|
+
<li>
|
|
881
|
+
<strong>Dynamic Behavior:</strong> Switch adapters at runtime for different modes
|
|
882
|
+
</li>
|
|
883
|
+
</ul>
|
|
884
|
+
</section>
|
|
885
|
+
</div>
|
|
886
|
+
);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
export default CompleteExtensibilityExample;
|