Changes
Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
interface Plan {
|
||||
kg: number;
|
||||
pricePerKg: number;
|
||||
shopifyLink: string;
|
||||
}
|
||||
|
||||
interface AbonnemangCheckoutProps {
|
||||
selectedPlan: string;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
const plans: Record<string, Plan> = {
|
||||
"Liten": {
|
||||
kg: 3,
|
||||
pricePerKg: 325,
|
||||
shopifyLink: "#" // Replace with actual Shopify link
|
||||
},
|
||||
"Mellan": {
|
||||
kg: 5,
|
||||
pricePerKg: 325,
|
||||
shopifyLink: "#"
|
||||
},
|
||||
"Stor": {
|
||||
kg: 10,
|
||||
pricePerKg: 400,
|
||||
shopifyLink: "#"
|
||||
},
|
||||
};
|
||||
|
||||
const AbonnemangCheckout = ({ selectedPlan, onBack }: AbonnemangCheckoutProps) => {
|
||||
const planName = selectedPlan.split(" (")[0]; // Extract plan name from "Liten (3 kg/vecka)"
|
||||
const plan = plans[planName] || plans["Mellan"];
|
||||
const weeklyPrice = plan.kg * plan.pricePerKg;
|
||||
const monthlyPrice = weeklyPrice * 4;
|
||||
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-24 h-px bg-primary/40 mx-auto mb-6"></div>
|
||||
|
||||
<h2 className="font-display text-3xl md:text-4xl font-semibold mb-4">
|
||||
Ditt abonnemang: {planName}
|
||||
</h2>
|
||||
|
||||
<div className="text-muted-foreground mb-6 space-y-2">
|
||||
<p>
|
||||
Du beställer <strong className="text-foreground">{plan.kg} kg</strong> kakor per vecka.
|
||||
</p>
|
||||
<p>
|
||||
Pris: <strong className="text-foreground">{weeklyPrice.toLocaleString("sv-SE")} kr/vecka</strong>{" "}
|
||||
<span className="text-sm">(~{monthlyPrice.toLocaleString("sv-SE")} kr/månad exkl. moms)</span>
|
||||
</p>
|
||||
<p className="text-primary font-semibold mt-4">
|
||||
Kakorna levereras nästa tisdag!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm mb-8">
|
||||
Betala direkt via kort eller med Klarna. Ingen manuell kontakt behövs.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-8">
|
||||
<a
|
||||
href={plan.shopifyLink}
|
||||
className="btn-classic px-8 py-4 text-center"
|
||||
>
|
||||
Betala med kort
|
||||
</a>
|
||||
<a
|
||||
href={plan.shopifyLink + "?payment=klarna"}
|
||||
className="btn-outline-classic px-8 py-4 text-center"
|
||||
>
|
||||
Betala med Klarna
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
När betalningen är genomförd skickas automatiskt ett bekräftelsemail
|
||||
och kakorna levereras nästa tisdag.
|
||||
</p>
|
||||
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="mt-6 text-primary underline hover:no-underline"
|
||||
>
|
||||
← Tillbaka till formulär
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AbonnemangCheckout;
|
||||
@@ -1,328 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
|
||||
// Validate Stockholm postal codes (100 00 - 199 99)
|
||||
const isStockholmPostalCode = (postalCode: string) => {
|
||||
const cleaned = postalCode.replace(/\s/g, "");
|
||||
if (!/^\d{5}$/.test(cleaned)) return false;
|
||||
const num = parseInt(cleaned, 10);
|
||||
return num >= 10000 && num <= 19999;
|
||||
};
|
||||
|
||||
// Get error message for postal code (for real-time validation display)
|
||||
const getPostalCodeError = (postalCode: string) => {
|
||||
if (!postalCode || postalCode.trim() === "") return null;
|
||||
const cleaned = postalCode.replace(/\s/g, "");
|
||||
if (cleaned.length < 5) return null; // Don't show error while typing
|
||||
if (!/^\d{5}$/.test(cleaned)) return "Ange ett giltigt postnummer (5 siffror)";
|
||||
const num = parseInt(cleaned, 10);
|
||||
if (num < 10000 || num > 19999) {
|
||||
return "Vi levererar endast inom Storstockholm (100 00 – 199 99)";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
företag: z.string().trim().min(1, "Fyll i företagsnamn").max(100),
|
||||
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()
|
||||
.min(1, "Fyll i postnummer för leverans")
|
||||
.max(10)
|
||||
.refine(
|
||||
(val) => isStockholmPostalCode(val),
|
||||
{ message: "Vi levererar endast inom Storstockholm (100 00 – 199 99)" }
|
||||
),
|
||||
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 { user } = useAuth();
|
||||
const [formData, setFormData] = useState({
|
||||
företag: "",
|
||||
epost: "",
|
||||
telefon: "",
|
||||
adress: "",
|
||||
postnummer: "",
|
||||
stad: "",
|
||||
abonnemang: selectedPlan || "",
|
||||
kommentarer: "",
|
||||
});
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Pre-fill form with user's profile data if logged in
|
||||
useEffect(() => {
|
||||
const fetchProfileData = async () => {
|
||||
if (user) {
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("display_name, phone, address")
|
||||
.eq("user_id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (data) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
telefon: data.phone || prev.telefon,
|
||||
epost: user.email || prev.epost,
|
||||
adress: data.address?.split(",")[0]?.trim() || prev.adress,
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
epost: user.email || prev.epost,
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchProfileData();
|
||||
}, [user]);
|
||||
|
||||
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 saveAddressToProfile = async () => {
|
||||
if (!user) return;
|
||||
|
||||
// Combine address fields into one string
|
||||
const fullAddress = [
|
||||
formData.adress,
|
||||
formData.postnummer && formData.stad
|
||||
? `${formData.postnummer} ${formData.stad}`
|
||||
: formData.postnummer || formData.stad,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
if (fullAddress) {
|
||||
await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
address: fullAddress,
|
||||
phone: formData.telefon || null,
|
||||
})
|
||||
.eq("user_id", user.id);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Save address to profile if user is logged in
|
||||
await saveAddressToProfile();
|
||||
|
||||
// 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="w-24 h-px bg-primary/40 mx-auto mb-6"></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>
|
||||
|
||||
{/* 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 <span className="text-primary">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="postnummer"
|
||||
placeholder="t.ex. 114 32"
|
||||
value={formData.postnummer}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-3 rounded-sm bg-background border text-foreground focus:outline-none focus:ring-2 ${
|
||||
getPostalCodeError(formData.postnummer)
|
||||
? "border-destructive focus:ring-destructive/30"
|
||||
: "border-border focus:ring-primary/30"
|
||||
}`}
|
||||
/>
|
||||
{(errors.postnummer || getPostalCodeError(formData.postnummer)) && (
|
||||
<p className="text-destructive text-sm mt-1">
|
||||
{errors.postnummer || getPostalCodeError(formData.postnummer)}
|
||||
</p>
|
||||
)}
|
||||
</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 – 975 kr</option>
|
||||
<option value="Mellan (5 kg/vecka)">Mellan – 5 kg/vecka – 1 625 kr</option>
|
||||
<option value="Stor (10 kg/vecka)">Stor – 10 kg/vecka – 4 000 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,33 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { storefrontApiRequest, STOREFRONT_PRODUCTS_QUERY, ShopifyProduct } from '@/lib/shopify';
|
||||
|
||||
export function useShopifyProducts(query?: string) {
|
||||
const [products, setProducts] = useState<ShopifyProduct[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchProducts() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await storefrontApiRequest(STOREFRONT_PRODUCTS_QUERY, {
|
||||
first: 10,
|
||||
query: query || null
|
||||
});
|
||||
if (data?.data?.products?.edges) {
|
||||
setProducts(data.data.products.edges);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch products:', err);
|
||||
setError('Kunde inte ladda produkter');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchProducts();
|
||||
}, [query]);
|
||||
|
||||
return { products, isLoading, error };
|
||||
}
|
||||
Reference in New Issue
Block a user