Typen Social Media und Video
Social Media buendelt Profile auf einer Seite. Wer „@firma" eintippt, meint das Profil beim gewaehlten Netzwerk — daraus wird die volle Adresse. Statt nachgezeichneter Markenlogos steht ein Kreis in der Hausfarbe mit Monogramm; ein schlecht nachgebautes Logo sieht schlechter aus als ein sauberes Zeichen. Video erkennt YouTube, Vimeo und direkte Dateien. Der Rahmen zum Anbieter laedt erst auf Klick, und davor steht, dass dabei Daten uebertragen werden — diese Seite bekommen Fremde nach einem Scan zu sehen, ungefragt einen Drittanbieter einzubinden waere nicht in Ordnung. Videodateien laden mit preload="none": wer unterwegs scannt, zahlt das Volumen.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { Icon } from "@/components/icons";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
import { Button, Field, Select, TextArea, TextInput } from "@/components/ui";
|
||||
import { SOCIAL_NETWORKS, getSocialNetwork } from "@/lib/qr/social-networks";
|
||||
import { MAX_PROFILES } from "@/lib/qr/types/social";
|
||||
|
||||
type Profile = { network: string; url: string };
|
||||
|
||||
function readProfiles(value: unknown): Profile[] {
|
||||
if (!Array.isArray(value)) return [{ network: "instagram", url: "" }];
|
||||
const entries = value.map((entry) => {
|
||||
const record = (entry ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
network: typeof record.network === "string" ? record.network : "website",
|
||||
url: typeof record.url === "string" ? record.url : "",
|
||||
};
|
||||
});
|
||||
return entries.length > 0 ? entries : [{ network: "instagram", url: "" }];
|
||||
}
|
||||
|
||||
export function SocialForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const title = typeof value.title === "string" ? value.title : "";
|
||||
const description =
|
||||
typeof value.description === "string" ? value.description : "";
|
||||
const profiles = readProfiles(value.profiles);
|
||||
|
||||
const belegt = new Set(profiles.map((profile) => profile.network));
|
||||
const naechstesFreies =
|
||||
SOCIAL_NETWORKS.find((network) => !belegt.has(network.id))?.id ?? "website";
|
||||
|
||||
const setProfiles = (next: Profile[]) =>
|
||||
onChange({ ...value, profiles: next });
|
||||
|
||||
const updateProfile = (index: number, patch: Partial<Profile>) =>
|
||||
setProfiles(
|
||||
profiles.map((profile, i) => (i === index ? { ...profile, ...patch } : profile)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<Field
|
||||
label="Überschrift der Seite"
|
||||
htmlFor="social-title"
|
||||
hint="Meist der Name, unter dem man euch kennt."
|
||||
>
|
||||
<TextInput
|
||||
id="social-title"
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
maxLength={80}
|
||||
placeholder="Bäckerei Sonnenschein"
|
||||
onChange={(event) => onChange({ ...value, title: event.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Kurzer Text (optional)"
|
||||
htmlFor="social-description"
|
||||
hint="Eine Zeile darunter, z. B. „Folge uns für Neues aus der Backstube“."
|
||||
>
|
||||
<TextArea
|
||||
id="social-description"
|
||||
rows={2}
|
||||
value={description}
|
||||
disabled={disabled}
|
||||
maxLength={300}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, description: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-baseline justify-between gap-3">
|
||||
<span className="text-sm font-medium text-slate-700">Profile</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{profiles.length} von {MAX_PROFILES}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3">
|
||||
{profiles.map((profile, index) => {
|
||||
const network = getSocialNetwork(profile.network);
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="rounded-xl border border-slate-200 bg-slate-50/70 p-3"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<Select
|
||||
aria-label={`Netzwerk von Profil ${index + 1}`}
|
||||
value={profile.network}
|
||||
disabled={disabled}
|
||||
onChange={(event) =>
|
||||
updateProfile(index, { network: event.target.value })
|
||||
}
|
||||
>
|
||||
{SOCIAL_NETWORKS.map((entry) => (
|
||||
<option
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
// Doppelte Netzwerke lehnt die Prüfung ohnehin ab —
|
||||
// besser, sie stehen gar nicht erst zur Wahl.
|
||||
disabled={
|
||||
entry.id !== profile.network && belegt.has(entry.id)
|
||||
}
|
||||
>
|
||||
{entry.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<TextInput
|
||||
aria-label={`Profil ${index + 1}`}
|
||||
inputMode="url"
|
||||
spellCheck={false}
|
||||
value={profile.url}
|
||||
disabled={disabled}
|
||||
maxLength={2048}
|
||||
placeholder={network?.placeholder ?? "https://example.com"}
|
||||
onChange={(event) =>
|
||||
updateProfile(index, { url: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || profiles.length === 1}
|
||||
onClick={() =>
|
||||
setProfiles(profiles.filter((_, i) => i !== index))
|
||||
}
|
||||
aria-label={`Profil ${index + 1} entfernen`}
|
||||
className="mt-1 rounded-md p-1.5 text-slate-500 transition-colors hover:bg-white hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
||||
>
|
||||
<Icon name="trash" className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{profiles.length < MAX_PROFILES &&
|
||||
profiles.length < SOCIAL_NETWORKS.length ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setProfiles([...profiles, { network: naechstesFreies, url: "" }])
|
||||
}
|
||||
className="mt-3"
|
||||
>
|
||||
Profil hinzufügen
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p role="alert" className="mt-2 text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user