3.05 Email Signature Automatizare si Branding
- AD prefill: 'Din cont' button pre-fills name + company from Authentik session - Logo size slider: 50%-200% scale control in Stil & Aranjare section - Promotional banner: configurable image+link below signature with preview - US/SDT custom graphics: dedicated dash (US) and dot (SDT) decorative icons replacing Beletage's grey/accent slashes for company-specific branding
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="7" viewBox="0 0 11 7">
|
||||
<circle cx="5.5" cy="3.5" r="2.5" fill="#0182A1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 142 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11">
|
||||
<circle cx="5.5" cy="5.5" r="3" fill="#A7A9AA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 142 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="7" viewBox="0 0 11 7">
|
||||
<rect x="1" y="2.5" width="9" height="2" rx="1" fill="#345476"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 156 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11">
|
||||
<rect x="1" y="4.5" width="9" height="2" rx="1" fill="#A7A9AA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 158 B |
@@ -8,6 +8,7 @@ import { SavedSignaturesPanel } from './saved-signatures-panel';
|
||||
import { Separator } from '@/shared/components/ui/separator';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import type { SignatureBanner } from '../types';
|
||||
|
||||
export function EmailSignatureModule() {
|
||||
const {
|
||||
@@ -17,6 +18,10 @@ export function EmailSignatureModule() {
|
||||
|
||||
const { saved, loading, save, remove } = useSavedSignatures();
|
||||
|
||||
const setBanner = (banner: SignatureBanner | undefined) => {
|
||||
updateField('banner', banner);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 lg:grid-cols-[360px_1fr]">
|
||||
{/* Left panel — configurator */}
|
||||
@@ -29,6 +34,7 @@ export function EmailSignatureModule() {
|
||||
onSetVariant={setVariant}
|
||||
onSetCompany={setCompany}
|
||||
onSetAddress={setAddress}
|
||||
onSetBanner={setBanner}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
SignatureColors,
|
||||
SignatureLayout,
|
||||
SignatureVariant,
|
||||
SignatureBanner,
|
||||
} from "../types";
|
||||
import {
|
||||
COMPANY_BRANDING,
|
||||
@@ -14,9 +15,11 @@ import {
|
||||
SDT_ADDRESSES,
|
||||
} from "../services/company-branding";
|
||||
import type { AddressKey } from "../services/company-branding";
|
||||
import { useAuth } from "@/core/auth";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { Switch } from "@/shared/components/ui/switch";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -26,6 +29,7 @@ import {
|
||||
} from "@/shared/components/ui/select";
|
||||
import { Separator } from "@/shared/components/ui/separator";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { UserCheck, ImagePlus, Trash2 } from "lucide-react";
|
||||
|
||||
interface SignatureConfiguratorProps {
|
||||
config: SignatureConfig;
|
||||
@@ -38,6 +42,7 @@ interface SignatureConfiguratorProps {
|
||||
onSetVariant: (variant: SignatureVariant) => void;
|
||||
onSetCompany: (company: CompanyId) => void;
|
||||
onSetAddress?: (address: string[]) => void;
|
||||
onSetBanner?: (banner: SignatureBanner | undefined) => void;
|
||||
}
|
||||
|
||||
/** Color palette per company */
|
||||
@@ -108,8 +113,10 @@ export function SignatureConfigurator({
|
||||
onSetVariant,
|
||||
onSetCompany,
|
||||
onSetAddress,
|
||||
onSetBanner,
|
||||
}: SignatureConfiguratorProps) {
|
||||
const palette = COMPANY_PALETTES[config.company];
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -248,7 +255,25 @@ export function SignatureConfigurator({
|
||||
|
||||
{/* Personal data */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Date personale</h3>
|
||||
{user && user.name && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => {
|
||||
onUpdateField("name", user.name);
|
||||
if (user.company && user.company !== "group") {
|
||||
onSetCompany(user.company);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<UserCheck className="mr-1 h-3 w-3" />
|
||||
Din cont
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="sig-prefix">Titulatură (prefix)</Label>
|
||||
<Input
|
||||
@@ -359,6 +384,28 @@ export function SignatureConfigurator({
|
||||
{/* Layout sliders */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Stil & Aranjare</h3>
|
||||
|
||||
{/* Logo scale slider */}
|
||||
<div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<Label>Dimensiune logo</Label>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round((config.layout.logoScale ?? 1) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={50}
|
||||
max={200}
|
||||
step={5}
|
||||
value={Math.round((config.layout.logoScale ?? 1) * 100)}
|
||||
onChange={(e) =>
|
||||
onUpdateLayout("logoScale", parseInt(e.target.value, 10) / 100)
|
||||
}
|
||||
className="mt-1 w-full accent-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{LAYOUT_CONTROLS.map(({ key, label, min, max }) => (
|
||||
<div key={key}>
|
||||
<div className="flex justify-between text-sm">
|
||||
@@ -380,6 +427,137 @@ export function SignatureConfigurator({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Promotional banner */}
|
||||
{onSetBanner && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Banner promoțional</h3>
|
||||
{config.banner ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => onSetBanner(undefined)}
|
||||
>
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Șterge
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() =>
|
||||
onSetBanner({
|
||||
imageUrl: "",
|
||||
linkUrl: "",
|
||||
alt: "Banner",
|
||||
width: 540,
|
||||
height: 80,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ImagePlus className="mr-1 h-3 w-3" />
|
||||
Adaugă
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{config.banner && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label htmlFor="banner-img">URL Imagine</Label>
|
||||
<Input
|
||||
id="banner-img"
|
||||
placeholder="https://example.com/banner.png"
|
||||
value={config.banner.imageUrl}
|
||||
onChange={(e) =>
|
||||
onSetBanner({
|
||||
...config.banner!,
|
||||
imageUrl: e.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="banner-link">URL Link (click)</Label>
|
||||
<Input
|
||||
id="banner-link"
|
||||
placeholder="https://example.com"
|
||||
value={config.banner.linkUrl}
|
||||
onChange={(e) =>
|
||||
onSetBanner({
|
||||
...config.banner!,
|
||||
linkUrl: e.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="banner-alt">Text alternativ</Label>
|
||||
<Input
|
||||
id="banner-alt"
|
||||
placeholder="Banner promoțional"
|
||||
value={config.banner.alt}
|
||||
onChange={(e) =>
|
||||
onSetBanner({ ...config.banner!, alt: e.target.value })
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="banner-w">Lățime (px)</Label>
|
||||
<Input
|
||||
id="banner-w"
|
||||
type="number"
|
||||
value={config.banner.width}
|
||||
onChange={(e) =>
|
||||
onSetBanner({
|
||||
...config.banner!,
|
||||
width: parseInt(e.target.value, 10) || 540,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="banner-h">Înălțime (px)</Label>
|
||||
<Input
|
||||
id="banner-h"
|
||||
type="number"
|
||||
value={config.banner.height}
|
||||
onChange={(e) =>
|
||||
onSetBanner({
|
||||
...config.banner!,
|
||||
height: parseInt(e.target.value, 10) || 80,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{config.banner.imageUrl && (
|
||||
<div className="rounded-md border p-2">
|
||||
<p className="mb-1 text-xs text-muted-foreground">
|
||||
Previzualizare:
|
||||
</p>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={config.banner.imageUrl}
|
||||
alt={config.banner.alt || "Banner"}
|
||||
className="max-h-24 w-full rounded object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { SignatureConfig, SignatureVariant, SignatureColors, SignatureLayout } from '../types';
|
||||
import { getBranding } from '../services/company-branding';
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type {
|
||||
SignatureConfig,
|
||||
SignatureVariant,
|
||||
SignatureColors,
|
||||
SignatureLayout,
|
||||
} from "../types";
|
||||
import { getBranding } from "../services/company-branding";
|
||||
|
||||
const DEFAULT_LAYOUT: SignatureLayout = {
|
||||
greenLineWidth: 97,
|
||||
@@ -14,46 +19,55 @@ const DEFAULT_LAYOUT: SignatureLayout = {
|
||||
sectionSpacing: 10,
|
||||
titleSpacing: 2,
|
||||
logoSpacing: 10,
|
||||
logoScale: 1,
|
||||
};
|
||||
|
||||
function createDefaultConfig(company: CompanyId = 'beletage'): SignatureConfig {
|
||||
function createDefaultConfig(company: CompanyId = "beletage"): SignatureConfig {
|
||||
const branding = getBranding(company);
|
||||
return {
|
||||
prefix: 'arh.',
|
||||
name: '',
|
||||
title: '',
|
||||
phone: '',
|
||||
prefix: "arh.",
|
||||
name: "",
|
||||
title: "",
|
||||
phone: "",
|
||||
company,
|
||||
colors: { ...branding.defaultColors },
|
||||
layout: { ...DEFAULT_LAYOUT },
|
||||
variant: 'full',
|
||||
variant: "full",
|
||||
useSvg: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function useSignatureConfig(initialCompany: CompanyId = 'beletage') {
|
||||
const [config, setConfig] = useState<SignatureConfig>(() => createDefaultConfig(initialCompany));
|
||||
export function useSignatureConfig(initialCompany: CompanyId = "beletage") {
|
||||
const [config, setConfig] = useState<SignatureConfig>(() =>
|
||||
createDefaultConfig(initialCompany),
|
||||
);
|
||||
|
||||
const updateField = useCallback(<K extends keyof SignatureConfig>(
|
||||
key: K,
|
||||
value: SignatureConfig[K]
|
||||
) => {
|
||||
const updateField = useCallback(
|
||||
<K extends keyof SignatureConfig>(key: K, value: SignatureConfig[K]) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateColor = useCallback((key: keyof SignatureColors, value: string) => {
|
||||
const updateColor = useCallback(
|
||||
(key: keyof SignatureColors, value: string) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
colors: { ...prev.colors, [key]: value },
|
||||
}));
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateLayout = useCallback((key: keyof SignatureLayout, value: number) => {
|
||||
const updateLayout = useCallback(
|
||||
(key: keyof SignatureLayout, value: number) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
layout: { ...prev.layout, [key]: value },
|
||||
}));
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const setVariant = useCallback((variant: SignatureVariant) => {
|
||||
setConfig((prev) => ({ ...prev, variant }));
|
||||
@@ -80,7 +94,8 @@ export function useSignatureConfig(initialCompany: CompanyId = 'beletage') {
|
||||
setConfig(loaded);
|
||||
}, []);
|
||||
|
||||
return useMemo(() => ({
|
||||
return useMemo(
|
||||
() => ({
|
||||
config,
|
||||
updateField,
|
||||
updateColor,
|
||||
@@ -90,5 +105,17 @@ export function useSignatureConfig(initialCompany: CompanyId = 'beletage') {
|
||||
setAddress,
|
||||
resetToDefaults,
|
||||
loadConfig,
|
||||
}), [config, updateField, updateColor, updateLayout, setVariant, setCompany, setAddress, resetToDefaults, loadConfig]);
|
||||
}),
|
||||
[
|
||||
config,
|
||||
updateField,
|
||||
updateColor,
|
||||
updateLayout,
|
||||
setVariant,
|
||||
setCompany,
|
||||
setAddress,
|
||||
resetToDefaults,
|
||||
loadConfig,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,10 +103,13 @@ export const COMPANY_BRANDING: Record<CompanyId, CompanyBranding> = {
|
||||
svg: "/logos/logo-us-light.svg",
|
||||
},
|
||||
slashGrey: {
|
||||
png: "https://beletage.ro/img/Grey-slash.png",
|
||||
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||
png: "/logos/us-dash-grey.svg",
|
||||
svg: "/logos/us-dash-grey.svg",
|
||||
},
|
||||
slashAccent: {
|
||||
png: "/logos/us-dash-accent.svg",
|
||||
svg: "/logos/us-dash-accent.svg",
|
||||
},
|
||||
slashAccent: { png: "", svg: "" },
|
||||
logoDimensions: { width: 140, height: 24 },
|
||||
address: [...ADDR_CHRISTESCU],
|
||||
website: "www.urbanswitch.ro",
|
||||
@@ -122,10 +125,13 @@ export const COMPANY_BRANDING: Record<CompanyId, CompanyBranding> = {
|
||||
svg: "/logos/logo-sdt-light.svg",
|
||||
},
|
||||
slashGrey: {
|
||||
png: "https://beletage.ro/img/Grey-slash.png",
|
||||
svg: "https://beletage.ro/img/Grey-slash.svg",
|
||||
png: "/logos/sdt-dot-grey.svg",
|
||||
svg: "/logos/sdt-dot-grey.svg",
|
||||
},
|
||||
slashAccent: {
|
||||
png: "/logos/sdt-dot-accent.svg",
|
||||
svg: "/logos/sdt-dot-accent.svg",
|
||||
},
|
||||
slashAccent: { png: "", svg: "" },
|
||||
logoDimensions: { width: 71, height: 24 },
|
||||
address: [...ADDR_CHRISTESCU],
|
||||
website: "www.studiideteren.ro",
|
||||
|
||||
@@ -37,13 +37,19 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
||||
sectionSpacing,
|
||||
titleSpacing,
|
||||
logoSpacing,
|
||||
logoScale,
|
||||
} = config.layout;
|
||||
const colors = config.colors;
|
||||
|
||||
const isReply = config.variant === "reply" || config.variant === "minimal";
|
||||
const isMinimal = config.variant === "minimal";
|
||||
|
||||
const logoDim = branding.logoDimensions ?? { width: 162, height: 24 };
|
||||
const rawDim = branding.logoDimensions ?? { width: 162, height: 24 };
|
||||
const scale = logoScale ?? 1;
|
||||
const logoDim = {
|
||||
width: Math.round(rawDim.width * scale),
|
||||
height: Math.round(rawDim.height * scale),
|
||||
};
|
||||
|
||||
const hide =
|
||||
"mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;";
|
||||
@@ -122,6 +128,7 @@ export function generateSignatureHtml(config: SignatureConfig): string {
|
||||
</td>
|
||||
</tr>
|
||||
${branding.motto ? `<tr style="${hideBottom}"><td style="padding:${mottoSpacing}px 0 0 0;"><span style="font-size:12px; color:${colors.motto}; font-style:italic;">${esc(branding.motto)}</span></td></tr>` : ""}
|
||||
${config.banner?.imageUrl ? `<tr style="${hideBottom}"><td style="padding:12px 0 0 0;"><a href="${esc(config.banner.linkUrl || "#")}" style="text-decoration:none; border:0;"><img src="${esc(config.banner.imageUrl)}" alt="${esc(config.banner.alt || "Banner")}" width="${config.banner.width || 540}" height="${config.banner.height || 80}" style="display:block; border:0; max-width:540px; height:auto;"></a></td></tr>` : ""}
|
||||
</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,16 @@ export interface SignatureLayout {
|
||||
sectionSpacing: number;
|
||||
titleSpacing: number;
|
||||
logoSpacing: number;
|
||||
/** Logo scale factor (0.5–2.0, default 1.0) */
|
||||
logoScale: number;
|
||||
}
|
||||
|
||||
export interface SignatureBanner {
|
||||
imageUrl: string;
|
||||
linkUrl: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface CompanyBranding {
|
||||
@@ -52,6 +62,8 @@ export interface SignatureConfig {
|
||||
useSvg: boolean;
|
||||
/** Override the default company address */
|
||||
addressOverride?: string[];
|
||||
/** Optional promotional banner below signature */
|
||||
banner?: SignatureBanner;
|
||||
}
|
||||
|
||||
export interface SavedSignature {
|
||||
|
||||
Reference in New Issue
Block a user