refactor(sdk): split hooks, dsr-portal, provider, sync approaching 500 LOC
All four files split into focused sibling modules so every file lands
comfortably under the 300-LOC soft target (hard cap 500):
hooks.ts (474→43) → hooks-core / hooks-dsgvo / hooks-compliance
hooks-rag-security / hooks-ui
dsr-portal.ts (464→129) → dsr-portal-translations / dsr-portal-render
provider.tsx (462→247) → provider-effects / provider-callbacks
sync.ts (435→299) → sync-storage / sync-conflict
Zero behaviour changes. All public APIs remain importable from the
original paths (hooks.ts re-exports every hook, provider.tsx keeps all
named exports, sync.ts preserves StateSyncManager + factory).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
67
breakpilot-compliance-sdk/packages/core/src/sync-conflict.ts
Normal file
67
breakpilot-compliance-sdk/packages/core/src/sync-conflict.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Conflict resolution helpers for StateSyncManager.
|
||||
*
|
||||
* Extracted from sync.ts to stay within the 300-LOC target.
|
||||
*/
|
||||
|
||||
import type { SDKState, ConflictResolution } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULT CONFLICT HANDLER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Default strategy: if local is newer, keep local; otherwise merge
|
||||
* server as the base but preserve local preferences and deduplicate
|
||||
* commandBarHistory / recentSearches.
|
||||
*/
|
||||
export async function defaultConflictHandler(
|
||||
local: SDKState,
|
||||
server: SDKState
|
||||
): Promise<ConflictResolution> {
|
||||
const localTime = new Date(local.lastModified).getTime()
|
||||
const serverTime = new Date(server.lastModified).getTime()
|
||||
|
||||
if (localTime > serverTime) {
|
||||
return { strategy: 'local' }
|
||||
}
|
||||
|
||||
const mergedState: SDKState = {
|
||||
...server,
|
||||
preferences: local.preferences,
|
||||
commandBarHistory: [
|
||||
...local.commandBarHistory,
|
||||
...server.commandBarHistory.filter(
|
||||
h => !local.commandBarHistory.some(lh => lh.id === h.id)
|
||||
),
|
||||
].slice(0, 50),
|
||||
recentSearches: [...new Set([...local.recentSearches, ...server.recentSearches])].slice(
|
||||
0,
|
||||
20
|
||||
),
|
||||
}
|
||||
|
||||
return { strategy: 'merge', mergedState }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFLICT RESOLUTION APPLIER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Given a resolution strategy and both states, returns the winning state.
|
||||
*/
|
||||
export function applyConflictResolution(
|
||||
resolution: ConflictResolution,
|
||||
localState: SDKState,
|
||||
serverState: SDKState
|
||||
): SDKState {
|
||||
switch (resolution.strategy) {
|
||||
case 'local':
|
||||
return localState
|
||||
case 'server':
|
||||
return serverState
|
||||
case 'merge':
|
||||
return resolution.mergedState || localState
|
||||
}
|
||||
}
|
||||
154
breakpilot-compliance-sdk/packages/core/src/sync-storage.ts
Normal file
154
breakpilot-compliance-sdk/packages/core/src/sync-storage.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Local-storage and BroadcastChannel helpers for StateSyncManager.
|
||||
*
|
||||
* Extracted from sync.ts to stay within the 300-LOC target.
|
||||
*/
|
||||
|
||||
import type { SDKState, SyncState } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export const STORAGE_KEY_PREFIX = 'breakpilot-compliance-sdk-state'
|
||||
export const SYNC_CHANNEL = 'breakpilot-sdk-state-sync'
|
||||
|
||||
// =============================================================================
|
||||
// TAB ID
|
||||
// =============================================================================
|
||||
|
||||
export function getTabId(): string {
|
||||
if (typeof window === 'undefined') return 'server'
|
||||
|
||||
let tabId = sessionStorage.getItem('breakpilot-sdk-tab-id')
|
||||
if (!tabId) {
|
||||
tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
sessionStorage.setItem('breakpilot-sdk-tab-id', tabId)
|
||||
}
|
||||
return tabId
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LOCAL STORAGE HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function getStorageKey(tenantId: string): string {
|
||||
return `${STORAGE_KEY_PREFIX}-${tenantId}`
|
||||
}
|
||||
|
||||
export function saveStateToLocalStorage(
|
||||
tenantId: string,
|
||||
state: SDKState,
|
||||
version: number
|
||||
): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
const data = {
|
||||
state,
|
||||
version,
|
||||
savedAt: new Date().toISOString(),
|
||||
}
|
||||
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function loadStateFromLocalStorage(
|
||||
tenantId: string,
|
||||
syncState: SyncState
|
||||
): SDKState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(getStorageKey(tenantId))
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored)
|
||||
syncState.localVersion = data.version || 0
|
||||
return data.state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function clearStateFromLocalStorage(tenantId: string): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.removeItem(getStorageKey(tenantId))
|
||||
} catch (error) {
|
||||
console.error('Failed to clear localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BROADCAST CHANNEL HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function createBroadcastChannel(
|
||||
tenantId: string
|
||||
): BroadcastChannel | null {
|
||||
if (typeof window === 'undefined' || !('BroadcastChannel' in window)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return new BroadcastChannel(`${SYNC_CHANNEL}-${tenantId}`)
|
||||
} catch (error) {
|
||||
console.warn('BroadcastChannel not available:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function broadcastStateUpdate(
|
||||
channel: BroadcastChannel | null,
|
||||
state: SDKState,
|
||||
version: number
|
||||
): void {
|
||||
if (!channel) return
|
||||
|
||||
channel.postMessage({
|
||||
type: 'STATE_UPDATED',
|
||||
state,
|
||||
version,
|
||||
tabId: getTabId(),
|
||||
})
|
||||
}
|
||||
|
||||
export function broadcastSyncComplete(
|
||||
channel: BroadcastChannel | null,
|
||||
version: number
|
||||
): void {
|
||||
if (!channel) return
|
||||
|
||||
channel.postMessage({
|
||||
type: 'SYNC_COMPLETE',
|
||||
version,
|
||||
tabId: getTabId(),
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ONLINE / OFFLINE LISTENER
|
||||
// =============================================================================
|
||||
|
||||
export interface OnlineListenerHandlers {
|
||||
onOnline: () => void
|
||||
onOffline: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers window online/offline listeners and returns the current online status.
|
||||
* No-ops in non-browser environments.
|
||||
*/
|
||||
export function setupOnlineListener(handlers: OnlineListenerHandlers): boolean {
|
||||
if (typeof window === 'undefined') return true
|
||||
|
||||
window.addEventListener('online', handlers.onOnline)
|
||||
window.addEventListener('offline', handlers.onOffline)
|
||||
|
||||
return navigator.onLine
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
/**
|
||||
* SDK State Synchronization
|
||||
*
|
||||
* Handles offline/online sync, multi-tab coordination,
|
||||
* and conflict resolution for SDK state.
|
||||
*/
|
||||
// SDK State Synchronization — offline/online sync, multi-tab coordination.
|
||||
// Split: localStorage+BroadcastChannel → sync-storage.ts | conflict → sync-conflict.ts
|
||||
|
||||
import type { SDKState, SyncState, SyncStatus, ConflictResolution } from '@breakpilot/compliance-sdk-types'
|
||||
import { ComplianceClient } from './client'
|
||||
import {
|
||||
saveStateToLocalStorage,
|
||||
loadStateFromLocalStorage,
|
||||
clearStateFromLocalStorage,
|
||||
createBroadcastChannel,
|
||||
broadcastStateUpdate,
|
||||
broadcastSyncComplete,
|
||||
setupOnlineListener,
|
||||
} from './sync-storage'
|
||||
import { defaultConflictHandler, applyConflictResolution } from './sync-conflict'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
// --- Types & Constants -------------------------------------------------------
|
||||
|
||||
export interface SyncOptions {
|
||||
debounceMs?: number
|
||||
maxRetries?: number
|
||||
conflictHandler?: (local: SDKState, server: SDKState) => Promise<ConflictResolution>
|
||||
}
|
||||
|
||||
export interface SyncCallbacks {
|
||||
onSyncStart?: () => void
|
||||
onSyncComplete?: (state: SDKState) => void
|
||||
@@ -27,18 +30,10 @@ export interface SyncCallbacks {
|
||||
onOnline?: () => void
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'breakpilot-compliance-sdk-state'
|
||||
const SYNC_CHANNEL = 'breakpilot-sdk-state-sync'
|
||||
const DEFAULT_DEBOUNCE_MS = 2000
|
||||
const DEFAULT_MAX_RETRIES = 3
|
||||
|
||||
// =============================================================================
|
||||
// STATE SYNC MANAGER
|
||||
// =============================================================================
|
||||
// --- StateSyncManager --------------------------------------------------------
|
||||
|
||||
export class StateSyncManager {
|
||||
private client: ComplianceClient
|
||||
@@ -63,7 +58,7 @@ export class StateSyncManager {
|
||||
this.options = {
|
||||
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
||||
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
||||
conflictHandler: options.conflictHandler ?? this.defaultConflictHandler.bind(this),
|
||||
conflictHandler: options.conflictHandler ?? defaultConflictHandler,
|
||||
}
|
||||
|
||||
this.syncState = {
|
||||
@@ -79,52 +74,33 @@ export class StateSyncManager {
|
||||
this.setupOnlineListener()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
// -- Setup --
|
||||
|
||||
private setupBroadcastChannel(): void {
|
||||
if (typeof window === 'undefined' || !('BroadcastChannel' in window)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
|
||||
this.broadcastChannel = createBroadcastChannel(this.tenantId)
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
|
||||
} catch (error) {
|
||||
console.warn('BroadcastChannel not available:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private setupOnlineListener(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true
|
||||
this.syncState.status = 'idle'
|
||||
this.callbacks.onOnline?.()
|
||||
if (this.pendingState) {
|
||||
this.syncToServer(this.pendingState)
|
||||
}
|
||||
this.isOnline = setupOnlineListener({
|
||||
onOnline: () => {
|
||||
this.isOnline = true
|
||||
this.syncState.status = 'idle'
|
||||
this.callbacks.onOnline?.()
|
||||
if (this.pendingState) this.syncToServer(this.pendingState)
|
||||
},
|
||||
onOffline: () => {
|
||||
this.isOnline = false
|
||||
this.syncState.status = 'offline'
|
||||
this.callbacks.onOffline?.()
|
||||
},
|
||||
})
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false
|
||||
this.syncState.status = 'offline'
|
||||
this.callbacks.onOffline?.()
|
||||
})
|
||||
|
||||
this.isOnline = navigator.onLine
|
||||
if (!this.isOnline) {
|
||||
this.syncState.status = 'offline'
|
||||
}
|
||||
if (!this.isOnline) this.syncState.status = 'offline'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Broadcast Channel Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
// -- Broadcast Channel --
|
||||
|
||||
private handleBroadcastMessage(event: MessageEvent): void {
|
||||
const { type, state, version } = event.data
|
||||
@@ -143,98 +119,33 @@ export class StateSyncManager {
|
||||
break
|
||||
|
||||
case 'REQUEST_STATE':
|
||||
this.broadcastState()
|
||||
this.broadcastCurrentState()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private broadcastState(): void {
|
||||
if (!this.broadcastChannel) return
|
||||
|
||||
private broadcastCurrentState(): void {
|
||||
const state = this.loadFromLocalStorage()
|
||||
if (state) {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: 'STATE_UPDATED',
|
||||
state,
|
||||
version: this.syncState.localVersion,
|
||||
tabId: this.getTabId(),
|
||||
})
|
||||
broadcastStateUpdate(this.broadcastChannel, state, this.syncState.localVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private broadcastSyncComplete(version: number): void {
|
||||
if (!this.broadcastChannel) return
|
||||
|
||||
this.broadcastChannel.postMessage({
|
||||
type: 'SYNC_COMPLETE',
|
||||
version,
|
||||
tabId: this.getTabId(),
|
||||
})
|
||||
}
|
||||
|
||||
private getTabId(): string {
|
||||
if (typeof window === 'undefined') return 'server'
|
||||
|
||||
let tabId = sessionStorage.getItem('breakpilot-sdk-tab-id')
|
||||
if (!tabId) {
|
||||
tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
sessionStorage.setItem('breakpilot-sdk-tab-id', tabId)
|
||||
}
|
||||
return tabId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local Storage Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getStorageKey(): string {
|
||||
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
|
||||
}
|
||||
// -- Local Storage (sync-storage helpers) --
|
||||
|
||||
saveToLocalStorage(state: SDKState): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
const data = {
|
||||
state,
|
||||
version: this.syncState.localVersion,
|
||||
savedAt: new Date().toISOString(),
|
||||
}
|
||||
localStorage.setItem(this.getStorageKey(), JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error)
|
||||
}
|
||||
saveStateToLocalStorage(this.tenantId, state, this.syncState.localVersion)
|
||||
}
|
||||
|
||||
loadFromLocalStorage(): SDKState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(this.getStorageKey())
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored)
|
||||
this.syncState.localVersion = data.version || 0
|
||||
return data.state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
return loadStateFromLocalStorage(this.tenantId, this.syncState)
|
||||
}
|
||||
|
||||
clearLocalStorage(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.removeItem(this.getStorageKey())
|
||||
} catch (error) {
|
||||
console.error('Failed to clear localStorage:', error)
|
||||
}
|
||||
clearStateFromLocalStorage(this.tenantId)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
// -- Sync --
|
||||
|
||||
queueSync(state: SDKState): void {
|
||||
this.pendingState = state
|
||||
@@ -242,7 +153,7 @@ export class StateSyncManager {
|
||||
|
||||
this.syncState.localVersion++
|
||||
this.saveToLocalStorage(state)
|
||||
this.broadcastState()
|
||||
broadcastStateUpdate(this.broadcastChannel, state, this.syncState.localVersion)
|
||||
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout)
|
||||
@@ -284,7 +195,7 @@ export class StateSyncManager {
|
||||
}
|
||||
|
||||
this.pendingState = null
|
||||
this.broadcastSyncComplete(response.version)
|
||||
broadcastSyncComplete(this.broadcastChannel, response.version)
|
||||
this.callbacks.onSyncComplete?.(state)
|
||||
} catch (error) {
|
||||
if ((error as { status?: number }).status === 409) {
|
||||
@@ -319,9 +230,7 @@ export class StateSyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conflict Resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
// -- Conflict Resolution (sync-conflict helpers) --
|
||||
|
||||
private async handleConflict(localState: SDKState): Promise<void> {
|
||||
this.syncState.status = 'conflict'
|
||||
@@ -338,19 +247,7 @@ export class StateSyncManager {
|
||||
this.callbacks.onConflict?.(localState, serverState)
|
||||
|
||||
const resolution = await this.options.conflictHandler(localState, serverState)
|
||||
|
||||
let resolvedState: SDKState
|
||||
switch (resolution.strategy) {
|
||||
case 'local':
|
||||
resolvedState = localState
|
||||
break
|
||||
case 'server':
|
||||
resolvedState = serverState
|
||||
break
|
||||
case 'merge':
|
||||
resolvedState = resolution.mergedState || localState
|
||||
break
|
||||
}
|
||||
const resolvedState = applyConflictResolution(resolution, localState, serverState)
|
||||
|
||||
const response = await this.client.saveState(resolvedState)
|
||||
this.syncState.serverVersion = response.version
|
||||
@@ -365,38 +262,7 @@ export class StateSyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async defaultConflictHandler(
|
||||
local: SDKState,
|
||||
server: SDKState
|
||||
): Promise<ConflictResolution> {
|
||||
const localTime = new Date(local.lastModified).getTime()
|
||||
const serverTime = new Date(server.lastModified).getTime()
|
||||
|
||||
if (localTime > serverTime) {
|
||||
return { strategy: 'local' }
|
||||
}
|
||||
|
||||
const mergedState: SDKState = {
|
||||
...server,
|
||||
preferences: local.preferences,
|
||||
commandBarHistory: [
|
||||
...local.commandBarHistory,
|
||||
...server.commandBarHistory.filter(
|
||||
h => !local.commandBarHistory.some(lh => lh.id === h.id)
|
||||
),
|
||||
].slice(0, 50),
|
||||
recentSearches: [...new Set([...local.recentSearches, ...server.recentSearches])].slice(
|
||||
0,
|
||||
20
|
||||
),
|
||||
}
|
||||
|
||||
return { strategy: 'merge', mergedState }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Getters & Cleanup
|
||||
// ---------------------------------------------------------------------------
|
||||
// -- Getters & Cleanup --
|
||||
|
||||
getSyncState(): SyncState {
|
||||
return { ...this.syncState }
|
||||
@@ -421,9 +287,7 @@ export class StateSyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FACTORY
|
||||
// =============================================================================
|
||||
// --- Factory -----------------------------------------------------------------
|
||||
|
||||
export function createStateSyncManager(
|
||||
client: ComplianceClient,
|
||||
|
||||
164
breakpilot-compliance-sdk/packages/react/src/hooks-compliance.ts
Normal file
164
breakpilot-compliance-sdk/packages/react/src/hooks-compliance.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import { useCompliance } from './hooks-core'
|
||||
import type { Control, Evidence, Risk } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// COMPLIANCE HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceModule() {
|
||||
const { compliance, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// Controls
|
||||
controls: state.controls,
|
||||
getControlById: compliance.getControlById.bind(compliance),
|
||||
getControlsByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getControlsByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
controlComplianceRate: compliance.getControlComplianceRate(),
|
||||
|
||||
// Evidence
|
||||
evidence: state.evidence,
|
||||
getEvidenceByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
|
||||
// Requirements
|
||||
requirements: state.requirements,
|
||||
getRequirementsByRegulation: compliance.getRequirementsByRegulation.bind(compliance),
|
||||
requirementComplianceRate: compliance.getRequirementComplianceRate(),
|
||||
|
||||
// Obligations
|
||||
obligations: state.obligations,
|
||||
upcomingObligations: compliance.getUpcomingObligations(),
|
||||
overdueObligations: compliance.getOverdueObligations(),
|
||||
|
||||
// AI Act
|
||||
aiActClassification: state.aiActClassification,
|
||||
aiActRiskCategory: compliance.getAIActRiskCategory(),
|
||||
isHighRiskAI: compliance.isHighRiskAI(),
|
||||
|
||||
// Score
|
||||
complianceScore: compliance.calculateComplianceScore(),
|
||||
|
||||
// Risks
|
||||
risks: state.risks,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
averageRiskScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[compliance, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useControls() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addControl = useCallback(
|
||||
(control: Control) => {
|
||||
dispatch({ type: 'ADD_CONTROL', payload: control })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateControl = useCallback(
|
||||
(id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
controls: state.controls,
|
||||
addControl,
|
||||
updateControl,
|
||||
getById: compliance.getControlById.bind(compliance),
|
||||
getByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
implementedCount: state.controls.filter(c => c.implementationStatus === 'IMPLEMENTED').length,
|
||||
totalCount: state.controls.length,
|
||||
complianceRate: compliance.getControlComplianceRate(),
|
||||
}),
|
||||
[state.controls, compliance, addControl, updateControl]
|
||||
)
|
||||
}
|
||||
|
||||
export function useEvidence() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addEvidence = useCallback(
|
||||
(evidence: Evidence) => {
|
||||
dispatch({ type: 'ADD_EVIDENCE', payload: evidence })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateEvidence = useCallback(
|
||||
(id: string, data: Partial<Evidence>) => {
|
||||
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteEvidence = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
evidence: state.evidence,
|
||||
addEvidence,
|
||||
updateEvidence,
|
||||
deleteEvidence,
|
||||
getByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
activeCount: state.evidence.filter(e => e.status === 'ACTIVE').length,
|
||||
totalCount: state.evidence.length,
|
||||
}),
|
||||
[state.evidence, compliance, addEvidence, updateEvidence, deleteEvidence]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRisks() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addRisk = useCallback(
|
||||
(risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateRisk = useCallback(
|
||||
(id: string, data: Partial<Risk>) => {
|
||||
dispatch({ type: 'UPDATE_RISK', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteRisk = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_RISK', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
risks: state.risks,
|
||||
addRisk,
|
||||
updateRisk,
|
||||
deleteRisk,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
getByStatus: compliance.getRisksByStatus.bind(compliance),
|
||||
getBySeverity: compliance.getRisksBySeverity.bind(compliance),
|
||||
averageScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[state.risks, compliance, addRisk, updateRisk, deleteRisk]
|
||||
)
|
||||
}
|
||||
31
breakpilot-compliance-sdk/packages/react/src/hooks-core.ts
Normal file
31
breakpilot-compliance-sdk/packages/react/src/hooks-core.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { ComplianceContext, type ComplianceContextValue } from './provider'
|
||||
import type { SDKState, SDKAction } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// MAIN HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCompliance(): ComplianceContextValue {
|
||||
const context = useContext(ComplianceContext)
|
||||
if (!context) {
|
||||
throw new Error('useCompliance must be used within ComplianceProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATE HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceState(): SDKState {
|
||||
const { state } = useCompliance()
|
||||
return state
|
||||
}
|
||||
|
||||
export function useComplianceDispatch(): React.Dispatch<SDKAction> {
|
||||
const { dispatch } = useCompliance()
|
||||
return dispatch
|
||||
}
|
||||
83
breakpilot-compliance-sdk/packages/react/src/hooks-dsgvo.ts
Normal file
83
breakpilot-compliance-sdk/packages/react/src/hooks-dsgvo.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCompliance } from './hooks-core'
|
||||
import type { ConsentPurpose } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// DSGVO HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useDSGVO() {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// DSR
|
||||
dsrRequests: state.dsrRequests,
|
||||
dsrConfig: state.dsrConfig,
|
||||
submitDSR: dsgvo.submitDSR.bind(dsgvo),
|
||||
|
||||
// Consent
|
||||
consents: state.consents,
|
||||
hasConsent: dsgvo.hasConsent.bind(dsgvo),
|
||||
getConsentsByUserId: dsgvo.getConsentsByUserId.bind(dsgvo),
|
||||
|
||||
// VVT
|
||||
processingActivities: state.vvt,
|
||||
getProcessingActivityById: dsgvo.getProcessingActivityById.bind(dsgvo),
|
||||
|
||||
// DSFA
|
||||
dsfa: state.dsfa,
|
||||
isDSFARequired: dsgvo.isDSFARequired.bind(dsgvo),
|
||||
|
||||
// TOMs
|
||||
toms: state.toms,
|
||||
getTOMsByCategory: dsgvo.getTOMsByCategory.bind(dsgvo),
|
||||
getTOMScore: dsgvo.getTOMScore.bind(dsgvo),
|
||||
|
||||
// Retention
|
||||
retentionPolicies: state.retentionPolicies,
|
||||
getUpcomingDeletions: dsgvo.getUpcomingDeletions.bind(dsgvo),
|
||||
|
||||
// Cookie Banner
|
||||
cookieBanner: state.cookieBanner,
|
||||
generateCookieBannerCode: dsgvo.generateCookieBannerCode.bind(dsgvo),
|
||||
}),
|
||||
[dsgvo, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useConsent(userId: string) {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(() => {
|
||||
const userConsents = state.consents.filter(c => c.userId === userId)
|
||||
|
||||
return {
|
||||
consents: userConsents,
|
||||
hasConsent: (purpose: ConsentPurpose) => dsgvo.hasConsent(userId, purpose),
|
||||
hasAnalyticsConsent: dsgvo.hasConsent(userId, 'ANALYTICS'),
|
||||
hasMarketingConsent: dsgvo.hasConsent(userId, 'MARKETING'),
|
||||
hasFunctionalConsent: dsgvo.hasConsent(userId, 'FUNCTIONAL'),
|
||||
}
|
||||
}, [dsgvo, state.consents, userId])
|
||||
}
|
||||
|
||||
export function useDSR() {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
requests: state.dsrRequests,
|
||||
config: state.dsrConfig,
|
||||
submitRequest: dsgvo.submitDSR.bind(dsgvo),
|
||||
pendingRequests: state.dsrRequests.filter(r => r.status !== 'COMPLETED' && r.status !== 'REJECTED'),
|
||||
overdueRequests: state.dsrRequests.filter(r => {
|
||||
if (r.status === 'COMPLETED' || r.status === 'REJECTED') return false
|
||||
return new Date(r.dueDate) < new Date()
|
||||
}),
|
||||
}),
|
||||
[dsgvo, state.dsrRequests, state.dsrConfig]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCompliance } from './hooks-core'
|
||||
|
||||
// =============================================================================
|
||||
// RAG HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useRAG() {
|
||||
const { rag } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
search: rag.search.bind(rag),
|
||||
searchByRegulation: rag.searchByRegulation.bind(rag),
|
||||
searchByArticle: rag.searchByArticle.bind(rag),
|
||||
ask: rag.ask.bind(rag),
|
||||
askAboutRegulation: rag.askAboutRegulation.bind(rag),
|
||||
explainArticle: rag.explainArticle.bind(rag),
|
||||
checkCompliance: rag.checkCompliance.bind(rag),
|
||||
getQuickAnswer: rag.getQuickAnswer.bind(rag),
|
||||
findRelevantArticles: rag.findRelevantArticles.bind(rag),
|
||||
availableRegulations: rag.getAvailableRegulations(),
|
||||
chatHistory: rag.getChatHistory(),
|
||||
clearChatHistory: rag.clearChatHistory.bind(rag),
|
||||
startNewSession: rag.startNewSession.bind(rag),
|
||||
}),
|
||||
[rag]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECURITY HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSecurity() {
|
||||
const { security, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// SBOM
|
||||
sbom: state.sbom,
|
||||
components: security.getComponents(),
|
||||
vulnerableComponents: security.getVulnerableComponents(),
|
||||
licenseSummary: security.getLicenseSummary(),
|
||||
|
||||
// Issues
|
||||
issues: state.securityIssues,
|
||||
openIssues: security.getOpenIssues(),
|
||||
criticalIssues: security.getCriticalIssues(),
|
||||
getIssuesBySeverity: security.getIssuesBySeverity.bind(security),
|
||||
getIssuesByTool: security.getIssuesByTool.bind(security),
|
||||
|
||||
// Backlog
|
||||
backlog: state.securityBacklog,
|
||||
overdueBacklogItems: security.getOverdueBacklogItems(),
|
||||
|
||||
// Scanning
|
||||
startScan: security.startScan.bind(security),
|
||||
getScanResult: security.getScanResult.bind(security),
|
||||
lastScanResult: security.getLastScanResult(),
|
||||
|
||||
// Summary
|
||||
summary: security.getSecuritySummary(),
|
||||
securityScore: security.getSecurityScore(),
|
||||
availableTools: security.getAvailableTools(),
|
||||
}),
|
||||
[security, state]
|
||||
)
|
||||
}
|
||||
132
breakpilot-compliance-sdk/packages/react/src/hooks-ui.ts
Normal file
132
breakpilot-compliance-sdk/packages/react/src/hooks-ui.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCompliance } from './hooks-core'
|
||||
|
||||
// =============================================================================
|
||||
// NAVIGATION HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSDKNavigation() {
|
||||
const {
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
state,
|
||||
} = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currentStep,
|
||||
currentPhase: state.currentPhase,
|
||||
completedSteps: state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
}),
|
||||
[
|
||||
currentStep,
|
||||
state.currentPhase,
|
||||
state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SYNC HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSync() {
|
||||
const { syncState, forceSyncToServer, isOnline, saveState, loadState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
status: syncState.status,
|
||||
lastSyncedAt: syncState.lastSyncedAt,
|
||||
pendingChanges: syncState.pendingChanges,
|
||||
error: syncState.error,
|
||||
isOnline,
|
||||
isSyncing: syncState.status === 'syncing',
|
||||
hasConflict: syncState.status === 'conflict',
|
||||
forceSyncToServer,
|
||||
saveState,
|
||||
loadState,
|
||||
}),
|
||||
[syncState, isOnline, forceSyncToServer, saveState, loadState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CHECKPOINT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useCheckpoints() {
|
||||
const { validateCheckpoint, overrideCheckpoint, getCheckpointStatus, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
checkpoints: state.checkpoints,
|
||||
validateCheckpoint,
|
||||
overrideCheckpoint,
|
||||
getCheckpointStatus,
|
||||
passedCheckpoints: Object.values(state.checkpoints).filter(c => c.passed).length,
|
||||
totalCheckpoints: Object.keys(state.checkpoints).length,
|
||||
}),
|
||||
[state.checkpoints, validateCheckpoint, overrideCheckpoint, getCheckpointStatus]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useExport() {
|
||||
const { exportState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
exportJSON: () => exportState('json'),
|
||||
exportPDF: () => exportState('pdf'),
|
||||
exportZIP: () => exportState('zip'),
|
||||
exportState,
|
||||
}),
|
||||
[exportState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMAND BAR HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCommandBar() {
|
||||
const { isCommandBarOpen, setCommandBarOpen } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isOpen: isCommandBarOpen,
|
||||
open: () => setCommandBarOpen(true),
|
||||
close: () => setCommandBarOpen(false),
|
||||
toggle: () => setCommandBarOpen(!isCommandBarOpen),
|
||||
}),
|
||||
[isCommandBarOpen, setCommandBarOpen]
|
||||
)
|
||||
}
|
||||
@@ -1,474 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useMemo, useCallback } from 'react'
|
||||
import { ComplianceContext, type ComplianceContextValue } from './provider'
|
||||
import type {
|
||||
SDKState,
|
||||
SDKAction,
|
||||
Control,
|
||||
Evidence,
|
||||
Risk,
|
||||
Requirement,
|
||||
Obligation,
|
||||
TOM,
|
||||
ProcessingActivity,
|
||||
ConsentPurpose,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// MAIN HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCompliance(): ComplianceContextValue {
|
||||
const context = useContext(ComplianceContext)
|
||||
if (!context) {
|
||||
throw new Error('useCompliance must be used within ComplianceProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATE HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceState(): SDKState {
|
||||
const { state } = useCompliance()
|
||||
return state
|
||||
}
|
||||
|
||||
export function useComplianceDispatch(): React.Dispatch<SDKAction> {
|
||||
const { dispatch } = useCompliance()
|
||||
return dispatch
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSGVO HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useDSGVO() {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// DSR
|
||||
dsrRequests: state.dsrRequests,
|
||||
dsrConfig: state.dsrConfig,
|
||||
submitDSR: dsgvo.submitDSR.bind(dsgvo),
|
||||
|
||||
// Consent
|
||||
consents: state.consents,
|
||||
hasConsent: dsgvo.hasConsent.bind(dsgvo),
|
||||
getConsentsByUserId: dsgvo.getConsentsByUserId.bind(dsgvo),
|
||||
|
||||
// VVT
|
||||
processingActivities: state.vvt,
|
||||
getProcessingActivityById: dsgvo.getProcessingActivityById.bind(dsgvo),
|
||||
|
||||
// DSFA
|
||||
dsfa: state.dsfa,
|
||||
isDSFARequired: dsgvo.isDSFARequired.bind(dsgvo),
|
||||
|
||||
// TOMs
|
||||
toms: state.toms,
|
||||
getTOMsByCategory: dsgvo.getTOMsByCategory.bind(dsgvo),
|
||||
getTOMScore: dsgvo.getTOMScore.bind(dsgvo),
|
||||
|
||||
// Retention
|
||||
retentionPolicies: state.retentionPolicies,
|
||||
getUpcomingDeletions: dsgvo.getUpcomingDeletions.bind(dsgvo),
|
||||
|
||||
// Cookie Banner
|
||||
cookieBanner: state.cookieBanner,
|
||||
generateCookieBannerCode: dsgvo.generateCookieBannerCode.bind(dsgvo),
|
||||
}),
|
||||
[dsgvo, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useConsent(userId: string) {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(() => {
|
||||
const userConsents = state.consents.filter(c => c.userId === userId)
|
||||
|
||||
return {
|
||||
consents: userConsents,
|
||||
hasConsent: (purpose: ConsentPurpose) => dsgvo.hasConsent(userId, purpose),
|
||||
hasAnalyticsConsent: dsgvo.hasConsent(userId, 'ANALYTICS'),
|
||||
hasMarketingConsent: dsgvo.hasConsent(userId, 'MARKETING'),
|
||||
hasFunctionalConsent: dsgvo.hasConsent(userId, 'FUNCTIONAL'),
|
||||
}
|
||||
}, [dsgvo, state.consents, userId])
|
||||
}
|
||||
|
||||
export function useDSR() {
|
||||
const { dsgvo, state, dispatch } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
requests: state.dsrRequests,
|
||||
config: state.dsrConfig,
|
||||
submitRequest: dsgvo.submitDSR.bind(dsgvo),
|
||||
pendingRequests: state.dsrRequests.filter(r => r.status !== 'COMPLETED' && r.status !== 'REJECTED'),
|
||||
overdueRequests: state.dsrRequests.filter(r => {
|
||||
if (r.status === 'COMPLETED' || r.status === 'REJECTED') return false
|
||||
return new Date(r.dueDate) < new Date()
|
||||
}),
|
||||
}),
|
||||
[dsgvo, state.dsrRequests, state.dsrConfig]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPLIANCE HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceModule() {
|
||||
const { compliance, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// Controls
|
||||
controls: state.controls,
|
||||
getControlById: compliance.getControlById.bind(compliance),
|
||||
getControlsByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getControlsByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
controlComplianceRate: compliance.getControlComplianceRate(),
|
||||
|
||||
// Evidence
|
||||
evidence: state.evidence,
|
||||
getEvidenceByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
|
||||
// Requirements
|
||||
requirements: state.requirements,
|
||||
getRequirementsByRegulation: compliance.getRequirementsByRegulation.bind(compliance),
|
||||
requirementComplianceRate: compliance.getRequirementComplianceRate(),
|
||||
|
||||
// Obligations
|
||||
obligations: state.obligations,
|
||||
upcomingObligations: compliance.getUpcomingObligations(),
|
||||
overdueObligations: compliance.getOverdueObligations(),
|
||||
|
||||
// AI Act
|
||||
aiActClassification: state.aiActClassification,
|
||||
aiActRiskCategory: compliance.getAIActRiskCategory(),
|
||||
isHighRiskAI: compliance.isHighRiskAI(),
|
||||
|
||||
// Score
|
||||
complianceScore: compliance.calculateComplianceScore(),
|
||||
|
||||
// Risks
|
||||
risks: state.risks,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
averageRiskScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[compliance, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useControls() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addControl = useCallback(
|
||||
(control: Control) => {
|
||||
dispatch({ type: 'ADD_CONTROL', payload: control })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateControl = useCallback(
|
||||
(id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
controls: state.controls,
|
||||
addControl,
|
||||
updateControl,
|
||||
getById: compliance.getControlById.bind(compliance),
|
||||
getByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
implementedCount: state.controls.filter(c => c.implementationStatus === 'IMPLEMENTED').length,
|
||||
totalCount: state.controls.length,
|
||||
complianceRate: compliance.getControlComplianceRate(),
|
||||
}),
|
||||
[state.controls, compliance, addControl, updateControl]
|
||||
)
|
||||
}
|
||||
|
||||
export function useEvidence() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addEvidence = useCallback(
|
||||
(evidence: Evidence) => {
|
||||
dispatch({ type: 'ADD_EVIDENCE', payload: evidence })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateEvidence = useCallback(
|
||||
(id: string, data: Partial<Evidence>) => {
|
||||
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteEvidence = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
evidence: state.evidence,
|
||||
addEvidence,
|
||||
updateEvidence,
|
||||
deleteEvidence,
|
||||
getByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
activeCount: state.evidence.filter(e => e.status === 'ACTIVE').length,
|
||||
totalCount: state.evidence.length,
|
||||
}),
|
||||
[state.evidence, compliance, addEvidence, updateEvidence, deleteEvidence]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRisks() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addRisk = useCallback(
|
||||
(risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateRisk = useCallback(
|
||||
(id: string, data: Partial<Risk>) => {
|
||||
dispatch({ type: 'UPDATE_RISK', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteRisk = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_RISK', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
risks: state.risks,
|
||||
addRisk,
|
||||
updateRisk,
|
||||
deleteRisk,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
getByStatus: compliance.getRisksByStatus.bind(compliance),
|
||||
getBySeverity: compliance.getRisksBySeverity.bind(compliance),
|
||||
averageScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[state.risks, compliance, addRisk, updateRisk, deleteRisk]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RAG HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useRAG() {
|
||||
const { rag } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
search: rag.search.bind(rag),
|
||||
searchByRegulation: rag.searchByRegulation.bind(rag),
|
||||
searchByArticle: rag.searchByArticle.bind(rag),
|
||||
ask: rag.ask.bind(rag),
|
||||
askAboutRegulation: rag.askAboutRegulation.bind(rag),
|
||||
explainArticle: rag.explainArticle.bind(rag),
|
||||
checkCompliance: rag.checkCompliance.bind(rag),
|
||||
getQuickAnswer: rag.getQuickAnswer.bind(rag),
|
||||
findRelevantArticles: rag.findRelevantArticles.bind(rag),
|
||||
availableRegulations: rag.getAvailableRegulations(),
|
||||
chatHistory: rag.getChatHistory(),
|
||||
clearChatHistory: rag.clearChatHistory.bind(rag),
|
||||
startNewSession: rag.startNewSession.bind(rag),
|
||||
}),
|
||||
[rag]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECURITY HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSecurity() {
|
||||
const { security, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// SBOM
|
||||
sbom: state.sbom,
|
||||
components: security.getComponents(),
|
||||
vulnerableComponents: security.getVulnerableComponents(),
|
||||
licenseSummary: security.getLicenseSummary(),
|
||||
|
||||
// Issues
|
||||
issues: state.securityIssues,
|
||||
openIssues: security.getOpenIssues(),
|
||||
criticalIssues: security.getCriticalIssues(),
|
||||
getIssuesBySeverity: security.getIssuesBySeverity.bind(security),
|
||||
getIssuesByTool: security.getIssuesByTool.bind(security),
|
||||
|
||||
// Backlog
|
||||
backlog: state.securityBacklog,
|
||||
overdueBacklogItems: security.getOverdueBacklogItems(),
|
||||
|
||||
// Scanning
|
||||
startScan: security.startScan.bind(security),
|
||||
getScanResult: security.getScanResult.bind(security),
|
||||
lastScanResult: security.getLastScanResult(),
|
||||
|
||||
// Summary
|
||||
summary: security.getSecuritySummary(),
|
||||
securityScore: security.getSecurityScore(),
|
||||
availableTools: security.getAvailableTools(),
|
||||
}),
|
||||
[security, state]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NAVIGATION HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSDKNavigation() {
|
||||
const {
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
state,
|
||||
} = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currentStep,
|
||||
currentPhase: state.currentPhase,
|
||||
completedSteps: state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
}),
|
||||
[
|
||||
currentStep,
|
||||
state.currentPhase,
|
||||
state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SYNC HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSync() {
|
||||
const { syncState, forceSyncToServer, isOnline, saveState, loadState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
status: syncState.status,
|
||||
lastSyncedAt: syncState.lastSyncedAt,
|
||||
pendingChanges: syncState.pendingChanges,
|
||||
error: syncState.error,
|
||||
isOnline,
|
||||
isSyncing: syncState.status === 'syncing',
|
||||
hasConflict: syncState.status === 'conflict',
|
||||
forceSyncToServer,
|
||||
saveState,
|
||||
loadState,
|
||||
}),
|
||||
[syncState, isOnline, forceSyncToServer, saveState, loadState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CHECKPOINT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useCheckpoints() {
|
||||
const { validateCheckpoint, overrideCheckpoint, getCheckpointStatus, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
checkpoints: state.checkpoints,
|
||||
validateCheckpoint,
|
||||
overrideCheckpoint,
|
||||
getCheckpointStatus,
|
||||
passedCheckpoints: Object.values(state.checkpoints).filter(c => c.passed).length,
|
||||
totalCheckpoints: Object.keys(state.checkpoints).length,
|
||||
}),
|
||||
[state.checkpoints, validateCheckpoint, overrideCheckpoint, getCheckpointStatus]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useExport() {
|
||||
const { exportState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
exportJSON: () => exportState('json'),
|
||||
exportPDF: () => exportState('pdf'),
|
||||
exportZIP: () => exportState('zip'),
|
||||
exportState,
|
||||
}),
|
||||
[exportState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMAND BAR HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCommandBar() {
|
||||
const { isCommandBarOpen, setCommandBarOpen } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isOpen: isCommandBarOpen,
|
||||
open: () => setCommandBarOpen(true),
|
||||
close: () => setCommandBarOpen(false),
|
||||
toggle: () => setCommandBarOpen(!isCommandBarOpen),
|
||||
}),
|
||||
[isCommandBarOpen, setCommandBarOpen]
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Public re-export barrel for all React hooks.
|
||||
* All hooks remain importable from this path — split into focused sibling files:
|
||||
* hooks-core.ts — useCompliance, useComplianceState, useComplianceDispatch
|
||||
* hooks-dsgvo.ts — useDSGVO, useConsent, useDSR
|
||||
* hooks-compliance.ts — useComplianceModule, useControls, useEvidence, useRisks
|
||||
* hooks-rag-security.ts — useRAG, useSecurity
|
||||
* hooks-ui.ts — useSDKNavigation, useSync, useCheckpoints, useExport, useCommandBar
|
||||
*/
|
||||
|
||||
export {
|
||||
useCompliance,
|
||||
useComplianceState,
|
||||
useComplianceDispatch,
|
||||
} from './hooks-core'
|
||||
|
||||
export {
|
||||
useDSGVO,
|
||||
useConsent,
|
||||
useDSR,
|
||||
} from './hooks-dsgvo'
|
||||
|
||||
export {
|
||||
useComplianceModule,
|
||||
useControls,
|
||||
useEvidence,
|
||||
useRisks,
|
||||
} from './hooks-compliance'
|
||||
|
||||
export {
|
||||
useRAG,
|
||||
useSecurity,
|
||||
} from './hooks-rag-security'
|
||||
|
||||
export {
|
||||
useSDKNavigation,
|
||||
useSync,
|
||||
useCheckpoints,
|
||||
useExport,
|
||||
useCommandBar,
|
||||
} from './hooks-ui'
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* useCallback factories extracted from ComplianceProvider.
|
||||
*
|
||||
* Each function creates and returns a memoised callback so provider.tsx
|
||||
* stays under 300 LOC.
|
||||
*/
|
||||
|
||||
import { useCallback, RefObject } from 'react'
|
||||
import type {
|
||||
SDKState,
|
||||
CheckpointStatus,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import { ComplianceClient, StateSyncManager } from '@breakpilot/compliance-sdk-core'
|
||||
import { SDK_STORAGE_KEY } from './provider-context'
|
||||
|
||||
// =============================================================================
|
||||
// CHECKPOINT CALLBACKS
|
||||
// =============================================================================
|
||||
|
||||
export function useValidateCheckpoint(
|
||||
state: SDKState,
|
||||
enableBackendSync: boolean,
|
||||
client: ComplianceClient,
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>
|
||||
) {
|
||||
return useCallback(
|
||||
async (checkpointId: string): Promise<CheckpointStatus> => {
|
||||
if (enableBackendSync) {
|
||||
try {
|
||||
const result = await client.validateCheckpoint(checkpointId, state)
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId: result.checkpointId,
|
||||
passed: result.passed,
|
||||
validatedAt: new Date(result.validatedAt),
|
||||
validatedBy: result.validatedBy,
|
||||
errors: result.errors,
|
||||
warnings: result.warnings,
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
} catch {
|
||||
// Fall through to local validation
|
||||
}
|
||||
}
|
||||
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId,
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
},
|
||||
[state, enableBackendSync, client, dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useOverrideCheckpoint(
|
||||
state: SDKState,
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>
|
||||
) {
|
||||
return useCallback(
|
||||
async (checkpointId: string, reason: string): Promise<void> => {
|
||||
const existingStatus = state.checkpoints[checkpointId]
|
||||
const overriddenStatus: CheckpointStatus = {
|
||||
...existingStatus,
|
||||
checkpointId,
|
||||
passed: true,
|
||||
overrideReason: reason,
|
||||
overriddenBy: state.userId,
|
||||
overriddenAt: new Date(),
|
||||
errors: [],
|
||||
warnings: existingStatus?.warnings || [],
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } })
|
||||
},
|
||||
[state.checkpoints, state.userId, dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useGetCheckpointStatus(state: SDKState) {
|
||||
return useCallback(
|
||||
(checkpointId: string): CheckpointStatus | undefined => state.checkpoints[checkpointId],
|
||||
[state.checkpoints]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATE UPDATE CALLBACKS
|
||||
// =============================================================================
|
||||
|
||||
export function useUpdateUseCase(dispatch: React.Dispatch<{ type: string; payload?: unknown }>) {
|
||||
return useCallback(
|
||||
(id: string, data: Partial<UseCaseAssessment>) => {
|
||||
dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useAddRisk(dispatch: React.Dispatch<{ type: string; payload?: unknown }>) {
|
||||
return useCallback(
|
||||
(risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
export function useUpdateControl(dispatch: React.Dispatch<{ type: string; payload?: unknown }>) {
|
||||
return useCallback(
|
||||
(id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PERSISTENCE CALLBACKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSaveState(
|
||||
state: SDKState,
|
||||
tenantId: string,
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>,
|
||||
setError: (e: Error) => void
|
||||
) {
|
||||
return useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
}
|
||||
}, [state, tenantId, enableBackendSync, syncManagerRef, setError])
|
||||
}
|
||||
|
||||
export function useLoadState(
|
||||
tenantId: string,
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>,
|
||||
setIsLoading: (v: boolean) => void,
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>,
|
||||
setError: (e: Error) => void
|
||||
) {
|
||||
return useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
dispatch({ type: 'SET_STATE', payload: JSON.parse(stored) })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [tenantId, enableBackendSync, syncManagerRef, setIsLoading, dispatch, setError])
|
||||
}
|
||||
|
||||
export function useResetState(
|
||||
tenantId: string,
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>
|
||||
) {
|
||||
return useCallback(() => {
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
}
|
||||
}, [tenantId, dispatch])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SYNC & EXPORT CALLBACKS
|
||||
// =============================================================================
|
||||
|
||||
export function useForceSyncToServer(
|
||||
state: SDKState,
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>
|
||||
) {
|
||||
return useCallback(async (): Promise<void> => {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
}, [state, enableBackendSync, syncManagerRef])
|
||||
}
|
||||
|
||||
export function useExportState(state: SDKState, client: ComplianceClient) {
|
||||
return useCallback(
|
||||
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
||||
if (format === 'json') {
|
||||
return new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
return client.exportState(format)
|
||||
},
|
||||
[state, client]
|
||||
)
|
||||
}
|
||||
203
breakpilot-compliance-sdk/packages/react/src/provider-effects.ts
Normal file
203
breakpilot-compliance-sdk/packages/react/src/provider-effects.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Side-effect hooks extracted from ComplianceProvider.
|
||||
*
|
||||
* Each function encapsulates one useEffect concern so provider.tsx
|
||||
* stays under 300 LOC.
|
||||
*/
|
||||
|
||||
import { useEffect, RefObject } from 'react'
|
||||
import type { SDKState } from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
ComplianceClient,
|
||||
StateSyncManager,
|
||||
createStateSyncManager,
|
||||
} from '@breakpilot/compliance-sdk-core'
|
||||
import { SDK_STORAGE_KEY } from './provider-context'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface SyncStateSetters {
|
||||
setSyncState: React.Dispatch<React.SetStateAction<{
|
||||
status: string
|
||||
lastSyncedAt: Date | null
|
||||
localVersion: number
|
||||
serverVersion: number
|
||||
pendingChanges: number
|
||||
error: string | null
|
||||
}>>
|
||||
setIsOnline: (v: boolean) => void
|
||||
setError: (e: Error | null) => void
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SYNC MANAGER EFFECT
|
||||
// =============================================================================
|
||||
|
||||
export function useSyncManagerEffect(
|
||||
enableBackendSync: boolean,
|
||||
tenantId: string,
|
||||
client: ComplianceClient,
|
||||
state: SDKState,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>,
|
||||
{ setSyncState, setIsOnline, setError, dispatch }: SyncStateSetters
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (enableBackendSync && typeof window !== 'undefined') {
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
client,
|
||||
tenantId,
|
||||
{ debounceMs: 2000, maxRetries: 3 },
|
||||
{
|
||||
onSyncStart: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
||||
},
|
||||
onSyncComplete: syncedState => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
pendingChanges: 0,
|
||||
}))
|
||||
if (new Date(syncedState.lastModified) > new Date(state.lastModified)) {
|
||||
dispatch({ type: 'SET_STATE', payload: syncedState })
|
||||
}
|
||||
},
|
||||
onSyncError: err => {
|
||||
setSyncState(prev => ({ ...prev, status: 'error', error: err.message }))
|
||||
setError(err)
|
||||
},
|
||||
onConflict: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
||||
},
|
||||
onOffline: () => {
|
||||
setIsOnline(false)
|
||||
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
||||
},
|
||||
onOnline: () => {
|
||||
setIsOnline(true)
|
||||
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
syncManagerRef.current?.destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enableBackendSync, tenantId, client])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LOAD INITIAL STATE EFFECT
|
||||
// =============================================================================
|
||||
|
||||
export interface LoadStateSetters {
|
||||
setIsLoading: (v: boolean) => void
|
||||
setIsInitialized: (v: boolean) => void
|
||||
setError: (e: Error | null) => void
|
||||
dispatch: React.Dispatch<{ type: string; payload?: unknown }>
|
||||
onError?: (e: Error) => void
|
||||
}
|
||||
|
||||
export function useLoadInitialStateEffect(
|
||||
tenantId: string,
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>,
|
||||
{ setIsLoading, setIsInitialized, setError, dispatch, onError }: LoadStateSetters
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const loadInitialState = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
if (parsed.lastModified) {
|
||||
parsed.lastModified = new Date(parsed.lastModified)
|
||||
}
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
}
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
onError?.(err as Error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}
|
||||
|
||||
loadInitialState()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tenantId, enableBackendSync])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUTO-SAVE EFFECT
|
||||
// =============================================================================
|
||||
|
||||
export function useAutoSaveEffect(
|
||||
state: SDKState,
|
||||
tenantId: string,
|
||||
isInitialized: boolean,
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: RefObject<StateSyncManager | null>
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !state.preferences.autoSave) return
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
syncManagerRef.current.queueSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save state:', err)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(saveTimeout)
|
||||
}, [state, tenantId, isInitialized, enableBackendSync])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// KEYBOARD SHORTCUTS EFFECT
|
||||
// =============================================================================
|
||||
|
||||
export function useKeyboardShortcutsEffect(
|
||||
isCommandBarOpen: boolean,
|
||||
setCommandBarOpen: (fn: (prev: boolean) => boolean) => void
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandBarOpen(prev => !prev)
|
||||
}
|
||||
if (e.key === 'Escape' && isCommandBarOpen) {
|
||||
setCommandBarOpen(() => false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isCommandBarOpen, setCommandBarOpen])
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import React, {
|
||||
useReducer,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -13,19 +11,12 @@ import {
|
||||
sdkReducer,
|
||||
initialState,
|
||||
StateSyncManager,
|
||||
createStateSyncManager,
|
||||
createDSGVOModule,
|
||||
createComplianceModule,
|
||||
createRAGModule,
|
||||
createSecurityModule,
|
||||
} from '@breakpilot/compliance-sdk-core'
|
||||
import type {
|
||||
CheckpointStatus,
|
||||
SyncState,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import type { SyncState } from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
getStepById,
|
||||
getNextStep,
|
||||
@@ -35,10 +26,29 @@ import {
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
ComplianceContext,
|
||||
SDK_STORAGE_KEY,
|
||||
type ComplianceContextValue,
|
||||
type ComplianceProviderProps,
|
||||
} from './provider-context'
|
||||
import {
|
||||
useSyncManagerEffect,
|
||||
useLoadInitialStateEffect,
|
||||
useAutoSaveEffect,
|
||||
useKeyboardShortcutsEffect,
|
||||
} from './provider-effects'
|
||||
import {
|
||||
useValidateCheckpoint,
|
||||
useOverrideCheckpoint,
|
||||
useGetCheckpointStatus,
|
||||
useUpdateUseCase,
|
||||
useAddRisk,
|
||||
useUpdateControl,
|
||||
useSaveState,
|
||||
useLoadState,
|
||||
useResetState,
|
||||
useForceSyncToServer,
|
||||
useExportState,
|
||||
} from './provider-callbacks'
|
||||
import { useCallback, useMemo as useMemoReact } from 'react'
|
||||
|
||||
export {
|
||||
ComplianceContext,
|
||||
@@ -69,7 +79,7 @@ export function ComplianceProvider({
|
||||
tenantId,
|
||||
userId,
|
||||
})
|
||||
const [isCommandBarOpen, setCommandBarOpen] = useState(false)
|
||||
const [isCommandBarOpen, setCommandBarOpenRaw] = useState(false)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
@@ -87,7 +97,7 @@ export function ComplianceProvider({
|
||||
const clientRef = useRef<ComplianceClient | null>(null)
|
||||
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
||||
|
||||
// Initialize client
|
||||
// Initialize client (once)
|
||||
if (!clientRef.current) {
|
||||
clientRef.current = new ComplianceClient({
|
||||
apiEndpoint,
|
||||
@@ -103,144 +113,46 @@ export function ComplianceProvider({
|
||||
const client = clientRef.current
|
||||
|
||||
// Modules
|
||||
const dsgvo = useMemo(
|
||||
() => createDSGVOModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
const compliance = useMemo(
|
||||
() => createComplianceModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
const dsgvo = useMemo(() => createDSGVOModule(client, () => state), [client, state])
|
||||
const compliance = useMemo(() => createComplianceModule(client, () => state), [client, state])
|
||||
const rag = useMemo(() => createRAGModule(client), [client])
|
||||
const security = useMemo(
|
||||
() => createSecurityModule(client, () => state),
|
||||
[client, state]
|
||||
const security = useMemo(() => createSecurityModule(client, () => state), [client, state])
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Effects (extracted to provider-effects.ts)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const syncStateSetters = useMemo(
|
||||
() => ({ setSyncState, setIsOnline, setError, dispatch }),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
// Initialize sync manager
|
||||
useEffect(() => {
|
||||
if (enableBackendSync && typeof window !== 'undefined') {
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
client,
|
||||
tenantId,
|
||||
{ debounceMs: 2000, maxRetries: 3 },
|
||||
{
|
||||
onSyncStart: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
||||
},
|
||||
onSyncComplete: syncedState => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
pendingChanges: 0,
|
||||
}))
|
||||
if (new Date(syncedState.lastModified) > new Date(state.lastModified)) {
|
||||
dispatch({ type: 'SET_STATE', payload: syncedState })
|
||||
}
|
||||
},
|
||||
onSyncError: err => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
}))
|
||||
setError(err)
|
||||
},
|
||||
onConflict: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
||||
},
|
||||
onOffline: () => {
|
||||
setIsOnline(false)
|
||||
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
||||
},
|
||||
onOnline: () => {
|
||||
setIsOnline(true)
|
||||
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
useSyncManagerEffect(enableBackendSync, tenantId, client, state, syncManagerRef, syncStateSetters)
|
||||
|
||||
return () => {
|
||||
syncManagerRef.current?.destroy()
|
||||
}
|
||||
}, [enableBackendSync, tenantId, client])
|
||||
useLoadInitialStateEffect(tenantId, enableBackendSync, syncManagerRef, {
|
||||
setIsLoading,
|
||||
setIsInitialized,
|
||||
setError,
|
||||
dispatch,
|
||||
onError,
|
||||
})
|
||||
|
||||
// Load initial state
|
||||
useEffect(() => {
|
||||
const loadInitialState = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Load from localStorage first
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
if (parsed.lastModified) {
|
||||
parsed.lastModified = new Date(parsed.lastModified)
|
||||
}
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
}
|
||||
}
|
||||
useAutoSaveEffect(state, tenantId, isInitialized, enableBackendSync, syncManagerRef)
|
||||
|
||||
// Then load from server if enabled
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
onError?.(err as Error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}
|
||||
const setCommandBarOpen = useCallback(
|
||||
(fn: boolean | ((prev: boolean) => boolean)) => {
|
||||
setCommandBarOpenRaw(typeof fn === 'function' ? fn : () => fn)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
loadInitialState()
|
||||
}, [tenantId, enableBackendSync])
|
||||
|
||||
// Auto-save
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !state.preferences.autoSave) return
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
syncManagerRef.current.queueSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save state:', err)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(saveTimeout)
|
||||
}, [state, tenantId, isInitialized, enableBackendSync])
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandBarOpen(prev => !prev)
|
||||
}
|
||||
if (e.key === 'Escape' && isCommandBarOpen) {
|
||||
setCommandBarOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isCommandBarOpen])
|
||||
useKeyboardShortcutsEffect(isCommandBarOpen, setCommandBarOpen as (fn: (prev: boolean) => boolean) => void)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Navigation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
|
||||
|
||||
const goToStep = useCallback(
|
||||
@@ -256,16 +168,12 @@ export function ComplianceProvider({
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const nextStep = getNextStep(state.currentStep)
|
||||
if (nextStep) {
|
||||
goToStep(nextStep.id)
|
||||
}
|
||||
if (nextStep) goToStep(nextStep.id)
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
const prevStep = getPreviousStep(state.currentStep)
|
||||
if (prevStep) {
|
||||
goToStep(prevStep.id)
|
||||
}
|
||||
if (prevStep) goToStep(prevStep.id)
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const canGoNext = useMemo(() => getNextStep(state.currentStep) !== undefined, [state.currentStep])
|
||||
@@ -274,152 +182,29 @@ export function ComplianceProvider({
|
||||
[state.currentStep]
|
||||
)
|
||||
|
||||
// Progress
|
||||
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
|
||||
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
|
||||
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
|
||||
|
||||
// Checkpoints
|
||||
const validateCheckpoint = useCallback(
|
||||
async (checkpointId: string): Promise<CheckpointStatus> => {
|
||||
if (enableBackendSync) {
|
||||
try {
|
||||
const result = await client.validateCheckpoint(checkpointId, state)
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId: result.checkpointId,
|
||||
passed: result.passed,
|
||||
validatedAt: new Date(result.validatedAt),
|
||||
validatedBy: result.validatedBy,
|
||||
errors: result.errors,
|
||||
warnings: result.warnings,
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
} catch {
|
||||
// Fall through to local validation
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Callbacks (extracted to provider-callbacks.ts)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Local validation
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId,
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
const validateCheckpoint = useValidateCheckpoint(state, enableBackendSync, client, dispatch)
|
||||
const overrideCheckpoint = useOverrideCheckpoint(state, dispatch)
|
||||
const getCheckpointStatus = useGetCheckpointStatus(state)
|
||||
const updateUseCase = useUpdateUseCase(dispatch)
|
||||
const addRisk = useAddRisk(dispatch)
|
||||
const updateControl = useUpdateControl(dispatch)
|
||||
const saveState = useSaveState(state, tenantId, enableBackendSync, syncManagerRef, e => setError(e))
|
||||
const loadState = useLoadState(tenantId, enableBackendSync, syncManagerRef, setIsLoading, dispatch, e => setError(e))
|
||||
const resetState = useResetState(tenantId, dispatch)
|
||||
const forceSyncToServer = useForceSyncToServer(state, enableBackendSync, syncManagerRef)
|
||||
const exportState = useExportState(state, client)
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
},
|
||||
[state, enableBackendSync, client]
|
||||
)
|
||||
|
||||
const overrideCheckpoint = useCallback(
|
||||
async (checkpointId: string, reason: string): Promise<void> => {
|
||||
const existingStatus = state.checkpoints[checkpointId]
|
||||
const overriddenStatus: CheckpointStatus = {
|
||||
...existingStatus,
|
||||
checkpointId,
|
||||
passed: true,
|
||||
overrideReason: reason,
|
||||
overriddenBy: state.userId,
|
||||
overriddenAt: new Date(),
|
||||
errors: [],
|
||||
warnings: existingStatus?.warnings || [],
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } })
|
||||
},
|
||||
[state.checkpoints, state.userId]
|
||||
)
|
||||
|
||||
const getCheckpointStatus = useCallback(
|
||||
(checkpointId: string): CheckpointStatus | undefined => {
|
||||
return state.checkpoints[checkpointId]
|
||||
},
|
||||
[state.checkpoints]
|
||||
)
|
||||
|
||||
// State Updates
|
||||
const updateUseCase = useCallback((id: string, data: Partial<UseCaseAssessment>) => {
|
||||
dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } })
|
||||
}, [])
|
||||
|
||||
const addRisk = useCallback((risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
}, [])
|
||||
|
||||
const updateControl = useCallback((id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
}, [])
|
||||
|
||||
// Persistence
|
||||
const saveState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
}
|
||||
}, [state, tenantId, enableBackendSync])
|
||||
|
||||
const loadState = useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
dispatch({ type: 'SET_STATE', payload: JSON.parse(stored) })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [tenantId, enableBackendSync])
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
}
|
||||
}, [tenantId])
|
||||
|
||||
// Sync
|
||||
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
}, [state, enableBackendSync])
|
||||
|
||||
// Export
|
||||
const exportState = useCallback(
|
||||
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
||||
if (format === 'json') {
|
||||
return new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
return client.exportState(format)
|
||||
},
|
||||
[state, client]
|
||||
)
|
||||
// -------------------------------------------------------------------------
|
||||
// Context value
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const value: ComplianceContextValue = {
|
||||
state,
|
||||
@@ -452,7 +237,7 @@ export function ComplianceProvider({
|
||||
isOnline,
|
||||
exportState,
|
||||
isCommandBarOpen,
|
||||
setCommandBarOpen,
|
||||
setCommandBarOpen: setCommandBarOpenRaw,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Render helpers for <breakpilot-dsr-portal>
|
||||
*
|
||||
* Provides DSR_PORTAL_STYLES, buildFormHtml, and buildSuccessHtml.
|
||||
* Kept separate from the element class to stay under the 300-LOC target.
|
||||
*/
|
||||
|
||||
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
|
||||
import { COMMON_STYLES } from './base'
|
||||
import type { DSRTranslations } from './dsr-portal-translations'
|
||||
|
||||
// =============================================================================
|
||||
// STYLES
|
||||
// =============================================================================
|
||||
|
||||
export const DSR_PORTAL_STYLES = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.portal {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-option:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.type-option.selected {
|
||||
border-color: #1a1a1a;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.type-option input {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #1a1a1a;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
margin: 0 0 10px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: 0;
|
||||
color: #166534;
|
||||
}
|
||||
`
|
||||
|
||||
// =============================================================================
|
||||
// HTML BUILDERS
|
||||
// =============================================================================
|
||||
|
||||
export interface FormRenderOptions {
|
||||
t: DSRTranslations
|
||||
selectedType: DSRRequestType | null
|
||||
name: string
|
||||
email: string
|
||||
additionalInfo: string
|
||||
isSubmitting: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function buildFormHtml(styles: string, opts: FormRenderOptions): string {
|
||||
const { t, selectedType, name, email, additionalInfo, isSubmitting, error } = opts
|
||||
|
||||
const types: DSRRequestType[] = [
|
||||
'ACCESS',
|
||||
'RECTIFICATION',
|
||||
'ERASURE',
|
||||
'PORTABILITY',
|
||||
'RESTRICTION',
|
||||
'OBJECTION',
|
||||
]
|
||||
|
||||
const typesHtml = types
|
||||
.map(
|
||||
type => `
|
||||
<label class="type-option ${selectedType === type ? 'selected' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
name="dsrType"
|
||||
value="${type}"
|
||||
${selectedType === type ? 'checked' : ''}
|
||||
/>
|
||||
<div>
|
||||
<div class="type-name">${t.types[type].name}</div>
|
||||
<div class="type-description">${t.types[type].description}</div>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const isValid = selectedType && email && name
|
||||
const isDisabled = !isValid || isSubmitting
|
||||
|
||||
return `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<h2 class="title">${t.title}</h2>
|
||||
<p class="subtitle">${t.subtitle}</p>
|
||||
|
||||
<form id="dsr-form">
|
||||
<div class="form-group">
|
||||
<label class="label">${t.requestType} *</label>
|
||||
<div class="type-options">
|
||||
${typesHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.name} *</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="name-input"
|
||||
value="${name}"
|
||||
placeholder="${t.namePlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.email} *</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
id="email-input"
|
||||
value="${email}"
|
||||
placeholder="${t.emailPlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.additionalInfo}</label>
|
||||
<textarea
|
||||
class="input textarea"
|
||||
id="info-input"
|
||||
placeholder="${t.additionalInfoPlaceholder}"
|
||||
rows="4"
|
||||
>${additionalInfo}</textarea>
|
||||
</div>
|
||||
|
||||
${error ? `<div class="error">${error}</div>` : ''}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-submit"
|
||||
${isDisabled ? 'disabled' : ''}
|
||||
>
|
||||
${isSubmitting ? t.submitting : t.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="disclaimer">${t.disclaimer}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export function buildSuccessHtml(styles: string, t: DSRTranslations, email: string): string {
|
||||
return `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<div class="success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2 class="success-title">${t.successTitle}</h2>
|
||||
<p class="success-message">
|
||||
${t.successMessage} ${email}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Translations for <breakpilot-dsr-portal>
|
||||
*
|
||||
* Supported languages: 'de' | 'en'
|
||||
*/
|
||||
|
||||
export const DSR_TRANSLATIONS = {
|
||||
de: {
|
||||
title: 'Betroffenenrechte-Portal',
|
||||
subtitle:
|
||||
'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.',
|
||||
requestType: 'Art der Anfrage',
|
||||
name: 'Ihr Name',
|
||||
namePlaceholder: 'Max Mustermann',
|
||||
email: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'max@example.com',
|
||||
additionalInfo: 'Zusätzliche Informationen (optional)',
|
||||
additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...',
|
||||
submit: 'Anfrage einreichen',
|
||||
submitting: 'Wird gesendet...',
|
||||
successTitle: 'Anfrage eingereicht',
|
||||
successMessage:
|
||||
'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an',
|
||||
disclaimer:
|
||||
'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Auskunft (Art. 15)',
|
||||
description: 'Welche Daten haben Sie über mich gespeichert?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Berichtigung (Art. 16)',
|
||||
description: 'Korrigieren Sie falsche Daten über mich.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Löschung (Art. 17)',
|
||||
description: 'Löschen Sie alle meine personenbezogenen Daten.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Datenübertragbarkeit (Art. 20)',
|
||||
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Einschränkung (Art. 18)',
|
||||
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Widerspruch (Art. 21)',
|
||||
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Data Subject Rights Portal',
|
||||
subtitle:
|
||||
'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.',
|
||||
requestType: 'Request Type',
|
||||
name: 'Your Name',
|
||||
namePlaceholder: 'John Doe',
|
||||
email: 'Email Address',
|
||||
emailPlaceholder: 'john@example.com',
|
||||
additionalInfo: 'Additional Information (optional)',
|
||||
additionalInfoPlaceholder: 'Any additional details about your request...',
|
||||
submit: 'Submit Request',
|
||||
submitting: 'Submitting...',
|
||||
successTitle: 'Request Submitted',
|
||||
successMessage:
|
||||
'We will process your request within 30 days. You will receive a confirmation email at',
|
||||
disclaimer:
|
||||
'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Access (Art. 15)',
|
||||
description: 'What data do you have about me?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Rectification (Art. 16)',
|
||||
description: 'Correct inaccurate data about me.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Erasure (Art. 17)',
|
||||
description: 'Delete all my personal data.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Data Portability (Art. 20)',
|
||||
description: 'Export my data in a machine-readable format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Restriction (Art. 18)',
|
||||
description: 'Restrict the processing of my data.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Objection (Art. 21)',
|
||||
description: 'I object to the processing of my data.',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
export type DSRLanguage = keyof typeof DSR_TRANSLATIONS
|
||||
export type DSRTranslations = (typeof DSR_TRANSLATIONS)[DSRLanguage]
|
||||
@@ -8,103 +8,15 @@
|
||||
* api-key="pk_live_xxx"
|
||||
* language="de">
|
||||
* </breakpilot-dsr-portal>
|
||||
*
|
||||
* Split: translations → dsr-portal-translations.ts
|
||||
* styles + HTML builders → dsr-portal-render.ts
|
||||
*/
|
||||
|
||||
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
|
||||
import { BreakPilotElement, COMMON_STYLES } from './base'
|
||||
|
||||
const TRANSLATIONS = {
|
||||
de: {
|
||||
title: 'Betroffenenrechte-Portal',
|
||||
subtitle:
|
||||
'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.',
|
||||
requestType: 'Art der Anfrage',
|
||||
name: 'Ihr Name',
|
||||
namePlaceholder: 'Max Mustermann',
|
||||
email: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'max@example.com',
|
||||
additionalInfo: 'Zusätzliche Informationen (optional)',
|
||||
additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...',
|
||||
submit: 'Anfrage einreichen',
|
||||
submitting: 'Wird gesendet...',
|
||||
successTitle: 'Anfrage eingereicht',
|
||||
successMessage:
|
||||
'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an',
|
||||
disclaimer:
|
||||
'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Auskunft (Art. 15)',
|
||||
description: 'Welche Daten haben Sie über mich gespeichert?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Berichtigung (Art. 16)',
|
||||
description: 'Korrigieren Sie falsche Daten über mich.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Löschung (Art. 17)',
|
||||
description: 'Löschen Sie alle meine personenbezogenen Daten.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Datenübertragbarkeit (Art. 20)',
|
||||
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Einschränkung (Art. 18)',
|
||||
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Widerspruch (Art. 21)',
|
||||
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Data Subject Rights Portal',
|
||||
subtitle:
|
||||
'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.',
|
||||
requestType: 'Request Type',
|
||||
name: 'Your Name',
|
||||
namePlaceholder: 'John Doe',
|
||||
email: 'Email Address',
|
||||
emailPlaceholder: 'john@example.com',
|
||||
additionalInfo: 'Additional Information (optional)',
|
||||
additionalInfoPlaceholder: 'Any additional details about your request...',
|
||||
submit: 'Submit Request',
|
||||
submitting: 'Submitting...',
|
||||
successTitle: 'Request Submitted',
|
||||
successMessage:
|
||||
'We will process your request within 30 days. You will receive a confirmation email at',
|
||||
disclaimer:
|
||||
'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Access (Art. 15)',
|
||||
description: 'What data do you have about me?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Rectification (Art. 16)',
|
||||
description: 'Correct inaccurate data about me.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Erasure (Art. 17)',
|
||||
description: 'Delete all my personal data.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Data Portability (Art. 20)',
|
||||
description: 'Export my data in a machine-readable format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Restriction (Art. 18)',
|
||||
description: 'Restrict the processing of my data.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Objection (Art. 21)',
|
||||
description: 'I object to the processing of my data.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
import { BreakPilotElement } from './base'
|
||||
import { DSR_TRANSLATIONS, type DSRLanguage } from './dsr-portal-translations'
|
||||
import { DSR_PORTAL_STYLES, buildFormHtml, buildSuccessHtml } from './dsr-portal-render'
|
||||
|
||||
export class DSRPortalElement extends BreakPilotElement {
|
||||
static get observedAttributes(): string[] {
|
||||
@@ -119,12 +31,12 @@ export class DSRPortalElement extends BreakPilotElement {
|
||||
private isSubmitted = false
|
||||
private error: string | null = null
|
||||
|
||||
private get language(): 'de' | 'en' {
|
||||
return (this.getAttribute('language') as 'de' | 'en') || 'de'
|
||||
private get language(): DSRLanguage {
|
||||
return (this.getAttribute('language') as DSRLanguage) || 'de'
|
||||
}
|
||||
|
||||
private get t() {
|
||||
return TRANSLATIONS[this.language]
|
||||
return DSR_TRANSLATIONS[this.language]
|
||||
}
|
||||
|
||||
private handleTypeSelect = (type: DSRRequestType): void => {
|
||||
@@ -165,253 +77,23 @@ export class DSRPortalElement extends BreakPilotElement {
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const styles = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.portal {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-option:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.type-option.selected {
|
||||
border-color: #1a1a1a;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.type-option input {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #1a1a1a;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
margin: 0 0 10px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: 0;
|
||||
color: #166534;
|
||||
}
|
||||
`
|
||||
|
||||
if (this.isSubmitted) {
|
||||
this.renderSuccess(styles)
|
||||
this.shadow.innerHTML = buildSuccessHtml(DSR_PORTAL_STYLES, this.t, this.email)
|
||||
} else {
|
||||
this.renderForm(styles)
|
||||
this.renderForm()
|
||||
}
|
||||
}
|
||||
|
||||
private renderForm(styles: string): void {
|
||||
const t = this.t
|
||||
const types: DSRRequestType[] = [
|
||||
'ACCESS',
|
||||
'RECTIFICATION',
|
||||
'ERASURE',
|
||||
'PORTABILITY',
|
||||
'RESTRICTION',
|
||||
'OBJECTION',
|
||||
]
|
||||
|
||||
const typesHtml = types
|
||||
.map(
|
||||
type => `
|
||||
<label class="type-option ${this.selectedType === type ? 'selected' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
name="dsrType"
|
||||
value="${type}"
|
||||
${this.selectedType === type ? 'checked' : ''}
|
||||
/>
|
||||
<div>
|
||||
<div class="type-name">${t.types[type].name}</div>
|
||||
<div class="type-description">${t.types[type].description}</div>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const isValid = this.selectedType && this.email && this.name
|
||||
const isDisabled = !isValid || this.isSubmitting
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<h2 class="title">${t.title}</h2>
|
||||
<p class="subtitle">${t.subtitle}</p>
|
||||
|
||||
<form id="dsr-form">
|
||||
<div class="form-group">
|
||||
<label class="label">${t.requestType} *</label>
|
||||
<div class="type-options">
|
||||
${typesHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.name} *</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="name-input"
|
||||
value="${this.name}"
|
||||
placeholder="${t.namePlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.email} *</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
id="email-input"
|
||||
value="${this.email}"
|
||||
placeholder="${t.emailPlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.additionalInfo}</label>
|
||||
<textarea
|
||||
class="input textarea"
|
||||
id="info-input"
|
||||
placeholder="${t.additionalInfoPlaceholder}"
|
||||
rows="4"
|
||||
>${this.additionalInfo}</textarea>
|
||||
</div>
|
||||
|
||||
${this.error ? `<div class="error">${this.error}</div>` : ''}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-submit"
|
||||
${isDisabled ? 'disabled' : ''}
|
||||
>
|
||||
${this.isSubmitting ? t.submitting : t.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="disclaimer">${t.disclaimer}</p>
|
||||
</div>
|
||||
`
|
||||
private renderForm(): void {
|
||||
this.shadow.innerHTML = buildFormHtml(DSR_PORTAL_STYLES, {
|
||||
t: this.t,
|
||||
selectedType: this.selectedType,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
additionalInfo: this.additionalInfo,
|
||||
isSubmitting: this.isSubmitting,
|
||||
error: this.error,
|
||||
})
|
||||
|
||||
// Bind events
|
||||
const form = this.shadow.getElementById('dsr-form') as HTMLFormElement
|
||||
@@ -439,23 +121,6 @@ export class DSRPortalElement extends BreakPilotElement {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private renderSuccess(styles: string): void {
|
||||
const t = this.t
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<div class="success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2 class="success-title">${t.successTitle}</h2>
|
||||
<p class="success-message">
|
||||
${t.successMessage} ${this.email}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
|
||||
Reference in New Issue
Block a user