Compare commits

...

4 Commits

Author SHA1 Message Date
DanielS
2b1bb77673 feat(branding): add CASPOS Classic Blue preset
All checks were successful
Staging Build / build (push) Successful in 3m8s
2026-08-27 00:39:07 +02:00
DanielS
b692e6d4cf Merge branch 'staging' of https://gitea.hephex.de/admin/webshop into staging
# Conflicts:
#	shop/lib/actions/email-templates.ts
2026-08-27 00:34:39 +02:00
DanielS
0c99da2eeb feat(email): add primaryColor branding param to email templates 2026-08-27 00:32:18 +02:00
DanielS
f68d67de43 feat(wizard): add showValidationWarning prop to summary sidebar 2026-08-27 00:32:09 +02:00
7 changed files with 71 additions and 16 deletions

View File

@@ -43,7 +43,20 @@ export async function POST(request: Request) {
const { to, subject, text, html } = await request.json();
try {
const info = await sendMail({ to, subject, text, html });
const { data: brandData } = await supabase.from('settings').select('primary_color').eq('id', 'branding').maybeSingle();
const primaryColor = brandData?.primary_color || '#2563eb';
const testHtml = html || `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="color: ${primaryColor}; margin-bottom: 16px;">Test-E-Mail</h2>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">${text || 'Dies ist eine automatische Test-E-Mail.'}</p>
<div style="margin-top: 24px; padding: 12px; background-color: #f8fafc; border-left: 4px solid ${primaryColor}; font-size: 14px; color: #334155;">
SMTP-Konfiguration & Branding-Farbe erfolgreich verifiziert.
</div>
</div>
`;
const info = await sendMail({ to, subject, text, html: testHtml });
return NextResponse.json({ message: 'Email sent', info });
} catch (err) {
console.error('Mail error', err);

View File

@@ -295,6 +295,9 @@ export async function POST(request: Request) {
const itemsSection = buildEmailItemsSection(items);
const { data: brandData } = await supabase.from('settings').select('primary_color').eq('id', 'branding').maybeSingle();
const primaryColor = brandData?.primary_color || '#2563eb';
const emailTemplate = getOrderEmailTemplate({
orderNumber: order.order_number,
formattedDate,
@@ -303,7 +306,7 @@ export async function POST(request: Request) {
totalDetailsHtml,
itemsDetailsText: itemsSection.text,
itemsDetailsHtml: itemsSection.html
}, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false);
}, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false, primaryColor);
const { data: bufferData, error: downloadError } = await supabase
.storage

View File

@@ -175,6 +175,7 @@ export function OrderWizard({
const [orderNotes, setOrderNotes] = useState<string>('')
const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null)
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null)
const [showValidationWarning, setShowValidationWarning] = useState(false)
useEffect(() => {
if (toast) {
@@ -545,14 +546,21 @@ export function OrderWizard({
}
const nextStep = () => {
if (step === 3 && allCategoriesFilled && productValidationErrors.length === 0) {
const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null
if (editingIdx !== null) {
addToBasket()
} else if (basketItems.length === 0 && hasActiveSelection) {
addToBasket()
} else if (isDirtyNewDevice && hasActiveSelection) {
addToBasket()
if (step === 3) {
if (isNextStepDisabled && basketItems.length === 0) {
setShowValidationWarning(true)
return
}
setShowValidationWarning(false)
if (allCategoriesFilled && productValidationErrors.length === 0) {
const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null
if (editingIdx !== null) {
addToBasket()
} else if (basketItems.length === 0 && hasActiveSelection) {
addToBasket()
} else if (isDirtyNewDevice && hasActiveSelection) {
addToBasket()
}
}
}
setDirection(1)
@@ -945,7 +953,7 @@ export function OrderWizard({
onClick={() => setActiveCategoryId(cat.id)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl border transition-all duration-300 text-left relative overflow-hidden group ${
isMissingRequired
? 'animate-eye-catcher bg-red-500/10 border-red-500 text-red-400 font-bold shadow-[0_0_20px_rgba(239,68,68,0.4)]'
? 'bg-red-500/10 border-red-500/50 text-red-400 font-bold'
: isActive
? 'bg-primary/10 border-primary text-white shadow-[0_0_15px_rgba(59,130,246,0.2)]'
: 'bg-slate-900/50 border-white/5 text-slate-400 hover:bg-slate-900 hover:border-white/20 hover:text-white'
@@ -1125,6 +1133,7 @@ export function OrderWizard({
addToBasket={addToBasket}
isNextStepDisabled={isNextStepDisabled}
hasActiveSelection={hasActiveSelection}
showValidationWarning={showValidationWarning}
nextStep={nextStep}
prevStep={prevStep}
/>

View File

@@ -102,7 +102,7 @@ export function StepSoftware({
return (
<div className={`flex items-center gap-3 p-4 rounded-xl border transition-all ${
isMissingRequired
? 'animate-eye-catcher bg-red-500/10 border-red-500 text-red-400 font-bold'
? 'bg-red-500/10 border-red-500/50 text-red-400 font-bold'
: 'bg-white/5 border-white/10'
}`}>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${

View File

@@ -45,6 +45,7 @@ interface SummarySidebarProps {
addToBasket: () => void
isNextStepDisabled: boolean
hasActiveSelection: boolean
showValidationWarning?: boolean
nextStep: () => void
prevStep: () => void
}
@@ -77,6 +78,7 @@ export function SummarySidebar({
addToBasket,
isNextStepDisabled,
hasActiveSelection,
showValidationWarning = false,
nextStep,
prevStep,
}: SummarySidebarProps) {
@@ -235,7 +237,7 @@ export function SummarySidebar({
<p>{updatePriceModifier.label}</p>
</div>
)}
{!allCategoriesFilled && (
{!allCategoriesFilled && showValidationWarning && (
<div className="text-xs text-red-400 flex items-start gap-2 bg-red-500/10 p-3 rounded-xl border border-red-500/40 animate-eye-catcher">
<AlertCircle className="w-4 h-4 shrink-0 text-red-400 mt-0.5" />
<div>
@@ -244,6 +246,15 @@ export function SummarySidebar({
</div>
</div>
)}
{!allCategoriesFilled && !showValidationWarning && (
<div className="text-xs text-slate-400 flex items-start gap-2 bg-slate-900/50 p-2.5 rounded-xl border border-white/5">
<AlertCircle className="w-4 h-4 shrink-0 text-slate-500 mt-0.5" />
<div>
<p className="font-semibold text-[11px] text-slate-300">Pflichtkategorien beachten</p>
<p className="text-[11px] text-slate-400">Bitte wählen Sie für die Rot markierten Kategorien ein Produkt aus.</p>
</div>
</div>
)}
{productValidationErrors.map((err, errIdx) => (
<p key={errIdx} className="text-xs text-red-400 flex items-center gap-1 bg-red-500/10 p-2 rounded-lg border border-red-500/20">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-red-400" />

View File

@@ -14,7 +14,8 @@ interface EmailDetails {
export function getOrderEmailTemplate(
details: EmailDetails,
siteUrl: string,
isUpdate: boolean = false
isUpdate: boolean = false,
primaryColor: string = '#2563eb'
) {
const title = isUpdate ? 'Anfrage geändert' : 'Anfrage erhalten'
const intro = isUpdate
@@ -65,7 +66,7 @@ export function getOrderEmailTemplate(
const html = `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="color: #0f172a; margin-bottom: 16px;">${title}</h2>
<h2 style="color: ${primaryColor}; margin-bottom: 16px;">${title}</h2>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">${intro}</p>
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
@@ -90,7 +91,7 @@ export function getOrderEmailTemplate(
</div>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Die Bestätigung liegt als PDF im Anhang.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">
<a href="${siteUrl}/api/orders/${details.orderNumber}/download" style="color: #3b82f6; text-decoration: underline; font-weight: 500;">Anfragebestätigung PDF herunterladen</a>
<a href="${siteUrl}/api/orders/${details.orderNumber}/download" style="color: ${primaryColor}; text-decoration: underline; font-weight: 500;">Anfragebestätigung PDF herunterladen</a>
</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Viele Grüße,<br>Dein CASPOS Team</p>
</div>

View File

@@ -117,6 +117,24 @@ export const PRESET_COLOR_SCHEMES: ColorPreset[] = [
buttonBg: '#2563eb',
ringColor: '#38bdf8',
},
{
id: 'caspos_classic_blue',
name: 'CASPOS Classic Blue',
description: 'Das originale CASPOS-Blau vertraut & clean',
primary: '#3b82f6',
accent: '#60a5fa',
success: '#10b981',
warning: '#f59e0b',
destructive: '#ef4444',
bgGlow1: '#3b82f6',
bgGlow2: '#1d4ed8',
gradientFrom: '#3b82f6',
gradientTo: '#2563eb',
cardBorder: 'rgba(96, 165, 250, 0.35)',
textHighlight: '#bfdbfe',
buttonBg: '#3b82f6',
ringColor: '#60a5fa',
},
{
id: 'emerald_green',
name: 'Emerald Green',