Ersatte abonnemang med kalkylator
X-Lovable-Edit-ID: edt-4014a865-0fc7-446a-9e5d-c147aaf8cdde Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
This commit is contained in:
+106
-128
@@ -1,68 +1,45 @@
|
|||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { Check, Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useShopifyProducts } from "@/hooks/useShopifyProducts";
|
import { useShopifyProducts } from "@/hooks/useShopifyProducts";
|
||||||
import { useCartStore } from "@/stores/cartStore";
|
import { useCartStore } from "@/stores/cartStore";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const planDetails = [
|
const PRICE_PER_KG = 325;
|
||||||
{
|
const KG_PER_EMPLOYEE = 0.3;
|
||||||
name: "Liten",
|
|
||||||
kgPerWeek: "3 kg",
|
// Map recommended kg to the closest available Shopify subscription plan
|
||||||
priceWeekExclVat: "975",
|
const availablePlans = [
|
||||||
employeeRecommendation: "1–10 anställda",
|
{ kg: 3, handle: "liten-kakabonnemang-3-kg-vecka", name: "Liten" },
|
||||||
features: [
|
{ kg: 5, handle: "mellan-kakabonnemang-5-kg-vecka", name: "Mellan" },
|
||||||
"3 kg kakor per vecka",
|
{ kg: 10, handle: "stor-kakabonnemang-10-kg-vecka", name: "Stor" },
|
||||||
"Veckoleverans",
|
|
||||||
"Betala direkt eller med Klarna",
|
|
||||||
],
|
|
||||||
shopifyHandle: "liten-kakabonnemang-3-kg-vecka",
|
|
||||||
isPremium: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mellan",
|
|
||||||
kgPerWeek: "5 kg",
|
|
||||||
priceWeekExclVat: "1 625",
|
|
||||||
employeeRecommendation: "10–25 anställda",
|
|
||||||
features: [
|
|
||||||
"5 kg kakor per vecka",
|
|
||||||
"Veckoleverans",
|
|
||||||
"Betala direkt eller med Klarna",
|
|
||||||
],
|
|
||||||
shopifyHandle: "mellan-kakabonnemang-5-kg-vecka",
|
|
||||||
isPremium: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Stor",
|
|
||||||
kgPerWeek: "10 kg",
|
|
||||||
priceWeekExclVat: "4 000",
|
|
||||||
employeeRecommendation: "25+ anställda",
|
|
||||||
features: [
|
|
||||||
"10 kg kakor per vecka",
|
|
||||||
"Veckoleverans",
|
|
||||||
"Betala direkt eller med Klarna",
|
|
||||||
"Prioriterad leverans",
|
|
||||||
],
|
|
||||||
shopifyHandle: "stor-kakabonnemang-10-kg-vecka",
|
|
||||||
isPremium: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pickPlan = (kg: number) => {
|
||||||
|
const match = availablePlans.find((p) => p.kg >= kg);
|
||||||
|
return match || availablePlans[availablePlans.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
const PlansSection = () => {
|
const PlansSection = () => {
|
||||||
const [selectedPlan, setSelectedPlan] = useState<string>("");
|
const [employees, setEmployees] = useState(10);
|
||||||
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
||||||
|
|
||||||
const { products, isLoading: productsLoading } = useShopifyProducts("product_type:Abonnemang");
|
const { products, isLoading: productsLoading } = useShopifyProducts("product_type:Abonnemang");
|
||||||
const { addItem, clearCart, isLoading: cartLoading } = useCartStore();
|
const { addItem, clearCart, isLoading: cartLoading } = useCartStore();
|
||||||
|
|
||||||
const handleSelectPlan = async (plan: typeof planDetails[0]) => {
|
const recommendedKg = useMemo(
|
||||||
// Find the matching Shopify product
|
() => Math.max(1, Math.round(employees * KG_PER_EMPLOYEE)),
|
||||||
const shopifyProduct = products.find(p => p.node.handle === plan.shopifyHandle);
|
[employees]
|
||||||
|
);
|
||||||
|
const weeklyPrice = recommendedKg * PRICE_PER_KG;
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
const plan = pickPlan(recommendedKg);
|
||||||
|
const shopifyProduct = products.find((p) => p.node.handle === plan.handle);
|
||||||
|
|
||||||
if (!shopifyProduct) {
|
if (!shopifyProduct) {
|
||||||
toast.error("Kunde inte hitta produkten");
|
toast.error("Kunde inte hitta produkten");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variant = shopifyProduct.node.variants.edges[0]?.node;
|
const variant = shopifyProduct.node.variants.edges[0]?.node;
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
toast.error("Ingen variant tillgänglig");
|
toast.error("Ingen variant tillgänglig");
|
||||||
@@ -70,12 +47,8 @@ const PlansSection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCheckoutLoading(true);
|
setCheckoutLoading(true);
|
||||||
setSelectedPlan(plan.name);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear any existing cart items first for a clean order
|
|
||||||
clearCart();
|
clearCart();
|
||||||
|
|
||||||
await addItem({
|
await addItem({
|
||||||
product: shopifyProduct,
|
product: shopifyProduct,
|
||||||
variantId: variant.id,
|
variantId: variant.id,
|
||||||
@@ -84,12 +57,11 @@ const PlansSection = () => {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
selectedOptions: variant.selectedOptions || [],
|
selectedOptions: variant.selectedOptions || [],
|
||||||
});
|
});
|
||||||
|
toast.success(`Abonnemang tillagt (${plan.name} – ${plan.kg} kg/vecka)`, {
|
||||||
toast.success(`${plan.name} tillagt i varukorgen`, {
|
|
||||||
description: "Klicka på varukorgen för att gå till betalning",
|
description: "Klicka på varukorgen för att gå till betalning",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error('Add to cart error:', error);
|
console.error(e);
|
||||||
toast.error("Något gick fel");
|
toast.error("Något gick fel");
|
||||||
} finally {
|
} finally {
|
||||||
setCheckoutLoading(false);
|
setCheckoutLoading(false);
|
||||||
@@ -99,84 +71,90 @@ const PlansSection = () => {
|
|||||||
const isLoading = productsLoading || cartLoading || checkoutLoading;
|
const isLoading = productsLoading || cartLoading || checkoutLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="abonnemang" className="py-12 md:py-24 px-2 md:px-6 bg-background">
|
<section id="abonnemang" className="py-16 md:py-28 px-6 bg-background">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
{/* Section header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8 md:mb-16">
|
<div className="text-center mb-10 md:mb-14">
|
||||||
<div className="w-16 md:w-24 h-px bg-primary/40 mx-auto mb-4 md:mb-6"></div>
|
<div className="w-24 h-px bg-primary/40 mx-auto mb-6"></div>
|
||||||
<h2 className="font-display text-2xl md:text-5xl font-semibold mb-2 md:mb-4">
|
<h2 className="font-display text-3xl md:text-5xl font-semibold mb-4">
|
||||||
Våra Abonnemang
|
Hur många anställda har ni?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground text-sm md:text-lg max-w-2xl mx-auto">
|
<p className="text-muted-foreground text-base md:text-lg max-w-xl mx-auto">
|
||||||
Välj det abonnemang som passar ert företag bäst
|
Vi rekommenderar automatiskt rätt mängd kakor utifrån antalet medarbetare.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plans grid */}
|
{/* Calculator card */}
|
||||||
<div className="grid grid-cols-3 gap-4 md:gap-8">
|
<div className="bg-card border border-border rounded-sm shadow-[0_10px_40px_-15px_rgba(0,0,0,0.15)] p-6 md:p-12">
|
||||||
{planDetails.map((plan, index) => (
|
{/* Employee count display */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={employees}
|
||||||
className={`relative bg-card p-3 md:p-8 card-classic rounded-sm flex flex-col ${
|
className="font-display text-6xl md:text-8xl font-semibold text-primary tabular-nums animate-fade-in"
|
||||||
plan.isPremium
|
|
||||||
? 'ring-2 ring-amber-500 hover:ring-2 hover:ring-amber-500 shadow-[0_0_20px_rgba(217,164,32,0.3)] hover:shadow-[0_0_30px_rgba(217,164,32,0.4)]'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
|
{employees}
|
||||||
{/* Plan header */}
|
|
||||||
<div className={`text-center mb-3 md:mb-6 pb-3 md:pb-6 border-b ${plan.isPremium ? 'border-amber-400/50' : 'border-border'}`}>
|
|
||||||
<h3 className={`font-display text-base md:text-3xl font-semibold ${plan.isPremium ? 'text-amber-700' : 'text-foreground'}`}>
|
|
||||||
{plan.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground text-[10px] md:text-sm mt-1 md:mt-2 leading-tight">
|
|
||||||
{plan.employeeRecommendation}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Price info */}
|
|
||||||
<div className="text-center mb-3 md:mb-6 space-y-1 md:space-y-2">
|
|
||||||
<div className="text-muted-foreground text-[10px] md:text-sm">
|
|
||||||
{plan.kgPerWeek}/vecka
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-display text-sm md:text-3xl font-bold text-primary">
|
|
||||||
{plan.priceWeekExclVat}
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground text-[8px] md:text-base ml-0.5 md:ml-1">kr/v</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Features - hidden on mobile */}
|
|
||||||
<ul className="hidden md:block space-y-3 mb-8 flex-grow">
|
|
||||||
{plan.features.map((feature, i) => (
|
|
||||||
<li key={i} className="flex items-start gap-3">
|
|
||||||
<Check className="w-4 h-4 text-primary mt-1 flex-shrink-0" />
|
|
||||||
<span className="text-foreground/80 text-sm">{feature}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{/* CTA Button - always at bottom */}
|
|
||||||
<button
|
|
||||||
onClick={() => handleSelectPlan(plan)}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="block w-full text-center py-1.5 md:py-3 px-2 md:px-6 rounded-sm text-[10px] md:text-base font-semibold transition-all duration-300 btn-classic disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading && selectedPlan === plan.name ? (
|
|
||||||
<span className="flex items-center justify-center gap-1 md:gap-2">
|
|
||||||
<Loader2 className="w-3 h-3 md:w-4 md:h-4 animate-spin" />
|
|
||||||
<span className="hidden md:inline">Laddar...</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
"Beställ"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="text-muted-foreground text-sm md:text-base mt-2 tracking-wide uppercase">
|
||||||
|
{employees === 1 ? "anställd" : "anställda"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slider */}
|
||||||
|
<div className="mb-10 md:mb-12">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={employees}
|
||||||
|
onChange={(e) => setEmployees(parseInt(e.target.value, 10))}
|
||||||
|
className="calculator-slider w-full"
|
||||||
|
aria-label="Antal anställda"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground mt-3">
|
||||||
|
<span>1</span>
|
||||||
|
<span>100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
<div className="text-center border-t border-border pt-8 md:pt-10 space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
||||||
|
Rekommenderad mängd
|
||||||
|
</div>
|
||||||
|
<div className="font-display text-4xl md:text-5xl font-semibold text-foreground tabular-nums">
|
||||||
|
{recommendedKg} kg
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm mt-1">per vecka</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
||||||
|
Pris per vecka
|
||||||
|
</div>
|
||||||
|
<div className="font-display text-3xl md:text-4xl font-semibold text-primary tabular-nums">
|
||||||
|
{weeklyPrice.toLocaleString("sv-SE")} kr
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs mt-1">exklusive moms</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn-classic w-full py-4 text-base md:text-lg rounded-sm disabled:opacity-50 mt-4"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
Laddar...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"Starta abonnemang"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -175,4 +175,47 @@
|
|||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Calculator slider — minimal, exclusive */
|
||||||
|
.calculator-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 4px;
|
||||||
|
background: hsl(var(--border));
|
||||||
|
border-radius: 999px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
.calculator-slider:hover {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
}
|
||||||
|
.calculator-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
border: 3px solid hsl(var(--card));
|
||||||
|
box-shadow: 0 2px 10px hsl(var(--primary) / 0.35);
|
||||||
|
cursor: grab;
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
.calculator-slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 16px hsl(var(--primary) / 0.5);
|
||||||
|
}
|
||||||
|
.calculator-slider::-webkit-slider-thumb:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.calculator-slider::-moz-range-thumb {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
border: 3px solid hsl(var(--card));
|
||||||
|
box-shadow: 0 2px 10px hsl(var(--primary) / 0.35);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user