mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat: added contact and pricing
- Updated framer motion
This commit is contained in:
parent
5acc05a119
commit
1a3faf03d5
55 changed files with 1802 additions and 55 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import { IconMenu2, IconUser, IconX } from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion, useMotionValueEvent, useScroll } from "framer-motion";
|
||||
import { AnimatePresence, motion, useMotionValueEvent, useScroll } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useInView } from "framer-motion";
|
||||
import { useInView } from "motion/react";
|
||||
import { Manrope } from "next/font/google";
|
||||
import { useEffect, useMemo, useReducer, useRef } from "react";
|
||||
import { RoughNotation, RoughNotationGroup } from "react-rough-notation";
|
||||
|
|
|
|||
282
surfsense_web/components/contact/contact-form.tsx
Normal file
282
surfsense_web/components/contact/contact-form.tsx
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { IconMailFilled } from "@tabler/icons-react";
|
||||
import { useId } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function ContactFormGridWithDetails() {
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 gap-10 px-4 py-10 md:px-6 md:py-20 lg:grid-cols-2">
|
||||
<div className="relative flex flex-col items-center overflow-hidden lg:items-start">
|
||||
<div className="flex items-start justify-start">
|
||||
<FeatureIconContainer className="flex items-center justify-center overflow-hidden">
|
||||
<IconMailFilled className="h-6 w-6 text-blue-500" />
|
||||
</FeatureIconContainer>
|
||||
</div>
|
||||
<h2 className="mt-9 bg-gradient-to-b from-neutral-800 to-neutral-900 bg-clip-text text-left text-xl font-bold text-transparent md:text-3xl lg:text-5xl dark:from-neutral-200 dark:to-neutral-300">
|
||||
Contact us
|
||||
</h2>
|
||||
<p className="mt-8 max-w-lg text-center text-base text-neutral-600 md:text-left dark:text-neutral-400">
|
||||
We are always looking for ways to improve our products and services.
|
||||
Contact us and let us know how we can help you.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 hidden flex-col items-center gap-4 md:flex-row lg:flex">
|
||||
<Link href="mailto:rohan@surfsense.com" className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
rohan@surfsense.com
|
||||
</Link>
|
||||
|
||||
{/* <div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" />
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
+1 (800) 123 XX21
|
||||
</p> */}
|
||||
<div className="h-1 w-1 rounded-full bg-neutral-500 dark:bg-neutral-400" />
|
||||
|
||||
<Link href="https://cal.com/mod-surfsense" className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
https://cal.com/mod-surfsense
|
||||
</Link>
|
||||
</div>
|
||||
<div className="div relative mt-20 flex w-[600px] flex-shrink-0 -translate-x-10 items-center justify-center [perspective:800px] [transform-style:preserve-3d] sm:-translate-x-0 lg:-translate-x-32">
|
||||
<Pin className="h-30 w-85 top-0 left-0" />
|
||||
|
||||
<Image
|
||||
src="/contact/world.svg"
|
||||
width={500}
|
||||
height={500}
|
||||
alt="world map"
|
||||
className="[transform:rotateX(45deg)_translateZ(0px)] dark:invert dark:filter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mx-auto flex w-full max-w-2xl flex-col items-start gap-4 overflow-hidden rounded-3xl bg-gradient-to-b from-gray-100 to-gray-200 p-4 sm:p-10 dark:from-neutral-900 dark:to-neutral-950">
|
||||
<Grid size={20} />
|
||||
<div className="relative z-20 mb-4 w-full">
|
||||
<label
|
||||
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
|
||||
htmlFor="name"
|
||||
>
|
||||
Full name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
className="shadow-input h-10 w-full rounded-md border border-transparent bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-20 mb-4 w-full">
|
||||
<label
|
||||
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="john.doe@example.com"
|
||||
className="shadow-input h-10 w-full rounded-md border border-transparent bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-20 mb-4 w-full">
|
||||
<label
|
||||
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
|
||||
htmlFor="company"
|
||||
>
|
||||
Company
|
||||
</label>
|
||||
<input
|
||||
id="company"
|
||||
type="text"
|
||||
placeholder="Example Inc."
|
||||
className="shadow-input h-10 w-full rounded-md border border-transparent bg-white pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-20 mb-4 w-full">
|
||||
<label
|
||||
className="mb-2 inline-block text-sm font-medium text-neutral-600 dark:text-neutral-300"
|
||||
htmlFor="message"
|
||||
>
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={5}
|
||||
placeholder="Type your message here"
|
||||
className="shadow-input w-full rounded-md border border-transparent bg-white pt-4 pl-4 text-sm text-neutral-700 placeholder-neutral-500 outline-none focus:ring-2 focus:ring-neutral-800 focus:outline-none active:outline-none dark:border-neutral-800 dark:bg-neutral-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<button className="relative z-10 flex items-center justify-center rounded-md border border-transparent bg-neutral-800 px-4 py-2 text-sm font-medium text-white shadow-[0px_1px_0px_0px_#FFFFFF20_inset] transition duration-200 hover:bg-neutral-900 md:text-sm">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Pin = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<motion.div
|
||||
style={{ transform: "translateZ(1px)" }}
|
||||
className={cn(
|
||||
"pointer-events-none absolute z-[60] flex h-40 w-96 items-center justify-center opacity-100 transition duration-500",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="h-full w-full">
|
||||
<div className="absolute inset-x-0 top-0 z-20 mx-auto inline-block w-fit rounded-lg bg-neutral-200 px-2 py-1 text-xs font-normal text-neutral-700 dark:bg-neutral-800 dark:text-white">
|
||||
We are here
|
||||
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-blue-400/0 via-blue-400/90 to-blue-400/0 transition-opacity duration-500"></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
perspective: "800px",
|
||||
transform: "rotateX(70deg) translateZ(0px)",
|
||||
}}
|
||||
className="absolute top-1/2 left-1/2 mt-4 ml-[0.09375rem] -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
}}
|
||||
transition={{ duration: 6, repeat: Infinity, delay: 0 }}
|
||||
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
}}
|
||||
transition={{ duration: 6, repeat: Infinity, delay: 2 }}
|
||||
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0.5, 0],
|
||||
scale: 1,
|
||||
}}
|
||||
transition={{ duration: 6, repeat: Infinity, delay: 4 }}
|
||||
className="absolute top-1/2 left-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-[50%] bg-sky-500/[0.08] shadow-[0_8px_16px_rgb(0_0_0/0.4)] dark:bg-sky-500/[0.2]"
|
||||
></motion.div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500 blur-[2px]" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 h-20 w-px translate-y-[14px] bg-gradient-to-b from-transparent to-blue-500" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[4px] w-[4px] translate-x-[1.5px] translate-y-[14px] rounded-full bg-blue-600 blur-[3px]" />
|
||||
<motion.div className="absolute right-1/2 bottom-1/2 z-40 h-[2px] w-[2px] translate-x-[0.5px] translate-y-[14px] rounded-full bg-blue-300" />
|
||||
</>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeatureIconContainer = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-14 w-14 rounded-md bg-gradient-to-b from-gray-50 to-neutral-200 p-[4px] dark:from-neutral-800 dark:to-neutral-950",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-20 h-full w-full rounded-[5px] bg-gray-50 dark:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="absolute inset-x-0 bottom-0 z-30 mx-auto h-4 w-full rounded-full bg-neutral-600 opacity-50 blur-lg"></div>
|
||||
<div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-500 to-transparent"></div>
|
||||
<div className="absolute inset-x-0 bottom-0 mx-auto h-px w-[60%] bg-gradient-to-r from-transparent via-blue-600 to-transparent dark:h-[8px] dark:blur-sm"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Grid = ({
|
||||
pattern,
|
||||
size,
|
||||
}: {
|
||||
pattern?: number[][];
|
||||
size?: number;
|
||||
}) => {
|
||||
const p = pattern ?? [
|
||||
[9, 3],
|
||||
[8, 5],
|
||||
[10, 2],
|
||||
[7, 4],
|
||||
[9, 6],
|
||||
];
|
||||
return (
|
||||
<div className="pointer-events-none absolute top-0 left-1/2 -mt-2 -ml-20 h-full w-full [mask-image:linear-gradient(white,transparent)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-zinc-900/30 to-zinc-900/30 opacity-10 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 dark:to-zinc-900/30">
|
||||
<GridPattern
|
||||
width={size ?? 20}
|
||||
height={size ?? 20}
|
||||
x="-12"
|
||||
y="4"
|
||||
squares={p}
|
||||
className="absolute inset-0 h-full w-full fill-black/100 stroke-black/100 mix-blend-overlay dark:fill-white/100 dark:stroke-white/100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function GridPattern({ width, height, x, y, squares, ...props }: any) {
|
||||
const patternId = useId();
|
||||
|
||||
return (
|
||||
<svg aria-hidden="true" {...props}>
|
||||
<defs>
|
||||
<pattern
|
||||
id={patternId}
|
||||
width={width}
|
||||
height={height}
|
||||
patternUnits="userSpaceOnUse"
|
||||
x={x}
|
||||
y={y}
|
||||
>
|
||||
<path d={`M.5 ${height}V.5H${width}`} fill="none" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
strokeWidth={0}
|
||||
fill={`url(#${patternId})`}
|
||||
/>
|
||||
{squares && (
|
||||
<svg x={x} y={y} className="overflow-visible">
|
||||
{squares.map(([x, y]: any, idx: number) => (
|
||||
<rect
|
||||
strokeWidth="0"
|
||||
key={`${x}-${y}-${idx}`}
|
||||
width={width + 1}
|
||||
height={height + 1}
|
||||
x={x * width}
|
||||
y={y * height}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { AlertCircle, Bot, Plus, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowRight, Bot, Brain, CheckCircle, Sparkles, Zap } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
|
|
|||
212
surfsense_web/components/pricing.tsx
Normal file
212
surfsense_web/components/pricing.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"use client";
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check, Star } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useRef } from "react";
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
interface PricingPlan {
|
||||
name: string;
|
||||
price: string;
|
||||
yearlyPrice: string;
|
||||
period: string;
|
||||
features: string[];
|
||||
description: string;
|
||||
buttonText: string;
|
||||
href: string;
|
||||
isPopular: boolean;
|
||||
}
|
||||
|
||||
interface PricingProps {
|
||||
plans: PricingPlan[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function Pricing({
|
||||
plans,
|
||||
title = "Simple, Transparent Pricing",
|
||||
description = "Choose the plan that works for you\nAll plans include access to our platform, lead generation tools, and dedicated support.",
|
||||
}: PricingProps) {
|
||||
const [isMonthly, setIsMonthly] = useState(true);
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
const switchRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleToggle = (checked: boolean) => {
|
||||
setIsMonthly(!checked);
|
||||
if (checked && switchRef.current) {
|
||||
const rect = switchRef.current.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
|
||||
confetti({
|
||||
particleCount: 50,
|
||||
spread: 60,
|
||||
origin: {
|
||||
x: x / window.innerWidth,
|
||||
y: y / window.innerHeight,
|
||||
},
|
||||
colors: [
|
||||
"hsl(var(--primary))",
|
||||
"hsl(var(--accent))",
|
||||
"hsl(var(--secondary))",
|
||||
"hsl(var(--muted))",
|
||||
],
|
||||
ticks: 200,
|
||||
gravity: 1.2,
|
||||
decay: 0.94,
|
||||
startVelocity: 30,
|
||||
shapes: ["circle"],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container py-20">
|
||||
<div className="text-center space-y-4 mb-12">
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg whitespace-pre-line">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-10">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<Label>
|
||||
<Switch
|
||||
ref={switchRef as any}
|
||||
checked={!isMonthly}
|
||||
onCheckedChange={handleToggle}
|
||||
className="relative"
|
||||
/>
|
||||
</Label>
|
||||
</label>
|
||||
<span className="ml-2 font-semibold">
|
||||
Annual billing <span className="text-primary">(Save 20%)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
|
||||
{plans.map((plan, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ y: 50, opacity: 1 }}
|
||||
whileInView={
|
||||
isDesktop
|
||||
? {
|
||||
y: plan.isPopular ? -20 : 0,
|
||||
opacity: 1,
|
||||
x: index === 2 ? -30 : index === 0 ? 30 : 0,
|
||||
scale: index === 0 || index === 2 ? 0.94 : 1.0,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
duration: 1.6,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
delay: 0.4,
|
||||
opacity: { duration: 0.5 },
|
||||
}}
|
||||
className={cn(
|
||||
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
|
||||
plan.isPopular ? "border-primary border-2" : "border-border",
|
||||
"flex flex-col",
|
||||
!plan.isPopular && "mt-5",
|
||||
index === 0 || index === 2
|
||||
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
|
||||
: "z-10",
|
||||
index === 0 && "origin-right",
|
||||
index === 2 && "origin-left"
|
||||
)}
|
||||
>
|
||||
{plan.isPopular && (
|
||||
<div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-xl rounded-tr-xl flex items-center">
|
||||
<Star className="text-primary-foreground h-4 w-4 fill-current" />
|
||||
<span className="text-primary-foreground ml-1 font-sans font-semibold">
|
||||
Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<p className="text-base font-semibold text-muted-foreground">
|
||||
{plan.name}
|
||||
</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-x-2">
|
||||
<span className="text-5xl font-bold tracking-tight text-foreground">
|
||||
<NumberFlow
|
||||
value={
|
||||
isMonthly ? Number(plan.price) : Number(plan.yearlyPrice)
|
||||
}
|
||||
format={{
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}}
|
||||
transformTiming={{
|
||||
duration: 500,
|
||||
easing: "ease-out",
|
||||
}}
|
||||
willChange
|
||||
className="font-variant-numeric: tabular-nums"
|
||||
/>
|
||||
</span>
|
||||
{plan.period !== "Next 3 months" && (
|
||||
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
|
||||
/ {plan.period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{isMonthly ? "billed monthly" : "billed annually"}
|
||||
</p>
|
||||
|
||||
<ul className="mt-5 gap-2 flex flex-col">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<Check className="h-4 w-4 text-primary mt-1 flex-shrink-0" />
|
||||
<span className="text-left">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<hr className="w-full my-4" />
|
||||
|
||||
<Link
|
||||
href={plan.href}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter",
|
||||
"transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-primary-foreground",
|
||||
plan.isPopular
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-background text-foreground"
|
||||
)}
|
||||
>
|
||||
{plan.buttonText}
|
||||
</Link>
|
||||
<p className="mt-6 text-xs leading-5 text-muted-foreground">
|
||||
{plan.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
surfsense_web/components/pricing/pricing-section.tsx
Normal file
74
surfsense_web/components/pricing/pricing-section.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"use client";
|
||||
|
||||
import { Pricing } from "@/components/pricing";
|
||||
|
||||
const demoPlans = [
|
||||
{
|
||||
name: "STARTER",
|
||||
price: "50",
|
||||
yearlyPrice: "40",
|
||||
period: "per month",
|
||||
features: [
|
||||
"Up to 10 projects",
|
||||
"Basic analytics",
|
||||
"48-hour support response time",
|
||||
"Limited API access",
|
||||
"Community support",
|
||||
],
|
||||
description: "Perfect for individuals and small projects",
|
||||
buttonText: "Start Free Trial",
|
||||
href: "/sign-up",
|
||||
isPopular: false,
|
||||
},
|
||||
{
|
||||
name: "PROFESSIONAL",
|
||||
price: "99",
|
||||
yearlyPrice: "79",
|
||||
period: "per month",
|
||||
features: [
|
||||
"Unlimited projects",
|
||||
"Advanced analytics",
|
||||
"24-hour support response time",
|
||||
"Full API access",
|
||||
"Priority support",
|
||||
"Team collaboration",
|
||||
"Custom integrations",
|
||||
],
|
||||
description: "Ideal for growing teams and businesses",
|
||||
buttonText: "Get Started",
|
||||
href: "/sign-up",
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
name: "ENTERPRISE",
|
||||
price: "299",
|
||||
yearlyPrice: "239",
|
||||
period: "per month",
|
||||
features: [
|
||||
"Everything in Professional",
|
||||
"Custom solutions",
|
||||
"Dedicated account manager",
|
||||
"1-hour support response time",
|
||||
"SSO Authentication",
|
||||
"Advanced security",
|
||||
"Custom contracts",
|
||||
"SLA agreement",
|
||||
],
|
||||
description: "For large organizations with specific needs",
|
||||
buttonText: "Contact Sales",
|
||||
href: "/contact",
|
||||
isPopular: false,
|
||||
},
|
||||
];
|
||||
|
||||
function PricingBasic() {
|
||||
return (
|
||||
<Pricing
|
||||
plans={demoPlans}
|
||||
title="Simple, Transparent Pricing"
|
||||
description="Choose the plan that works for you"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default PricingBasic;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { motion, type Variants } from "framer-motion";
|
||||
import { motion, type Variants } from "motion/react";
|
||||
import { MoveLeftIcon, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import { motion, type SpringOptions, useSpring, useTransform } from "framer-motion";
|
||||
import { motion, type SpringOptions, useSpring, useTransform } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
|
|||
29
surfsense_web/components/ui/switch.tsx
Normal file
29
surfsense_web/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
useMotionValue,
|
||||
useSpring,
|
||||
useTransform,
|
||||
} from "framer-motion";
|
||||
} from "motion/react";
|
||||
import type React from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue