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>
287 lines
5.8 KiB
TypeScript
287 lines
5.8 KiB
TypeScript
/**
|
|
* 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>
|
|
`
|
|
}
|