Integrations av abonnemangsfom
Lagt till AbonnemangForm med Zod-validering och planval iPlansSection samt koppling via dialog. Form stödjer företagsdata, kontakt, e-post, leveransadress och abonnemangsval, med klientvalidering och visuellt anpassad styling. Samt öppnar form i dialog när användare väljer abonnemang. X-Lovable-Edit-ID: edt-4e6dc072-bdf1-497e-be0c-62ebb95682f5
This commit is contained in:
@@ -0,0 +1,249 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
företag: z.string().trim().min(1, "Fyll i företagsnamn").max(100),
|
||||||
|
kontaktperson: z.string().trim().max(100).optional(),
|
||||||
|
epost: z.string().trim().email("Ange en giltig e-postadress").max(255),
|
||||||
|
telefon: z.string().trim().max(20).optional(),
|
||||||
|
adress: z.string().trim().max(200).optional(),
|
||||||
|
postnummer: z.string().trim().max(10).optional(),
|
||||||
|
stad: z.string().trim().max(100).optional(),
|
||||||
|
abonnemang: z.string().min(1, "Välj ett abonnemang"),
|
||||||
|
kommentarer: z.string().trim().max(1000).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AbonnemangFormProps {
|
||||||
|
selectedPlan?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AbonnemangForm = ({ selectedPlan, onClose }: AbonnemangFormProps) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
företag: "",
|
||||||
|
kontaktperson: "",
|
||||||
|
epost: "",
|
||||||
|
telefon: "",
|
||||||
|
adress: "",
|
||||||
|
postnummer: "",
|
||||||
|
stad: "",
|
||||||
|
abonnemang: selectedPlan || "",
|
||||||
|
kommentarer: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({ ...formData, [name]: value });
|
||||||
|
// Clear error when user types
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors({ ...errors, [name]: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErrors({});
|
||||||
|
|
||||||
|
const result = formSchema.safeParse(formData);
|
||||||
|
if (!result.success) {
|
||||||
|
const fieldErrors: Record<string, string> = {};
|
||||||
|
result.error.errors.forEach((err) => {
|
||||||
|
if (err.path[0]) {
|
||||||
|
fieldErrors[err.path[0] as string] = err.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setErrors(fieldErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, just show success - backend integration can be added later
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (submitted) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12 px-6">
|
||||||
|
<div className="divider-ornament mb-6">
|
||||||
|
<span className="ornament">✦</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display text-3xl md:text-4xl font-semibold mb-4">
|
||||||
|
Tack för din beställning!
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
Vi har tagit emot din beställning för{" "}
|
||||||
|
<strong className="text-primary">{formData.abonnemang}</strong> och
|
||||||
|
kontaktar dig inom kort.
|
||||||
|
</p>
|
||||||
|
{onClose && (
|
||||||
|
<button onClick={onClose} className="btn-classic">
|
||||||
|
Stäng
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="font-display text-3xl font-semibold mb-2">
|
||||||
|
Beställ abonnemang
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Fyll i formuläret så kontaktar vi dig
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Företagsnamn */}
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Företagsnamn <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="företag"
|
||||||
|
value={formData.företag}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
{errors.företag && (
|
||||||
|
<p className="text-destructive text-sm mt-1">{errors.företag}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kontaktperson */}
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Kontaktperson
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="kontaktperson"
|
||||||
|
value={formData.kontaktperson}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* E-post & Telefon */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
E-post <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="epost"
|
||||||
|
value={formData.epost}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
{errors.epost && (
|
||||||
|
<p className="text-destructive text-sm mt-1">{errors.epost}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Telefon
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="telefon"
|
||||||
|
value={formData.telefon}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Adress */}
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Leveransadress
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="adress"
|
||||||
|
value={formData.adress}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Postnummer & Stad */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Postnummer
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="postnummer"
|
||||||
|
value={formData.postnummer}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Stad
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="stad"
|
||||||
|
value={formData.stad}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Abonnemang */}
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Abonnemang <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="abonnemang"
|
||||||
|
value={formData.abonnemang}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
>
|
||||||
|
<option value="">Välj abonnemang</option>
|
||||||
|
<option value="Liten (3 kg/vecka)">Liten – 3 kg/vecka – 1 350 kr</option>
|
||||||
|
<option value="Mellan (5 kg/vecka)">Mellan – 5 kg/vecka – 2 250 kr</option>
|
||||||
|
<option value="Stor (10 kg/vecka)">Stor – 10 kg/vecka – 4 500 kr</option>
|
||||||
|
</select>
|
||||||
|
{errors.abonnemang && (
|
||||||
|
<p className="text-destructive text-sm mt-1">{errors.abonnemang}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kommentarer */}
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2 font-semibold text-foreground">
|
||||||
|
Kommentarer
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="kommentarer"
|
||||||
|
value={formData.kommentarer}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<button type="submit" className="w-full btn-classic py-4 text-lg">
|
||||||
|
Skicka beställning
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AbonnemangForm;
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import AbonnemangForm from "./AbonnemangForm";
|
||||||
|
|
||||||
const plans = [
|
const plans = [
|
||||||
{
|
{
|
||||||
@@ -37,6 +44,14 @@ const plans = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const PlansSection = () => {
|
const PlansSection = () => {
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<string>("");
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleSelectPlan = (plan: typeof plans[0]) => {
|
||||||
|
setSelectedPlan(`${plan.name} (${plan.kgPerWeek}/vecka)`);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="plans" className="py-24 px-6 bg-background">
|
<section id="plans" className="py-24 px-6 bg-background">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
@@ -94,12 +109,12 @@ const PlansSection = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<a
|
<button
|
||||||
href="#"
|
onClick={() => handleSelectPlan(plan)}
|
||||||
className="block text-center py-3 px-6 rounded-sm font-semibold transition-all duration-300 btn-classic"
|
className="block w-full text-center py-3 px-6 rounded-sm font-semibold transition-all duration-300 btn-classic"
|
||||||
>
|
>
|
||||||
Välj abonnemang
|
Välj abonnemang
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -109,6 +124,16 @@ const PlansSection = () => {
|
|||||||
❦
|
❦
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Order Dialog */}
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent className="max-w-xl max-h-[90vh] overflow-y-auto bg-card">
|
||||||
|
<AbonnemangForm
|
||||||
|
selectedPlan={selectedPlan}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user