This commit is contained in:
user
2025-10-30 19:28:30 +02:00
commit bf40976e40
76 changed files with 2440 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { onMount } from "svelte";
import { fly, type FlyParams } from "svelte/transition";
export let options: FlyParams;
let visible = false;
let el;
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
visible = true;
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.2 },
);
observer.observe(el);
return () => observer.disconnect();
});
</script>
<div bind:this={el}>
{#if visible}
<div transition:fly={options}>
<slot />
</div>
{/if}
</div>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
let {
icon: Icon,
cls,
}: {
icon: any;
cls?: string;
} = $props();
</script>
<Icon class={cls} />

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import Label from "$lib/components/ui/label/label.svelte";
import { cn } from "$lib/utils";
let {
label,
labelCls = "pl-2",
children,
}: {
label: string;
labelCls?: string;
children?: any;
} = $props();
</script>
<div class="flex w-full flex-col gap-2">
<Label class={cn(labelCls)}>{label}</Label>
{@render children?.()}
</div>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { cn } from "$lib/utils";
let { cls }: { cls?: string } = $props();
</script>
<img
src="/logo.png"
alt="Easy Save Bills"
class={cn(cls ? cls : "w-auto h-14 object-contain")}
/>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { cn } from "$lib/utils";
let { classNames, id, children }: { classNames?: string; id?: string; children: any } = $props();
let cls = cn("mx-auto h-full w-full max-w-screen-xl", classNames);
</script>
<div class={cls} {id}>
{@render children()}
</div>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { cn } from "$lib/utils";
type TitleSize = "h1" | "h2" | "h3" | "h4" | "h5" | "p";
type TitleFontWeight = "bold" | "semibold" | "normal" | "medium";
const colors = {
theme: "text-green-900 dark:text-green-200",
white: "text-white",
black: "text-black",
primaryLight: "text-green-900",
primaryDark: "text-green-200",
gradientPrimary:
"bg-gradient-to-br from-green-600 to-green-950 dark:from-green-100 dark:to-green-400 inline-block text-transparent bg-clip-text leading-normal sm:pb-1 xl:pb-2",
};
const weights = {
bold: "font-bold",
semibold: "font-semibold",
medium: "font-medium",
normal: "font-normal",
} as const;
const sizes = {
h1: "text-6xl md:text-7xl",
h2: "text-5xl lg:text-6xl",
h3: "text-4xl lg:text-5xl",
h4: "text-2xl lg:text-3xl",
h5: "text-xl lg:text-2xl",
p: "text-lg lg:text-xl",
};
let {
size = "h2",
weight = "medium",
capitalize = true,
color = "black",
center = false,
id = undefined,
children,
}: {
size?: TitleSize;
weight?: TitleFontWeight;
capitalize?: boolean;
color?: keyof typeof colors;
center?: boolean;
id?: string;
children?: any;
} = $props();
</script>
<svelte:element
this={size}
class={cn(
sizes[size] || sizes.p,
weights[weight ?? "bold"],
capitalize && "capitalize",
colors[color],
center && "text-center",
)}
{id}
>
{@render children?.()}
</svelte:element>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { COMPANY_NAME, navLinks } from "$lib/core/constants";
import Logo from "../atoms/logo.svelte";
</script>
<footer
class="bg-gradient-to-b from-green-50 to-green-100 border-t-2 border-green-400"
>
<div class="w-full max-w-screen-xl mx-auto p-4 md:py-12">
<div
class="flex flex-col sm:flex-row gap-8 sm:items-center sm:justify-between"
>
<a
href="/"
class="flex items-center mb-4 sm:mb-0 space-x-3 rtl:space-x-reverse"
>
<Logo />
</a>
<ul
class="flex flex-wrap items-center mb-6 text-sm font-medium text-gray-500 sm:mb-0 dark:text-gray-400"
>
{#each [...navLinks, { name: "Legal", href: "/legal" }] as link}
<li>
<a
href={link.href}
class="hover:underline me-4 md:me-6"
>
{link.name}
</a>
</li>
{/each}
</ul>
</div>
<hr
class="my-6 border-green-400 sm:mx-auto dark:border-gray-700 lg:my-8"
/>
<span
class="block text-sm text-gray-500 sm:text-center dark:text-gray-400"
>
© 2025
<a href="/" class="hover:underline">
{COMPANY_NAME}
</a> All Rights Reserved.
</span>
</div>
</footer>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import { cn } from "$lib/utils";
export let width = 40;
export let height = 40;
export let x = -1;
export let y = -1;
export let strokeDashArray: string = "";
export let squares: Array<[x: number, y: number]> = [[0, 0]];
let className: any = "";
export { className as class };
let id = crypto.randomUUID().toString().slice(0, 8);
export let fillColor = "rgb(156 163 175 / 0.3)";
// : rgb(156 163 175 / 0.3)
export let strokeWidth = 1;
</script>
<svg
aria-hidden="true"
class={cn("pointer-events-none absolute inset-0 h-full w-full", className)}
{...$$restProps}
stroke={fillColor}
stroke-width={strokeWidth}
>
<defs>
<pattern {id} {width} {height} patternUnits="userSpaceOnUse" {x} {y}>
<path
d="M.5 {height}V.5H{width}"
fill="none"
stroke-dasharray={strokeDashArray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" stroke-width={0} fill="url(#{id})" />
{#if squares}
<svg {x} {y} class="overflow-visible">
{#each squares as sq}
<rect
stroke={fillColor}
fill="none"
stroke-width="0"
width={width - 1}
height={height - 1}
x={sq[0] * width + 1}
y={sq[1] * height + 1}
/>
{/each}
</svg>
{/if}
</svg>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import Logo from "../atoms/logo.svelte";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import * as Sheet from "$lib/components/ui/sheet";
import Icon from "../atoms/icon.svelte";
import MenuIcon from "~icons/solar/hamburger-menu-broken";
import { cn } from "$lib/utils";
import { navLinks, TRANSITION_COLORS } from "$lib/core/constants";
import Button from "../ui/button/button.svelte";
</script>
<nav
class="bg-transparent absolute top-0 w-screen grid place-items-center left-0 z-10"
>
<div
class="flex w-full max-w-7xl flex-wrap items-center justify-between p-4 py-6 md:py-8 bg-transparent"
>
<a href="/">
<Logo />
</a>
<Sheet.Root>
<Sheet.Trigger
class={cn(
"block md:hidden",
buttonVariants({ variant: "outline", size: "icon" }),
)}
>
<Icon icon={MenuIcon} cls="w-auto h-12" />
</Sheet.Trigger>
<Sheet.Content side="bottom">
<div class="flex flex-col gap-4 p-8">
{#each navLinks as link}
<a
href={link.href}
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-green-700 md:p-0 dark:text-white md:dark:hover:text-green-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
>
{link.name}
</a>
{/each}
</div>
</Sheet.Content>
</Sheet.Root>
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
<ul class="flex items-center gap-4">
{#each navLinks as link}
{#if link.name !== "Contact"}
<li>
<a
href={link.href}
class={cn(
"py-2 px-3 rounded-md hover:text-green-600 active:text-green-700",
TRANSITION_COLORS,
)}
aria-current="page"
>
{link.name}
</a>
</li>
{/if}
{/each}
<Button href="/#contact">Get In Touch</Button>
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import StarIcon from "~icons/solar/star-bold";
import Icon from "../atoms/icon.svelte";
let {
testimonial,
}: {
testimonial: {
image: string;
name: string;
review: string;
rating: number;
};
} = $props();
// Create an array of length equal to rating for filled stars
const filledStars = Array(testimonial.rating).fill(true);
// Create an array of length equal to remaining stars (5 - rating) for empty stars
const emptyStars = Array(5 - testimonial.rating).fill(false);
// Combine both arrays
const stars = [...filledStars, ...emptyStars];
</script>
<div class="mb-8 sm:break-inside-avoid">
<blockquote class="rounded-lg bg-gray-50 p-6 shadow-sm sm:p-8">
<div class="flex items-center gap-4">
<img
alt={testimonial.name}
src={testimonial.image}
class="size-14 rounded-full object-cover"
/>
<div>
<div class="flex justify-center gap-0.5 text-green-500">
{#each stars as isFilled}
<Icon
icon={StarIcon}
cls="w-auto h-8 {isFilled
? 'text-amber-500'
: 'text-gray-300'}"
/>
{/each}
</div>
<p class="mt-0.5 text-lg font-medium text-gray-900">
{testimonial.name}
</p>
</div>
</div>
<p class="mt-4 text-gray-700">
{testimonial.review}
</p>
</blockquote>
</div>

View File

@@ -0,0 +1,152 @@
<script lang="ts">
import MaxWidthWrapper from "$lib/components/atoms/max-width-wrapper.svelte";
import Title from "$lib/components/atoms/title.svelte";
import IconVerifyOutline from "~icons/bitcoin-icons/verify-outline";
import IconCheckCircle from "~icons/solar/check-circle-broken";
import AnimateWrapper from "$lib/components/atoms/animate-wrapper.svelte";
import { COMPANY_NAME } from "$lib/core/constants";
import Badge from "../ui/badge/badge.svelte";
</script>
<MaxWidthWrapper
id="about-us"
classNames="flex flex-col gap-8 py-32 md:py-80 px-4 md:px-8"
>
<div class="flex flex-col gap-8 md:grid md:grid-cols-6 md:gap-20">
<div class="relative col-span-3 h-full w-full">
<img
src="/images/laptop.jpg"
alt=""
class="aspect-square h-full w-full rounded-lg object-cover"
/>
</div>
<div class="col-span-3 flex flex-col gap-4">
<AnimateWrapper options={{ y: 100, opacity: 0, duration: 600 }}>
<Badge variant="outline">About Us</Badge>
</AnimateWrapper>
<AnimateWrapper options={{ y: 150, duration: 500 }}>
<Title size="h3" weight="medium" color="gradientPrimary">
Helping You Save on Your Internet Bills
</Title>
</AnimateWrapper>
<AnimateWrapper options={{ y: 120, opacity: 0, duration: 800 }}>
<p>
At {COMPANY_NAME}, we're dedicated to helping you reduce your internet
expenses. Our team of experts works tirelessly to find and secure the
best possible discounts on your internet services. We believe everyone
deserves access to affordable internet without compromising on quality
or speed.
</p>
</AnimateWrapper>
<AnimateWrapper options={{ y: 150, duration: 900 }}>
<div class="flex items-start gap-2">
<div>
<IconCheckCircle class="h-8 w-8 text-green-600" />
</div>
<div class="flex flex-col gap-2">
<Title size="h5">Bill Saving Specialists</Title>
<p>
Our team are experts in negotiating with internet service
providers. We understand the industry inside and out, and we use
this knowledge to secure the best possible rates for our clients.
We've helped thousands of customers significantly reduce their
monthly internet bills.
</p>
</div>
</div>
</AnimateWrapper>
<AnimateWrapper options={{ y: 150, duration: 900 }}>
<div class="flex w-full items-start gap-2">
<div>
<IconCheckCircle class="h-8 w-8 text-green-600" />
</div>
<div class="flex flex-col gap-2">
<Title size="h5">Customer-First Approach</Title>
<p>
We prioritize your needs and budget, working diligently to find
the best internet service deals available in your area. Our
platform makes it easy to compare options and choose the plan that
works best for you, whether you're a casual user or need
high-speed business internet.
</p>
</div>
</div>
</AnimateWrapper>
</div>
</div>
<img
src="/images/office-conference.jpg"
alt=""
class="h-80 w-full rounded-lg object-cover object-right"
/>
<div class="flex flex-col gap-8 md:flex-row md:gap-20">
<div class="flex flex-col gap-8">
<AnimateWrapper options={{ y: 100, duration: 700 }}>
<span
class="h-max w-max rounded-full border-2 border-green-950 bg-green-50/20 p-1 px-6 text-green-950"
>
Our Mission
</span>
</AnimateWrapper>
<AnimateWrapper options={{ y: 100, duration: 850 }}>
<Title size="h3" weight="medium" color="black">
To Make Internet Service Affordable for Everyone
</Title>
</AnimateWrapper>
</div>
<div class="relative flex w-full flex-col gap-8">
<div
class="flex flex-col gap-8 md:absolute md:-top-48 md:right-0 xl:mr-20"
>
<AnimateWrapper options={{ y: 100, opacity: 0, duration: 1000 }}>
<div
class="flex flex-col gap-4 rounded-xl bg-green-950 p-6 text-white drop-shadow-xl md:p-8 lg:p-12"
>
<span
class="h-max w-max rounded-full border-2 border-green-100 bg-white/20 p-1 px-6 text-green-100"
>
Best in class
</span>
<Title color="white" size="h4" weight="medium">
Your Partner in Reducing Internet Costs
</Title>
<div class="flex w-full flex-col gap-4 text-green-50">
<div class="flex items-center gap-1">
<IconVerifyOutline class="h-6 w-auto" />
<p>Proven Savings Record</p>
</div>
<div class="flex items-center gap-1">
<IconVerifyOutline class="h-6 w-auto" />
<p>Expert Negotiation Skills</p>
</div>
<div class="flex items-center gap-1">
<IconVerifyOutline class="h-6 w-auto" />
<p>Personalized Service Plans</p>
</div>
</div>
</div>
</AnimateWrapper>
<AnimateWrapper options={{ y: 100, opacity: 0, duration: 1200 }}>
<p>
At {COMPANY_NAME}, we're committed to making internet services more
affordable for everyone. We understand that internet access is
essential in today's world, and high bills shouldn't stand in the
way. Our mission is to leverage our industry relationships and
negotiation expertise to secure the best possible rates for our
clients. Whether you're a homeowner, renter, or business owner,
we're here to help you save on your internet bills while maintaining
the service quality you need.
</p>
</AnimateWrapper>
</div>
</div>
</div>
</MaxWidthWrapper>

View File

@@ -0,0 +1,127 @@
<script lang="ts">
import { CONTACT_INFO } from "$lib/core/constants";
import { Mail, MapPin, Phone } from "lucide-svelte";
import Title from "../atoms/title.svelte";
import LabelWrapper from "../atoms/label-wrapper.svelte";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import Button from "../ui/button/button.svelte";
import Icon from "../atoms/icon.svelte";
import ArrowRightUpIcon from "~icons/solar/arrow-right-up-broken";
const contactMethodsInfo = [
{
title: "Talk with us",
description:
"Convinced? Want to finally get that discount? Or have any other question? Contact us and we will get back to you as soon as possible.",
icon: Mail,
label: CONTACT_INFO.email,
href: `mailto:${CONTACT_INFO.email}`,
},
{
title: "Visit us",
description:
"You can drop by our office at the usual office hours, and we will be happy to assist you.",
icon: MapPin,
label: CONTACT_INFO.address,
href: "#",
},
{
title: "Call us",
description:
"We're available Mon-Fri, 9am-5pm. If you have any questions, feel free to call us.",
icon: Phone,
label: CONTACT_INFO.phone,
href: `tel:${CONTACT_INFO.phone}`,
},
];
function handleSubmit(
e: SubmitEvent & {
currentTarget: EventTarget & HTMLFormElement;
},
) {
e.preventDefault();
const data = new FormData(e.currentTarget);
console.log(data);
}
</script>
<section class="py-32 w-full grid place-items-center" id="contact">
<div
class="flex flex-col gap-20 items-center justify-center w-full max-w-7xl"
>
<div class="flex flex-col gap-2">
<Title center size="h3" weight="semibold">Talk with us</Title>
<p class="text-lg text-muted-foreground max-w-prose text-center">
Convinced? Want to finally get that discount? Or have any other
question? Contact us and we will get back to you as soon as possible.
</p>
</div>
<form
method="post"
action="/"
onsubmit={handleSubmit}
class="h-max flex flex-col gap-4 w-full max-w-3xl p-4 md:p-8"
>
<div class="flex flex-col gap-4 md:flex-row w-full">
<LabelWrapper label="First Name">
<Input type="text" id="first-name" name="first-name" required />
</LabelWrapper>
<LabelWrapper label="Last Name">
<Input type="text" id="last-name" name="last-name" required />
</LabelWrapper>
</div>
<LabelWrapper label="Email">
<Input type="email" id="email" name="email" required />
</LabelWrapper>
<LabelWrapper label="Phone Number">
<Input type="tel" id="phone" name="phone" required />
</LabelWrapper>
<LabelWrapper label="Message">
<Textarea id="message" name="message" required />
</LabelWrapper>
<Button type="submit" size="lg" class="w-full">Send Message</Button>
</form>
<div class="grid gap-10 md:grid-cols-3 w-full">
{#each contactMethodsInfo as { title, description, icon, label, href }}
<div
class="p-4 lg:p-8 rounded-lg bg-white justify-between drop-shadow-lg flex flex-col gap-4"
>
<div class="w-full flex flex-col gap-2">
<span
class="mb-3 flex w-14 h-14 flex-col items-center justify-center rounded-full bg-accent"
>
<svelte:component this={icon} class="h-6 w-auto" />
</span>
<Title size="h5">
{title}
</Title>
<p class="mb-3 text-muted-foreground">
{description}
</p>
</div>
<div class="flex items-center gap-2 w-full">
{#if href !== "#"}
<Icon
icon={ArrowRightUpIcon}
cls="w-auto h-6 font-semibold hover:underline text-black"
/>
{/if}
<a {href} class="font-semibold hover:underline text-black">
{label}
</a>
</div>
</div>
{/each}
</div>
</div>
</section>

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import * as Accordion from "$lib/components/ui/accordion/index.js";
import { TRANSITION_COLORS } from "$lib/core/constants";
import { cn } from "$lib/utils";
import Title from "../atoms/title.svelte";
const questions = [
{
question: "How does your bill discount service work?",
answer:
"We analyze your current internet bill and service plan, then negotiate with service providers on your behalf to secure better rates. We leverage our industry relationships and bulk negotiating power to get you the best possible discounts while maintaining your service quality.",
},
{
question: "How much can I typically save on my internet bill?",
answer:
"On average, most of our clients save between 12-41% on their monthly internet bills. The exact amount depends on your current plan, location, and available promotions, but we guarantee we'll find you the best possible rate.",
},
{
question: "Do I need to switch internet providers to save money?",
answer:
"Not necessarily. We often negotiate better rates with your current provider. However, if switching providers would result in significant savings, we'll present that option to you along with a complete cost-benefit analysis.",
},
{
question: "How long does the process take?",
answer:
"Most customers start seeing savings within 5-7 business days of signing up with our service. The exact timeline can vary depending on your provider and specific situation, but we work quickly to secure your discounts.",
},
{
question: "Are there any upfront fees for your service?",
answer:
"We operate on a success-based model - you only pay after we've successfully reduced your bill. Our fee is a percentage of what we save you, ensuring our interests are aligned with yours.",
},
{
question: "What happens if you can't reduce my bill?",
answer:
"If we're unable to secure any savings on your internet bill, you won't owe us anything. We're confident in our ability to find savings, which is why we offer this guarantee. If we can't save you money, our service is completely free.",
},
];
</script>
<section class="py-32 w-full grid place-items-center px-4 md:px-8">
<div class="flex flex-col gap-8 items-center justify-center w-full max-w-7xl">
<Title center size="h3" weight="semibold">Frequently Asked Questions</Title>
<Accordion.Root type="single" class="w-full flex flex-col gap-4">
{#each questions as question}
<Accordion.Item
value={question.question}
class={cn(
"shadow-sm rounded-lg border border-green-200 p-4 bg-green-50 hover:bg-green-100 active:bg-green-200 hover:shadow-md active:shadow-md",
TRANSITION_COLORS,
)}
>
<Accordion.Trigger
class="text-xl font-medium [&[data-state=open]]:text-green-800"
>
{question.question}
</Accordion.Trigger>
<Accordion.Content class="text-lg">
{question.answer}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
</div>
</section>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import {
BarChartHorizontal,
BatteryCharging,
CircleHelp,
Layers,
WandSparkles,
ZoomIn,
} from "lucide-svelte";
import Title from "../atoms/title.svelte";
import Icon from "../atoms/icon.svelte";
const reasons = [
{
title: "Expert Analysis",
description:
"Our team thoroughly analyzes your current internet bills and service plans to identify all possible savings opportunities and discounts you qualify for.",
icon: ZoomIn,
},
{
title: "Proven Track Record",
description:
"With years of experience and thousands of satisfied customers, we've consistently helped people save 20-40% on their internet bills through our negotiation services.",
icon: BarChartHorizontal,
},
{
title: "24/7 Support",
description:
"Our dedicated support team is always available to answer your questions, handle service issues, and ensure you're getting the most value from your internet plan.",
icon: CircleHelp,
},
{
title: "Smart Solutions",
description:
"We leverage cutting-edge technology and industry relationships to find innovative ways to reduce your bills while maintaining or improving your service quality.",
icon: WandSparkles,
},
{
title: "Multiple Options",
description:
"We work with various service providers and plans, presenting you with multiple cost-saving options tailored to your specific needs and usage patterns.",
icon: Layers,
},
{
title: "Fast Process",
description:
"Our streamlined process means you can start saving quickly. Most customers see results within days of signing up with our service.",
icon: BatteryCharging,
},
];
</script>
<section class="py-32 grid place-items-center px-4 md:px-8" id="services">
<div class="flex items-center justify-center flex-col gap-8 w-full max-w-7xl">
<div class="text-center flex flex-col gap-4 max-w-prose">
<Title size="h3" weight="semibold">Why work with us?</Title>
<p
class="font-light text-gray-500 lg:mb-16 sm:text-xl dark:text-gray-400"
>
Explore the whole collection of open-source web components and elements
built with the utility classes from Tailwind
</p>
</div>
<div class="grid gap-10 md:grid-cols-2 lg:grid-cols-3">
{#each reasons as reason, i}
<div
class="flex flex-col p-4 md:p-8 rounded-lg bg-white drop-shadow-xl gap-4"
>
<div
class="flex w-16 h-16 items-center justify-center rounded-full bg-accent"
>
<Icon icon={reason.icon} cls="w-auto h-8" />
</div>
<Title size="h5">
{reason.title}
</Title>
<p class="text-muted-foreground">{reason.description}</p>
</div>
{/each}
</div>
</div>
</section>

View File

@@ -0,0 +1,88 @@
<script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte";
import StarIcon from "~icons/solar/star-bold";
import Button from "$lib/components/ui/button/button.svelte";
import Title from "$lib/components/atoms/title.svelte";
import GridPattern from "$lib/components/molecules/grid-pattern.svelte";
import Badge from "../ui/badge/badge.svelte";
</script>
<section class="relative py-32 md:py-80 px-4 md:px-8">
<GridPattern
width={32}
height={32}
strokeDashArray="4 2"
fillColor="rgba(21, 128, 61, 0.3)"
class={"[mask-image:radial-gradient(50vw_circle_at_center,white,transparent)] z-[-1]"}
/>
<div class="text-center flex flex-col gap-8 w-full">
<div class="mx-auto flex max-w-screen-lg items-center flex-col gap-6">
<Badge variant="outlineTinted" size="lg" class="w-fit p-2 px-4">
Discounts, simplified
</Badge>
<Title size="h2" weight="semibold">No more headaches with billing</Title>
<p class="text-balance text-muted-foreground lg:text-lg">
A customer should not have to worry about billing. Our service is here
to help you save money on your monthly bills.
</p>
</div>
<div
class="flex flex-col w-full sm:flex-row gap-4 items-center justify-center"
>
<Button class="w-full sm:w-max" href="/#contact" size="lg">
Talk With Us
</Button>
<Button
class="w-full sm:w-max"
variant="ghost"
size="lg"
href="/#services"
>
Explore Our Services
</Button>
</div>
<div
class="mx-auto mt-10 flex w-fit flex-col items-center gap-4 sm:flex-row"
>
<span class="mx-4 inline-flex items-center -space-x-4">
<img
class="w-auto h-12 object-contain rounded-full border"
src="https://shadcnblocks.com/images/block/avatar-1.webp"
alt="placeholder"
/>
<img
class="w-auto h-12 object-contain rounded-full border"
src="https://shadcnblocks.com/images/block/avatar-2.webp"
alt="placeholder"
/>
<img
class="w-auto h-12 object-contain rounded-full border"
src="https://shadcnblocks.com/images/block/avatar-3.webp"
alt="placeholder"
/>
<img
class="w-auto h-12 object-contain rounded-full border"
src="https://shadcnblocks.com/images/block/avatar-4.webp"
alt="placeholder"
/>
<img
class="w-auto h-12 object-contain rounded-full border"
src="https://shadcnblocks.com/images/block/avatar-5.webp"
alt="placeholder"
/>
</span>
<div>
<div class="flex items-center gap-1">
<Icon icon={StarIcon} cls="w-auto h-6 text-yellow-400" />
<span class="font-semibold">4.8</span>
</div>
<p class="text-left font-medium text-muted-foreground">
from 200+ customers.
</p>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,106 @@
<script lang="ts">
import Title from "../atoms/title.svelte";
import StarIcon from "~icons/solar/star-bold";
const testimonials = [
{
name: "John Doe",
rating: 4,
image: "https://randomuser.me/api/portraits/men/1.jpg",
review:
"I was skeptical at first, but they delivered beyond my expectations. My bills are lower, and the process was seamless. Highly recommend their services!",
},
{
name: "Sophia Davis",
rating: 5,
image: "https://randomuser.me/api/portraits/women/3.jpg",
review:
"Amazing company! They handled everything professionally and saved me more than I expected. Ive already recommended them to my friends and family.",
},
{
name: "Jane Doe",
rating: 5,
image: "https://randomuser.me/api/portraits/women/1.jpg",
review:
"Fantastic service! They saved me a significant amount on my bills, and I didnt have to lift a finger. Truly professional and efficient.",
},
{
name: "Michael Smith",
rating: 5,
image: "https://randomuser.me/api/portraits/men/2.jpg",
review:
"Ive tried other services before, but nothing comes close to the results I got here. My monthly expenses are finally manageable thanks to them!",
},
{
name: "Emily Johnson",
rating: 4,
image: "https://randomuser.me/api/portraits/women/2.jpg",
review:
"Great experience overall. While it took a bit longer than I expected, the savings on my bills made it worth the wait. Will use them again!",
},
{
name: "Robert Brown",
rating: 3,
image: "https://randomuser.me/api/portraits/men/3.jpg",
review:
"They managed to save me some money, but I feel the process could be a bit faster. Still, a decent service if youre looking to cut costs.",
},
];
function getStars(rating: number) {
return Array(5).fill(null);
}
</script>
<section class="grid place-items-center w-full px-4 md:px-8">
<div
class="py-8 w-full max-w-7xl text-center lg:py-20 items-center flex flex-col"
>
<div class="w-full max-w-prose">
<Title size="h3" weight="semibold">What Our Customers Say</Title>
<p
class="mb-8 font-light text-gray-500 lg:mb-16 sm:text-xl dark:text-gray-400"
>
Don't just take our word for it - hear from some of our satisfied
customers who have successfully reduced their internet bills.
</p>
</div>
<div class="grid mb-8 lg:mb-12 lg:grid-cols-2 w-full gap-4">
{#each testimonials as testimonial}
<figure
class="flex flex-col justify-center items-center p-8 text-center bg-white shadow-lg border border-gray-200 rounded-lg md:p-12"
>
<blockquote
class="mx-auto mb-8 w-full text-gray-500 dark:text-gray-400"
>
<div class="flex justify-center items-center space-x-1 mb-2">
{#each getStars(testimonial.rating) as _, index}
<StarIcon
class={index < testimonial.rating
? "text-yellow-400"
: "text-gray-300 dark:text-gray-600"}
/>
{/each}
</div>
<Title size="p">
"{testimonial.review}"
</Title>
</blockquote>
<figcaption class="flex justify-center items-center space-x-3">
<img
class="w-9 h-9 rounded-full"
src={testimonial.image}
alt={`Profile picture of ${testimonial.name}`}
/>
<div class="space-y-0.5 font-medium dark:text-white text-left">
<div>{testimonial.name}</div>
<div class="text-xs font-light text-gray-500 dark:text-gray-400">
Verified Customer
</div>
</div>
</figcaption>
</figure>
{/each}
</div>
</div>
</section>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
class={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all",
className
)}
{...restProps}
>
<div class="pb-4 pt-0">
{@render children?.()}
</div>
</AccordionPrimitive.Content>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item bind:ref class={cn("border-b", className)} {...restProps} />

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
bind:ref
class={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDown class="size-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>

View File

@@ -0,0 +1,17 @@
import { Accordion as AccordionPrimitive } from "bits-ui";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
const Root = AccordionPrimitive.Root;
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
class={cn("aspect-square h-full w-full", className)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>

View File

@@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

View File

@@ -0,0 +1,60 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus:ring-ring inline-flex items-center rounded-full border font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "border-primary/60 text-green-900",
outlineTinted: "text-green-800 bg-green-600/15 border-primary/60",
},
size: {
sm: "px-3 py-1 text-sm",
default: "px-5 py-1.5 text-base tracking-wide",
lg: "px-8 py-2 text-lg tracking-wider",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
export type BadgeSize = VariantProps<typeof badgeVariants>["size"];
</script>
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
size = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
size?: BadgeSize;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant, size }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View File

@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View File

@@ -0,0 +1,75 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "px-5 py-3",
sm: "px-3 py-2",
lg: "px-8 py-4 text-base",
icon: "h-12 w-12",
iconSm: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View File

@@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
</script>
<input
bind:this={ref}
class={cn(
"border-input/40 bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex w-full rounded-md border px-4 py-3 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
bind:value
{...restProps}
/>

View File

@@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,36 @@
import { Dialog as SheetPrimitive } from "bits-ui";
import Overlay from "./sheet-overlay.svelte";
import Content from "./sheet-content.svelte";
import Header from "./sheet-header.svelte";
import Footer from "./sheet-footer.svelte";
import Title from "./sheet-title.svelte";
import Description from "./sheet-description.svelte";
const Root = SheetPrimitive.Root;
const Close = SheetPrimitive.Close;
const Trigger = SheetPrimitive.Trigger;
const Portal = SheetPrimitive.Portal;
export {
Root,
Close,
Trigger,
Portal,
Overlay,
Content,
Header,
Footer,
Title,
Description,
//
Root as Sheet,
Close as SheetClose,
Trigger as SheetTrigger,
Portal as SheetPortal,
Overlay as SheetOverlay,
Content as SheetContent,
Header as SheetHeader,
Footer as SheetFooter,
Title as SheetTitle,
Description as SheetDescription,
};

View File

@@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const sheetVariants = tv({
base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
variants: {
side: {
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
});
export type Side = VariantProps<typeof sheetVariants>["side"];
</script>
<script lang="ts">
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import SheetOverlay from "./sheet-overlay.svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
side = "right",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: SheetPrimitive.PortalProps;
side?: Side;
children: Snippet;
} = $props();
</script>
<SheetPrimitive.Portal {...portalProps}>
<SheetOverlay />
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
{@render children?.()}
<SheetPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPrimitive.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.DescriptionProps = $props();
</script>
<SheetPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.OverlayProps = $props();
export { className as class };
</script>
<SheetPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.TitleProps = $props();
</script>
<SheetPrimitive.Title
bind:ref
class={cn("text-foreground text-lg font-semibold", className)}
{...restProps}
/>

View File

@@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import type { WithElementRef, WithoutChildren } from "bits-ui";
import type { HTMLTextareaAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
</script>
<textarea
bind:this={ref}
class={cn(
"border-input/40 bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-32 w-full rounded-md border px-3 py-3 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
bind:value
{...restProps}
></textarea>