From 7ffb72189a51b57eae6d562bba76be2e1a7ed1cc Mon Sep 17 00:00:00 2001 From: Nil2000 Date: Sat, 3 Oct 2026 15:04:10 +0530 Subject: [PATCH 1/2] feat: enhance pricing and testimonial features on landing page - Updated pricing plans to use constants from the subscription library for consistency. - Introduced a new LandingFeatures component to showcase the testimonial collection process. - Added LandingPricing component to display subscription options with detailed features. - Refactored HeroSection to integrate new components and improve user flow. - Removed the Contact link from the navigation for a cleaner interface. --- apps/web/app/(protected)/buy-premium/page.tsx | 7 +- apps/web/components/hero-section.tsx | 430 ++++++++---------- apps/web/components/landing-features.tsx | 195 ++++++++ apps/web/components/landing-page-navbarv2.tsx | 1 - apps/web/components/landing-page.tsx | 44 +- apps/web/components/landing-pricing.tsx | 106 +++++ apps/web/lib/subscription.ts | 6 + 7 files changed, 548 insertions(+), 241 deletions(-) create mode 100644 apps/web/components/landing-features.tsx create mode 100644 apps/web/components/landing-pricing.tsx diff --git a/apps/web/app/(protected)/buy-premium/page.tsx b/apps/web/app/(protected)/buy-premium/page.tsx index a517d43..7190ad7 100644 --- a/apps/web/app/(protected)/buy-premium/page.tsx +++ b/apps/web/app/(protected)/buy-premium/page.tsx @@ -30,6 +30,7 @@ import { startUserTrial, getUserPlan } from "@/actions/subscriptionActions"; import { PlanType, PLAN_DISPLAY_NAMES, + PLAN_PRICES, TRIAL_DURATION_DAYS, getPlanFeatureList, } from "@/lib/subscription"; @@ -45,7 +46,7 @@ const pricingPlans: { }[] = [ { id: PlanType.FREE, - price: "Free", + price: PLAN_PRICES[PlanType.FREE], period: "", description: "Perfect for trying out the platform", icon: Sparkles, @@ -53,7 +54,7 @@ const pricingPlans: { }, { id: PlanType.PRO, - price: "$20", + price: PLAN_PRICES[PlanType.PRO], period: "/month", description: "Best for growing businesses and agencies", icon: Zap, @@ -61,7 +62,7 @@ const pricingPlans: { }, { id: PlanType.ENTERPRISE, - price: "$30", + price: PLAN_PRICES[PlanType.ENTERPRISE], period: "/month", description: "For large teams and organizations", icon: Crown, diff --git a/apps/web/components/hero-section.tsx b/apps/web/components/hero-section.tsx index 4e06aa2..ea8912f 100644 --- a/apps/web/components/hero-section.tsx +++ b/apps/web/components/hero-section.tsx @@ -1,276 +1,238 @@ "use client"; -import React from "react"; +import { useState } from "react"; +import Link from "next/link"; +import { LayoutGroup, motion, useReducedMotion } from "motion/react"; +import { ArrowRight, Star, Video } from "lucide-react"; import { Button } from "./ui/button"; -import { ArrowRight, Quote, Star } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { motion } from "motion/react"; -import Image from "next/image"; -import { Marquee } from "./ui/marquee"; +import { Badge } from "./ui/badge"; +import { Switch } from "./ui/switch"; import { cn } from "@/lib/utils"; -type Testimonial = { +type ReviewItem = { + id: string; name: string; - role: string; - avatar: string; quote: string; + kind: "text" | "video"; + stars: number; + sentiment: "positive" | "spam"; }; -const testimonials: Testimonial[] = [ +const REVIEWS: ReviewItem[] = [ { - name: "Maya Rensburg", - role: "Founder, Driftwork", - avatar: - "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&auto=format&fit=crop&q=60", + id: "lena", + name: "Lena", quote: - "We embedded our wall of love on the pricing page and trial sign-ups jumped 31% in two weeks.", + "We sent one link. The replies came back the same day, in their own words.", + kind: "text", + stars: 5, + sentiment: "positive", }, { - name: "Daniel Okafor", - role: "Head of Growth, Lumen", - avatar: - "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&auto=format&fit=crop&q=60", - quote: - "Collecting video testimonials used to take a month of chasing. Now it's a single link.", - }, - { - name: "Priya Nair", - role: "Indie maker", - avatar: - "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&auto=format&fit=crop&q=60", - quote: - "The cleanest way I've found to turn a happy DM into social proof on my landing page.", - }, - { - name: "Theo Lindqvist", - role: "CEO, Northwind", - avatar: - "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200&auto=format&fit=crop&q=60", - quote: - "Beautiful by default. Our testimonials finally look like they belong on the brand.", + id: "omar", + name: "Omar", + quote: "Filmed it between meetings. Took two minutes.", + kind: "video", + stars: 5, + sentiment: "positive", }, { - name: "Aisha Bello", - role: "Marketing lead, Paystack-ish", - avatar: - "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=200&auto=format&fit=crop&q=60", - quote: - "Set up in an afternoon. The import from Twitter alone paid for the whole plan.", - }, - { - name: "Marco Bianchi", - role: "Founder, Cobalt", - avatar: - "https://images.unsplash.com/photo-1633332755192-727a05c4013d?w=200&auto=format&fit=crop&q=60", - quote: - "Real customer love, organized and ready to publish. It's become part of our launch checklist.", + id: "spam", + name: "Chris", + quote: "Buy cheap followers now. Click here.", + kind: "text", + stars: 0, + sentiment: "spam", }, ]; -function TestimonialCard({ name, role, avatar, quote }: Testimonial) { +function Stars({ count }: { count: number }) { return ( -
-
+ ))} + ); } -const ease = [0.2, 0.8, 0.2, 1] as const; - -export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { - const router = useRouter(); - - const handleStart = () => { - router.push(loggedIn ? "/dashboard" : "/api/auth/signin"); - }; - - const firstRow = testimonials.slice(0, 3); - const secondRow = testimonials.slice(3); +function ReviewCard({ + item, + onWall, + onToggle, + reduceMotion, +}: { + item: ReviewItem; + onWall: boolean; + onToggle: (on: boolean) => void; + reduceMotion: boolean; +}) { + const isSpam = item.sentiment === "spam"; return ( -
- {/* Ambient background */} -
-
-
-
+ +
+
+

{item.name}

+

+ {item.quote} +

+ {item.kind === "video" && ( +

+

+ )} +
+ + {isSpam ? "Spam" : "Positive"} +
- -
- {/* Eyebrow / social proof */} - -
- {firstRow.concat(secondRow.slice(0, 1)).map((t) => ( - {t.name} - ))} -
-
- - {Array.from({ length: 5 }).map((_, i) => ( - - Loved by{" "} - 60,000+{" "} - founders -
-
- - {/* Headline */} - - Turn customer love into -
{" "} - - testimonials that sell - +
+ {item.stars > 0 ? : } + {isSpam ? ( + + Archived - + ) : ( + + )} +
+ + ); +} - {/* Subtext */} - - Collect, manage, and showcase testimonials on pages that feel like part - of your brand — and quietly do your selling for you. - +export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { + const reduceMotion = useReducedMotion() === true; + const [onWall, setOnWall] = useState>({ + lena: true, + }); - {/* CTAs */} - - - - - - No credit card required · Set up in minutes - +
+

+ testiflow.app/your-brand +

+
- {/* Signature: wall of love */} - +
-
- - {firstRow.map((t) => ( - - ))} - - - {secondRow.map((t) => ( - - ))} - +

+ Example inbox +

+
+ {inbox.map((item) => ( + + setOnWall((prev) => ({ ...prev, [item.id]: on })) + } + /> + ))}
- {/* Edge fades */} -
-
+

+ Wall of Love +

+ {wall.length === 0 ? ( +

+ Flip Wall on a testimonial to publish it. +

+ ) : ( +
1 && "sm:grid-cols-2", + )} + > + {wall.map((item) => ( + + setOnWall((prev) => ({ ...prev, [item.id]: on })) + } + /> + ))} +
)} - /> - -
+
+
+
); } diff --git a/apps/web/components/landing-features.tsx b/apps/web/components/landing-features.tsx new file mode 100644 index 0000000..4d47c9d --- /dev/null +++ b/apps/web/components/landing-features.tsx @@ -0,0 +1,195 @@ +import { Quote, Star } from "lucide-react"; +import { WALL_OF_LOVE_LAYOUTS } from "@/lib/constants"; + +function layoutPhrase() { + const labels = WALL_OF_LOVE_LAYOUTS.map((layout) => layout.label.toLowerCase()); + return `${labels.slice(0, -1).join(", ")}, or ${labels[labels.length - 1]}`; +} + +const stages = [ + { + step: "1", + title: "Collect", + items: [ + "A public link for each space", + "Text, video, or both", + "Questions you write", + "Star ratings, when you turn them on", + "A theme for the form, and a thank-you page after they send it", + ], + }, + { + step: "2", + title: "Manage", + items: [ + "Every submission in one inbox", + "AI spam detection, on paid plans", + "Sentiment tagged positive, negative, or neutral, on paid plans", + "Video is transcribed so sentiment can use what was said, on paid plans", + "Archive, sort, and edit", + ], + }, + { + step: "3", + title: "Showcase", + items: [ + `Wall of Love in a ${layoutPhrase()}`, + "One testimonial, embedded, with its own border, background, shadow, and type", + "A link you can share for a single testimonial", + "Page views for the collection page and the Wall of Love, on paid plans", + ], + }, +] as const; + +function CollectVisual() { + return ( + + ); +} + +function ManageVisual() { + const rows = [ + { label: "5 stars", tag: "Positive", tone: "bg-secondary text-secondary-foreground" }, + { label: "Video", tag: "Neutral", tone: "border border-border text-foreground" }, + { label: "Archived", tag: "Spam", tone: "bg-destructive text-destructive-foreground" }, + ]; + + return ( + + ); +} + +function ShowcaseVisual() { + return ( + + ); +} + +const visuals = [CollectVisual, ManageVisual, ShowcaseVisual]; + +export default function LandingFeatures() { + return ( +
+
+

+ From a link to a page that shows the proof. +

+

+ A space is the link you send, the inbox you review, and the page you + publish. +

+ +
+ {stages.map((stage, index) => { + const Visual = visuals[index]; + return ( +
+

+ {stage.step} +

+

+ {stage.title} +

+
+ +
+
    + {stage.items.map((item) => ( +
  • +
  • + ))} +
+
+ ); + })} +
+ +
+
+

+ Paste one testimonial where you already sell. +

+

+ Set the border, background, shadow, and type, then copy the embed. +

+
+{``}
+            
+
+
+
+ We sent one link. The replies came back the same day, in their + own words. +
+
+ Lena + +
+
+
+
+
+ ); +} diff --git a/apps/web/components/landing-page-navbarv2.tsx b/apps/web/components/landing-page-navbarv2.tsx index 1922a22..1ede6e6 100644 --- a/apps/web/components/landing-page-navbarv2.tsx +++ b/apps/web/components/landing-page-navbarv2.tsx @@ -30,7 +30,6 @@ export default function LandingPageNavbarV2({ session }: Props) { const navLinks = [ { label: "Features", href: "#features" }, { label: "Pricing", href: "#pricing" }, - { label: "Contact", href: "#contact" }, ]; return ( diff --git a/apps/web/components/landing-page.tsx b/apps/web/components/landing-page.tsx index a1f8b2d..be15f4b 100644 --- a/apps/web/components/landing-page.tsx +++ b/apps/web/components/landing-page.tsx @@ -1,16 +1,54 @@ "use client"; -import React from "react"; -import LandingPageNavbarV2 from "./landing-page-navbarv2"; + +import Link from "next/link"; import { Session } from "next-auth"; +import { ArrowRight } from "lucide-react"; +import LandingPageNavbarV2 from "./landing-page-navbarv2"; import HeroSection from "./hero-section"; +import LandingFeatures from "./landing-features"; +import LandingPricing from "./landing-pricing"; +import { Button } from "./ui/button"; + type Props = { session: Session | null; }; + export default function LandingPage({ session }: Props) { + const loggedIn = !!session; + const startHref = loggedIn ? "/dashboard" : "/api/auth/signin"; + return (
- + + + + +
+
+

+ Open a space and send the link. +

+ +
+
+ +
+
+ TestiFlow + Testimonials, collected and published. +
+
); } diff --git a/apps/web/components/landing-pricing.tsx b/apps/web/components/landing-pricing.tsx new file mode 100644 index 0000000..4d70ba1 --- /dev/null +++ b/apps/web/components/landing-pricing.tsx @@ -0,0 +1,106 @@ +import Link from "next/link"; +import { Check } from "lucide-react"; +import { Button } from "./ui/button"; +import { cn } from "@/lib/utils"; +import { + PlanType, + PLAN_DISPLAY_NAMES, + PLAN_PRICES, + TRIAL_DURATION_DAYS, + getPlanFeatureList, +} from "@/lib/subscription"; + +const PLANS = [PlanType.FREE, PlanType.PRO, PlanType.ENTERPRISE] as const; + +const PLAN_SUMMARY: Record = { + [PlanType.FREE]: "One space, so you can see how collection works.", + [PlanType.PRO]: "More spaces, more video, and AI review.", + [PlanType.ENTERPRISE]: "The same tools, with more room in each space.", +}; + +function planAction(plan: PlanType, loggedIn: boolean) { + if (plan === PlanType.FREE) { + return { + href: loggedIn ? "/dashboard" : "/api/auth/signin", + label: loggedIn ? "Go to dashboard" : "Start collecting free", + }; + } + return { + href: loggedIn ? "/buy-premium" : "/api/auth/signin", + label: `Start ${TRIAL_DURATION_DAYS}-day trial`, + }; +} + +export default function LandingPricing({ loggedIn }: { loggedIn: boolean }) { + return ( +
+
+

+ Start free. Add room when you need it. +

+

+ Paid plans include a {TRIAL_DURATION_DAYS}-day trial. No card on the + free plan. +

+ +
+ {PLANS.map((plan) => { + const popular = plan === PlanType.PRO; + const action = planAction(plan, loggedIn); + const price = PLAN_PRICES[plan]; + + return ( +
+
+

+ {PLAN_DISPLAY_NAMES[plan]} +

+ {popular && ( + + Popular + + )} +
+

+ {PLAN_SUMMARY[plan]} +

+

+ + {price} + + {plan !== PlanType.FREE && ( + /month + )} +

+
    + {getPlanFeatureList(plan).map((feature) => ( +
  • +
  • + ))} +
+ +
+ ); + })} +
+
+
+ ); +} diff --git a/apps/web/lib/subscription.ts b/apps/web/lib/subscription.ts index f1f8191..e36e271 100644 --- a/apps/web/lib/subscription.ts +++ b/apps/web/lib/subscription.ts @@ -59,6 +59,12 @@ export const PLAN_DISPLAY_NAMES: Record = { [PlanType.ENTERPRISE]: "Enterprise", }; +export const PLAN_PRICES: Record = { + [PlanType.FREE]: "Free", + [PlanType.PRO]: "$20", + [PlanType.ENTERPRISE]: "$30", +}; + function formatLimit(n: number, singular: string, plural: string) { if (n === -1) return `Unlimited ${plural}`; return `${n} ${n === 1 ? singular : plural}`; From a7bb6a12feb3f4d71f6ff370c0683d7537be64b1 Mon Sep 17 00:00:00 2001 From: Nil2000 Date: Sat, 3 Oct 2026 16:14:06 +0530 Subject: [PATCH 2/2] feat: enhance landing page with reveal animations and new visuals - Added Reveal component for animated visibility on landing page sections. - Integrated CollectVisual, ManageVisual, and ShowcaseVisual components to improve testimonial presentation. - Updated HeroSection to include smooth scrolling behavior for better user experience. - Refactored LandingFeatures and LandingPricing components to utilize the new Reveal component for enhanced visual appeal. --- apps/web/app/globals.css | 6 + apps/web/components/hero-section.tsx | 172 +++++++++++++++--- apps/web/components/landing-features.tsx | 99 +++------- apps/web/components/landing-page.tsx | 5 +- apps/web/components/landing-pricing.tsx | 28 +-- apps/web/components/landing-stage-visuals.tsx | 138 ++++++++++++++ apps/web/components/reveal.tsx | 43 +++++ 7 files changed, 375 insertions(+), 116 deletions(-) create mode 100644 apps/web/components/landing-stage-visuals.tsx create mode 100644 apps/web/components/reveal.tsx diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 0a2711c..5e9f2a0 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -192,3 +192,9 @@ body { .vjs-no-flex .vjs-duration { display: block !important; } + +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} diff --git a/apps/web/components/hero-section.tsx b/apps/web/components/hero-section.tsx index ea8912f..6725ef6 100644 --- a/apps/web/components/hero-section.tsx +++ b/apps/web/components/hero-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { LayoutGroup, motion, useReducedMotion } from "motion/react"; import { ArrowRight, Star, Video } from "lucide-react"; @@ -9,6 +9,14 @@ import { Badge } from "./ui/badge"; import { Switch } from "./ui/switch"; import { cn } from "@/lib/utils"; +const ease = [0.2, 0.8, 0.2, 1] as const; +const COLLECTION_LINK = "testiflow.app/your-brand"; +const HEADLINE = [ + "Collect testimonials.", + "Keep the good ones.", + "Put them to work.", +]; + type ReviewItem = { id: string; name: string; @@ -46,6 +54,22 @@ const REVIEWS: ReviewItem[] = [ }, ]; +function rise(play: boolean, delay: number) { + return { + initial: false as const, + animate: play ? ("show" as const) : ("rest" as const), + variants: { + rest: { opacity: 1, y: 0 }, + show: { opacity: [0, 1], y: [14, 0] }, + }, + transition: { + duration: play ? 0.55 : 0, + delay: play ? delay : 0, + ease, + }, + }; +} + function Stars({ count }: { count: number }) { return ( @@ -64,28 +88,80 @@ function Stars({ count }: { count: number }) { ); } +function TypedLink({ play }: { play: boolean }) { + const reduce = useReducedMotion() === true; + const [count, setCount] = useState(reduce ? COLLECTION_LINK.length : 0); + + useEffect(() => { + if (reduce || !play || count >= COLLECTION_LINK.length) return; + const delay = count === 0 ? 520 : 26; + const timer = setTimeout(() => setCount((current) => current + 1), delay); + return () => clearTimeout(timer); + }, [play, count, reduce]); + + const shown = reduce ? COLLECTION_LINK : COLLECTION_LINK.slice(0, count); + + return ( +

+ +

+ ); +} + function ReviewCard({ item, onWall, onToggle, reduceMotion, + intro, + pulse, + order, }: { item: ReviewItem; onWall: boolean; onToggle: (on: boolean) => void; reduceMotion: boolean; + intro: boolean; + pulse: boolean; + order: number; }) { const isSpam = item.sentiment === "spam"; + const enter = intro && !reduceMotion; return ( @@ -115,11 +191,22 @@ function ReviewCard({ ) : ( )} @@ -129,10 +216,22 @@ function ReviewCard({ export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { const reduceMotion = useReducedMotion() === true; + const [play, setPlay] = useState(false); + const [intro, setIntro] = useState(true); + const [hint, setHint] = useState(true); const [onWall, setOnWall] = useState>({ lena: true, }); + useEffect(() => { + const start = requestAnimationFrame(() => setPlay(true)); + const timer = setTimeout(() => setIntro(false), 1400); + return () => { + cancelAnimationFrame(start); + clearTimeout(timer); + }; + }, []); + const inbox = REVIEWS.filter( (item) => item.sentiment === "spam" || !onWall[item.id], ); @@ -140,20 +239,42 @@ export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { (item) => item.sentiment !== "spam" && onWall[item.id], ); + const publish = (id: string, on: boolean) => { + setHint(false); + setOnWall((prev) => ({ ...prev, [id]: on })); + }; + return (
-

+ Testimonial management -

+

- Collect testimonials. Keep the good ones. Put them to work. + {HEADLINE.map((line, index) => ( + + {line} + + ))}

-

+ Send one link for text and video. Review what comes in, then publish a Wall of Love or embed a single testimonial on your site. -

-
+ + -
-

- testiflow.app/your-brand -

+ +
-

Example inbox @@ -195,9 +315,10 @@ export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { item={item} onWall={false} reduceMotion={reduceMotion} - onToggle={(on) => - setOnWall((prev) => ({ ...prev, [item.id]: on })) - } + intro={intro} + pulse={hint && !reduceMotion && item.id === "omar"} + order={REVIEWS.findIndex((review) => review.id === item.id)} + onToggle={(on) => publish(item.id, on)} /> ))}

@@ -223,15 +344,16 @@ export default function HeroSection({ loggedIn }: { loggedIn: boolean }) { item={item} onWall reduceMotion={reduceMotion} - onToggle={(on) => - setOnWall((prev) => ({ ...prev, [item.id]: on })) - } + intro={intro} + pulse={false} + order={REVIEWS.findIndex((review) => review.id === item.id)} + onToggle={(on) => publish(item.id, on)} /> ))} )} - +
); diff --git a/apps/web/components/landing-features.tsx b/apps/web/components/landing-features.tsx index 4d47c9d..c9fcc24 100644 --- a/apps/web/components/landing-features.tsx +++ b/apps/web/components/landing-features.tsx @@ -1,5 +1,11 @@ -import { Quote, Star } from "lucide-react"; +import { Star } from "lucide-react"; import { WALL_OF_LOVE_LAYOUTS } from "@/lib/constants"; +import Reveal from "./reveal"; +import { + CollectVisual, + ManageVisual, + ShowcaseVisual, +} from "./landing-stage-visuals"; function layoutPhrase() { const labels = WALL_OF_LOVE_LAYOUTS.map((layout) => layout.label.toLowerCase()); @@ -41,93 +47,29 @@ const stages = [ }, ] as const; -function CollectVisual() { - return ( - - ); -} - -function ManageVisual() { - const rows = [ - { label: "5 stars", tag: "Positive", tone: "bg-secondary text-secondary-foreground" }, - { label: "Video", tag: "Neutral", tone: "border border-border text-foreground" }, - { label: "Archived", tag: "Spam", tone: "bg-destructive text-destructive-foreground" }, - ]; - - return ( - - ); -} - -function ShowcaseVisual() { - return ( - - ); -} - const visuals = [CollectVisual, ManageVisual, ShowcaseVisual]; export default function LandingFeatures() { return (
-

- From a link to a page that shows the proof. -

-

- A space is the link you send, the inbox you review, and the page you - publish. -

+ +

+ From a link to a page that shows the proof. +

+

+ A space is the link you send, the inbox you review, and the page you + publish. +

+
{stages.map((stage, index) => { const Visual = visuals[index]; return ( +

{stage.step} @@ -150,11 +92,13 @@ export default function LandingFeatures() { ))}

+
); })}
-
+ +

Paste one testimonial where you already sell. @@ -189,6 +133,7 @@ export default function LandingFeatures() {

+
); diff --git a/apps/web/components/landing-page.tsx b/apps/web/components/landing-page.tsx index be15f4b..6623ec5 100644 --- a/apps/web/components/landing-page.tsx +++ b/apps/web/components/landing-page.tsx @@ -8,6 +8,7 @@ import HeroSection from "./hero-section"; import LandingFeatures from "./landing-features"; import LandingPricing from "./landing-pricing"; import { Button } from "./ui/button"; +import Reveal from "./reveal"; type Props = { session: Session | null; @@ -25,7 +26,7 @@ export default function LandingPage({ session }: Props) {
-
+

Open a space and send the link.

@@ -40,7 +41,7 @@ export default function LandingPage({ session }: Props) { /> -
+