oh yeah
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM oven/bun:1.1.38
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lockb ./
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Bill Discount Services
|
||||||
|
|
||||||
|
Deployed to cloudflare pages - via static build
|
||||||
|
|
||||||
|
---
|
||||||
17
components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://next.shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "billdiscountservices",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "bun run ./build/index",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify/json": "^2.2.292",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.9",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "^1.0.0-next.77",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-svelte": "^0.469.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwind-variants": "^0.3.0",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"unplugin-icons": "^0.22.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
81
src/app.css
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Outfit";
|
||||||
|
src: url("/font/outfit-variable.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 142 4% 99%;
|
||||||
|
--foreground: 142 4% 10%;
|
||||||
|
--card: 142 4% 99%;
|
||||||
|
--card-foreground: 142 4% 15%;
|
||||||
|
--popover: 142 4% 99%;
|
||||||
|
--popover-foreground: 142 95% 10%;
|
||||||
|
--primary: 142 76.2% 36.3%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 142 10% 90%;
|
||||||
|
--secondary-foreground: 0 0% 0%;
|
||||||
|
--muted: 104 10% 95%;
|
||||||
|
--muted-foreground: 142 4% 40%;
|
||||||
|
--accent: 104 10% 90%;
|
||||||
|
--accent-foreground: 142 4% 15%;
|
||||||
|
--destructive: 0 50% 50%;
|
||||||
|
--destructive-foreground: 142 4% 99%;
|
||||||
|
--border: 142 20% 82%;
|
||||||
|
--input: 142 20% 50%;
|
||||||
|
--ring: 142 76.2% 36.3%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 142 10% 10%;
|
||||||
|
--foreground: 142 4% 99%;
|
||||||
|
--card: 142 4% 10%;
|
||||||
|
--card-foreground: 142 4% 99%;
|
||||||
|
--popover: 142 10% 5%;
|
||||||
|
--popover-foreground: 142 4% 99%;
|
||||||
|
--primary: 142 76.2% 36.3%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 142 10% 20%;
|
||||||
|
--secondary-foreground: 0 0% 100%;
|
||||||
|
--muted: 104 10% 25%;
|
||||||
|
--muted-foreground: 142 4% 65%;
|
||||||
|
--accent: 104 10% 25%;
|
||||||
|
--accent-foreground: 142 4% 95%;
|
||||||
|
--destructive: 0 50% 50%;
|
||||||
|
--destructive-foreground: 142 4% 99%;
|
||||||
|
--border: 142 20% 50%;
|
||||||
|
--input: 142 20% 50%;
|
||||||
|
--ring: 142 76.2% 36.3%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
font-family:
|
||||||
|
"Outfit",
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
"Helvetica Neue",
|
||||||
|
Arial,
|
||||||
|
"Noto Sans",
|
||||||
|
sans-serif,
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol",
|
||||||
|
"Noto Color Emoji";
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import "unplugin-icons/types/svelte";
|
||||||
|
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
src/lib/components/atoms/animate-wrapper.svelte
Normal 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>
|
||||||
11
src/lib/components/atoms/icon.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
icon: Icon,
|
||||||
|
cls,
|
||||||
|
}: {
|
||||||
|
icon: any;
|
||||||
|
cls?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon class={cls} />
|
||||||
19
src/lib/components/atoms/label-wrapper.svelte
Normal 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>
|
||||||
11
src/lib/components/atoms/logo.svelte
Normal 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")}
|
||||||
|
/>
|
||||||
11
src/lib/components/atoms/max-width-wrapper.svelte
Normal 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>
|
||||||
64
src/lib/components/atoms/title.svelte
Normal 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>
|
||||||
46
src/lib/components/molecules/footer.svelte
Normal 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>
|
||||||
49
src/lib/components/molecules/grid-pattern.svelte
Normal 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>
|
||||||
67
src/lib/components/molecules/navbar.svelte
Normal 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>
|
||||||
54
src/lib/components/molecules/testimonial-card.svelte
Normal 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>
|
||||||
152
src/lib/components/organisms/about.svelte
Normal 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>
|
||||||
127
src/lib/components/organisms/contact.svelte
Normal 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>
|
||||||
65
src/lib/components/organisms/faq.svelte
Normal 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>
|
||||||
82
src/lib/components/organisms/features.svelte
Normal 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>
|
||||||
88
src/lib/components/organisms/hero.svelte
Normal 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>
|
||||||
106
src/lib/components/organisms/testimonials.svelte
Normal 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. I’ve 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 didn’t have to lift a finger. Truly professional and efficient.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Michael Smith",
|
||||||
|
rating: 5,
|
||||||
|
image: "https://randomuser.me/api/portraits/men/2.jpg",
|
||||||
|
review:
|
||||||
|
"I’ve 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 you’re 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>
|
||||||
24
src/lib/components/ui/accordion/accordion-content.svelte
Normal 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>
|
||||||
12
src/lib/components/ui/accordion/accordion-item.svelte
Normal 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} />
|
||||||
29
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal 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>
|
||||||
17
src/lib/components/ui/accordion/index.ts
Normal 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,
|
||||||
|
};
|
||||||
16
src/lib/components/ui/avatar/avatar-fallback.svelte
Normal 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}
|
||||||
|
/>
|
||||||
16
src/lib/components/ui/avatar/avatar-image.svelte
Normal 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}
|
||||||
|
/>
|
||||||
16
src/lib/components/ui/avatar/avatar.svelte
Normal 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}
|
||||||
|
/>
|
||||||
13
src/lib/components/ui/avatar/index.ts
Normal 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,
|
||||||
|
};
|
||||||
60
src/lib/components/ui/badge/badge.svelte
Normal 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>
|
||||||
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
75
src/lib/components/ui/button/button.svelte
Normal 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}
|
||||||
17
src/lib/components/ui/button/index.ts
Normal 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,
|
||||||
|
};
|
||||||
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
22
src/lib/components/ui/input/input.svelte
Normal 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}
|
||||||
|
/>
|
||||||
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
19
src/lib/components/ui/label/label.svelte
Normal 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}
|
||||||
|
/>
|
||||||
36
src/lib/components/ui/sheet/index.ts
Normal 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,
|
||||||
|
};
|
||||||
53
src/lib/components/ui/sheet/sheet-content.svelte
Normal 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>
|
||||||
16
src/lib/components/ui/sheet/sheet-description.svelte
Normal 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}
|
||||||
|
/>
|
||||||
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal 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>
|
||||||
20
src/lib/components/ui/sheet/sheet-header.svelte
Normal 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>
|
||||||
21
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal 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}
|
||||||
|
/>
|
||||||
16
src/lib/components/ui/sheet/sheet-title.svelte
Normal 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}
|
||||||
|
/>
|
||||||
28
src/lib/components/ui/textarea/index.ts
Normal 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,
|
||||||
|
};
|
||||||
22
src/lib/components/ui/textarea/textarea.svelte
Normal 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>
|
||||||
17
src/lib/core/constants.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const TRANSITION_COLORS = "transition-colors duration-150 ease-in-out";
|
||||||
|
export const TRANSITION_ALL = "transition-all duration-150 ease-in-out";
|
||||||
|
|
||||||
|
export const navLinks = [
|
||||||
|
{ name: "Home", href: "/#" },
|
||||||
|
{ name: "About", href: "/#about-us" },
|
||||||
|
{ name: "Services", href: "/#services" },
|
||||||
|
{ name: "Contact", href: "/#contact" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CONTACT_INFO = {
|
||||||
|
email: "contact@billdiscountservices.com",
|
||||||
|
phone: "+1 (844) 392-4558",
|
||||||
|
address: "1846 E INNOVATION PARK DR STE 100 ORO VALLEY, AZ 85755",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMPANY_NAME = "Bill Discount Services";
|
||||||
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
13
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Footer from "$lib/components/molecules/footer.svelte";
|
||||||
|
import Navbar from "$lib/components/molecules/navbar.svelte";
|
||||||
|
import "../app.css";
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
|
|
||||||
|
<Footer />
|
||||||
1
src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
||||||
15
src/routes/+page.svelte
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Features from "$lib/components/organisms/features.svelte";
|
||||||
|
import Testimonials from "$lib/components/organisms/testimonials.svelte";
|
||||||
|
import Contact from "$lib/components/organisms/contact.svelte";
|
||||||
|
import Hero from "$lib/components/organisms/hero.svelte";
|
||||||
|
import About from "$lib/components/organisms/about.svelte";
|
||||||
|
import Faq from "$lib/components/organisms/faq.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Hero />
|
||||||
|
<About />
|
||||||
|
<Features />
|
||||||
|
<Testimonials />
|
||||||
|
<Faq />
|
||||||
|
<Contact />
|
||||||
56
src/routes/legal/(main)/+page.svelte
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
|
import { COMPANY_NAME } from "$lib/core/constants";
|
||||||
|
import TableOfContents from "../table-of-contents.svelte";
|
||||||
|
|
||||||
|
const toc = [{ title: "Introduction", link: "#introduction" }];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="flex w-full flex-col-reverse gap-4 md:gap-8 lg:flex-row">
|
||||||
|
<div
|
||||||
|
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-green-200 drop-shadow-lg bg-gradient-to-b from-green-50 to-green/30 p-4 md:p-8"
|
||||||
|
id="introduction"
|
||||||
|
>
|
||||||
|
<Title size="h1" weight="medium">Introduction</Title>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Welcome to {COMPANY_NAME}'s legal documentation. As a professional bill
|
||||||
|
negotiation service dedicated to helping individuals and businesses reduce
|
||||||
|
their monthly expenses, we take our legal obligations seriously. This
|
||||||
|
section outlines our Privacy Policy, Terms and Conditions, and Cookie
|
||||||
|
Policy, ensuring complete transparency in how we handle your personal
|
||||||
|
information and billing data.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Our policies are designed to comply with consumer protection laws and data
|
||||||
|
privacy regulations. They detail our commitment to securing your
|
||||||
|
information, our bill negotiation processes, and your rights as our
|
||||||
|
client. We cover everything from how we handle your service provider
|
||||||
|
account details to how we protect your financial information during
|
||||||
|
negotiations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
At {COMPANY_NAME}, we believe in building trust through transparency.
|
||||||
|
That's why we've made our legal documents clear and accessible, explaining
|
||||||
|
our responsibilities to you and how we work to save you money on your
|
||||||
|
monthly bills. We encourage you to read through each section to fully
|
||||||
|
understand how we operate and protect your interests while providing our
|
||||||
|
cost-saving services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">Key Areas Covered:</h2>
|
||||||
|
<ul class="list-disc pl-6 space-y-2">
|
||||||
|
<li>How we handle and protect your personal information</li>
|
||||||
|
<li>Our bill negotiation process and service commitments</li>
|
||||||
|
<li>Your rights and responsibilities as our client</li>
|
||||||
|
<li>How we interact with service providers on your behalf</li>
|
||||||
|
<li>Our website usage terms</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableOfContents {toc} />
|
||||||
|
</section>
|
||||||
34
src/routes/legal/+layout.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "$lib/utils";
|
||||||
|
import { TRANSITION_COLORS } from "$lib/core/constants";
|
||||||
|
import { legalArticles } from "./legal.articles";
|
||||||
|
interface Props {
|
||||||
|
children?: import("svelte").Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="py-32 grid grid-cols-1 gap-4 px-4 md:px-8 lg:grid-cols-5 lg:gap-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-max flex-col gap-4 border border-green-200 bg-green-50/50 p-4 lg:col-span-1 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
{#each legalArticles as article}
|
||||||
|
<a
|
||||||
|
href={article.link}
|
||||||
|
class={cn(
|
||||||
|
"cursor-pointer text-green-950 hover:text-green-600",
|
||||||
|
TRANSITION_COLORS,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{article.title}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-4">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
17
src/routes/legal/legal.articles.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const legalArticles = [
|
||||||
|
{
|
||||||
|
id: "introduction",
|
||||||
|
title: "Introduction",
|
||||||
|
link: "/legal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "privacy-policy",
|
||||||
|
title: "Privacy Policy",
|
||||||
|
link: "/legal/privacy-policy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "terms-and-conditions",
|
||||||
|
title: "Terms & Conditions",
|
||||||
|
link: "/legal/terms-and-conditions",
|
||||||
|
},
|
||||||
|
];
|
||||||
165
src/routes/legal/privacy-policy/+page.svelte
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
|
import { COMPANY_NAME, CONTACT_INFO } from "$lib/core/constants";
|
||||||
|
import TableOfContents from "../table-of-contents.svelte";
|
||||||
|
|
||||||
|
const toc = [
|
||||||
|
{ title: "Information We Collect", link: "#information-we-collect" },
|
||||||
|
{
|
||||||
|
title: "How We Use Your Information",
|
||||||
|
link: "#how-we-use-your-information",
|
||||||
|
},
|
||||||
|
{ title: "Data Protection", link: "#data-protection" },
|
||||||
|
{ title: "Data Sharing", link: "#data-sharing" },
|
||||||
|
{ title: "Your Rights", link: "#your-rights" },
|
||||||
|
{ title: "Data Retention", link: "#data-retention" },
|
||||||
|
{ title: "Changes to Policy", link: "#changes-to-policy" },
|
||||||
|
{ title: "Contact Us", link: "#contact-us" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const lastUpdated = "January 5, 2025";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||||
|
<div
|
||||||
|
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-green-200 drop-shadow-lg bg-gradient-to-b from-green-50 to-green/30 p-4 md:p-8"
|
||||||
|
>
|
||||||
|
<Title size="h1" weight="medium">Privacy Policy</Title>
|
||||||
|
<p>
|
||||||
|
{COMPANY_NAME} is committed to protecting your privacy and ensuring the
|
||||||
|
security of your personal and financial information. This Privacy Policy
|
||||||
|
explains how we collect, use, and protect your data when you use our
|
||||||
|
bill negotiation services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="information-we-collect"
|
||||||
|
>1. Information We Collect</Title
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
We may collect the following types of information, based on the
|
||||||
|
situation, and mostly what is provided to us by you:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>
|
||||||
|
Personal Information:
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Full name and contact details</li>
|
||||||
|
<li>Billing addresses</li>
|
||||||
|
<li>Service provider account information</li>
|
||||||
|
<li>Payment information</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Bill Information:
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Current service provider details</li>
|
||||||
|
<li>Bill statements and history</li>
|
||||||
|
<li>Service plan details</li>
|
||||||
|
<li>Usage patterns and preferences</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Website Usage:
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Device and browser information</li>
|
||||||
|
<li>IP address and location data</li>
|
||||||
|
<li>How you interact with our website</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="how-we-use-your-information"
|
||||||
|
>2. How We Use Your Information</Title
|
||||||
|
>
|
||||||
|
<p>We use your information to:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Negotiate with service providers on your behalf</li>
|
||||||
|
<li>Analyze your current bills for savings opportunities</li>
|
||||||
|
<li>Contact service providers for better rates</li>
|
||||||
|
<li>Process service changes and updates</li>
|
||||||
|
<li>Improve our negotiation strategies</li>
|
||||||
|
<li>Communicate service updates and savings opportunities</li>
|
||||||
|
<li>Generate anonymized statistics about our services</li>
|
||||||
|
<li>Prevent fraud and ensure security</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="data-protection"
|
||||||
|
>3. Data Protection</Title
|
||||||
|
>
|
||||||
|
<p>We implement robust security measures including:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Encryption of sensitive financial data</li>
|
||||||
|
<li>Secure access controls and authentication</li>
|
||||||
|
<li>Regular security audits and updates</li>
|
||||||
|
<li>Staff training on data protection</li>
|
||||||
|
<li>Secure data backup systems</li>
|
||||||
|
<li>Incident response procedures</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="data-sharing"
|
||||||
|
>4. Data Sharing</Title
|
||||||
|
>
|
||||||
|
<p>We may share your information with:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Service providers (only as needed for negotiations)</li>
|
||||||
|
<li>Payment processors for billing purposes</li>
|
||||||
|
<li>Legal authorities when required by law</li>
|
||||||
|
<li>Third-party services essential to our operations</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-2">
|
||||||
|
We never sell your personal information to third parties or use it
|
||||||
|
for marketing purposes without your consent.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="your-rights">5. Your Rights</Title>
|
||||||
|
<p>You have the right to:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Request access your personal data</li>
|
||||||
|
<li>Correct inaccurate information</li>
|
||||||
|
<li>Request deletion of your data</li>
|
||||||
|
<li>Restrict or object to processing</li>
|
||||||
|
<li>Data portability</li>
|
||||||
|
<li>Withdraw consent at any time</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="data-retention"
|
||||||
|
>6. Data Retention</Title
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
We retain your information for as long as needed to provide our
|
||||||
|
services and comply with legal obligations. Typically, this means:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>
|
||||||
|
Account information: While your account is active plus 2 years
|
||||||
|
</li>
|
||||||
|
<li>Billing records: 7 years for tax purposes</li>
|
||||||
|
<li>Negotiation records: 3 years after service completion</li>
|
||||||
|
<li>Communication records: 2 years</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="changes-to-policy"
|
||||||
|
>7. Changes to Policy</Title
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
We may update this privacy policy to reflect changes in our
|
||||||
|
practices or legal requirements. So please keep checking this page
|
||||||
|
from time to time to stay informed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="contact-us">8. Contact Us</Title>
|
||||||
|
<p>
|
||||||
|
If you have questions about this privacy policy or wish to exercise
|
||||||
|
your rights, please contact us at:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4 mt-2">
|
||||||
|
<li>Email: {CONTACT_INFO.email}</li>
|
||||||
|
<li>Phone: {CONTACT_INFO.phone}</li>
|
||||||
|
<li>Address: {CONTACT_INFO.address}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<small class="text-gray-500 mt-4">Last Updated: {lastUpdated}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableOfContents {toc} />
|
||||||
|
</section>
|
||||||
31
src/routes/legal/table-of-contents.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
|
import { cn } from "$lib/utils";
|
||||||
|
import { TRANSITION_COLORS } from "$lib/core/constants";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
toc?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { toc = [] }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col gap-4 lg:max-w-[15rem]">
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col gap-4 rounded-lg border border-green-200 bg-green-50/50 p-4 shadow-sm"
|
||||||
|
>
|
||||||
|
<Title capitalize size="h5" weight="medium">Table of contents</Title>
|
||||||
|
|
||||||
|
{#each toc as each}
|
||||||
|
<a
|
||||||
|
class={cn(
|
||||||
|
"text-sgreen hover:text-green-600 cursor-pointer",
|
||||||
|
TRANSITION_COLORS,
|
||||||
|
)}
|
||||||
|
href={each.link}
|
||||||
|
>
|
||||||
|
{each.title}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
148
src/routes/legal/terms-and-conditions/+page.svelte
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Title from "$lib/components/atoms/title.svelte";
|
||||||
|
import { COMPANY_NAME, CONTACT_INFO } from "$lib/core/constants";
|
||||||
|
import TableOfContents from "../table-of-contents.svelte";
|
||||||
|
|
||||||
|
const toc = [
|
||||||
|
{ title: "Service Use", link: "#service-use" },
|
||||||
|
{ title: "Service Disclaimer", link: "#service-disclaimer" },
|
||||||
|
{ title: "User Accounts", link: "#user-accounts" },
|
||||||
|
{ title: "Negotiation Process", link: "#negotiation-process" },
|
||||||
|
{ title: "Fees and Payments", link: "#fees-and-payments" },
|
||||||
|
{ title: "Liability", link: "#liability" },
|
||||||
|
{ title: "Termination", link: "#termination" },
|
||||||
|
{ title: "Changes to Terms", link: "#changes-to-terms" },
|
||||||
|
{ title: "Contact Us", link: "#contact-us" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const lastUpdated = "January 5, 2025";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||||
|
<div
|
||||||
|
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-green-200 drop-shadow-lg bg-gradient-to-b from-green-50 to-green/30 p-4 md:p-8"
|
||||||
|
>
|
||||||
|
<Title size="h1" weight="medium">Terms & Conditions</Title>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Welcome to {COMPANY_NAME}. By using our platform, you agree to these
|
||||||
|
terms. Please read them carefully as they govern your use of our
|
||||||
|
bill negotiation and cost-saving services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="service-use">1. Service Use</Title>
|
||||||
|
<p>
|
||||||
|
{COMPANY_NAME} provides bill negotiation and cost-saving services. You
|
||||||
|
agree to:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Provide accurate billing and account information</li>
|
||||||
|
<li>Authorize us to negotiate on your behalf</li>
|
||||||
|
<li>Use the service legally and appropriately</li>
|
||||||
|
<li>Maintain the confidentiality of your account</li>
|
||||||
|
<li>Not misrepresent your identity or authority</li>
|
||||||
|
<li>Promptly respond to requests for additional information</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="service-disclaimer"
|
||||||
|
>2. Service Disclaimer</Title
|
||||||
|
>
|
||||||
|
<p>While we strive for the best results:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>We cannot guarantee specific savings amounts</li>
|
||||||
|
<li>Results may vary based on individual circumstances</li>
|
||||||
|
<li>Some negotiations may not result in savings</li>
|
||||||
|
<li>Time frames for negotiations may vary</li>
|
||||||
|
<li>Service availability depends on provider cooperation</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="user-accounts"
|
||||||
|
>3. User Accounts</Title
|
||||||
|
>
|
||||||
|
<p>When creating and maintaining an account, you must:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Provide accurate and complete information</li>
|
||||||
|
<li>Keep login credentials secure</li>
|
||||||
|
<li>Update account information promptly</li>
|
||||||
|
<li>Accept responsibility for account activities</li>
|
||||||
|
<li>Notify us immediately of unauthorized access</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="negotiation-process"
|
||||||
|
>4. Negotiation Process</Title
|
||||||
|
>
|
||||||
|
<p>Our negotiation process involves:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Analysis of current bills and services</li>
|
||||||
|
<li>Direct communication with service providers</li>
|
||||||
|
<li>Exploration of available options and promotions</li>
|
||||||
|
<li>Regular updates on negotiation progress</li>
|
||||||
|
<li>Implementation of agreed changes</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-2">
|
||||||
|
You authorize us to act as your representative during these
|
||||||
|
negotiations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="fees-and-payments"
|
||||||
|
>5. Fees and Payments</Title
|
||||||
|
>
|
||||||
|
<p>Our fee structure includes:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Success-based fees calculated on achieved savings</li>
|
||||||
|
<li>No upfront costs for basic services</li>
|
||||||
|
<li>Clear communication of any applicable fees</li>
|
||||||
|
<li>Payment terms as specified in service agreements</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="liability"
|
||||||
|
>6. Limitation of Liability</Title
|
||||||
|
>
|
||||||
|
<p>{COMPANY_NAME} is not liable for:</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Service provider decisions or actions</li>
|
||||||
|
<li>Interruption of services during negotiations</li>
|
||||||
|
<li>Accuracy of provider-supplied information</li>
|
||||||
|
<li>Consequential or indirect damages</li>
|
||||||
|
<li>Technical issues beyond our control</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="termination">7. Termination</Title>
|
||||||
|
<p>
|
||||||
|
Either party may terminate services by providing written notice.
|
||||||
|
Upon termination:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Outstanding fees become immediately due</li>
|
||||||
|
<li>Our authorization to negotiate ends</li>
|
||||||
|
<li>You assume responsibility for future negotiations</li>
|
||||||
|
<li>We retain earned fees from successful negotiations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="changes-to-terms"
|
||||||
|
>8. Changes to Terms</Title
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
We reserve the right to modify these terms at any time. Changes will
|
||||||
|
be:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4">
|
||||||
|
<li>Posted on our website</li>
|
||||||
|
<li>Notified via email for crucial changes</li>
|
||||||
|
<li>Effective 30 days after posting</li>
|
||||||
|
<li>Subject to your continued use constituting acceptance</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Title size="h3" weight="medium" id="contact-us">9. Contact Us</Title>
|
||||||
|
<p>For questions about these terms or our services, contact us at:</p>
|
||||||
|
<ul class="list-disc pl-4 mt-2">
|
||||||
|
<li>Email: {CONTACT_INFO.email}</li>
|
||||||
|
<li>Phone: {CONTACT_INFO.phone}</li>
|
||||||
|
<li>Address: {CONTACT_INFO.address}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<small class="text-gray-500 mt-4">Last Updated: {lastUpdated}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableOfContents {toc} />
|
||||||
|
</section>
|
||||||
BIN
static/favicon.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
static/font/outfit-variable.ttf
Normal file
BIN
static/images/contact-us.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
static/images/hero-img.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
static/images/how-we-work.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
static/images/laptop.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
static/images/office-conference.jpg
Normal file
|
After Width: | Height: | Size: 433 KiB |
BIN
static/logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
27
svelte.config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import adapter from "@sveltejs/adapter-static";
|
||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter({
|
||||||
|
pages: "build",
|
||||||
|
assets: "build",
|
||||||
|
fallback: undefined,
|
||||||
|
precompress: false,
|
||||||
|
strict: true,
|
||||||
|
}),
|
||||||
|
csrf: {
|
||||||
|
checkOrigin: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
97
tailwind.config.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
import tailwindcssAnimate from "tailwindcss-animate";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||||
|
safelist: ["dark"],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border) / <alpha-value>)",
|
||||||
|
input: "hsl(var(--input) / <alpha-value>)",
|
||||||
|
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||||
|
background: "hsl(var(--background) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||||
|
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: "hsl(var(--sidebar-background))",
|
||||||
|
foreground: "hsl(var(--sidebar-foreground))",
|
||||||
|
primary: "hsl(var(--sidebar-primary))",
|
||||||
|
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||||
|
accent: "hsl(var(--sidebar-accent))",
|
||||||
|
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||||
|
border: "hsl(var(--sidebar-border))",
|
||||||
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
xl: "calc(var(--radius) + 4px)",
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Outfit", ...fontFamily.sans],
|
||||||
|
serif: ["Outfit", ...fontFamily.serif],
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--bits-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--bits-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
"caret-blink": {
|
||||||
|
"0%,70%,100%": { opacity: "1" },
|
||||||
|
"20%,50%": { opacity: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [tailwindcssAnimate],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import Icons from "unplugin-icons/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit(), Icons({ compiler: "svelte" })],
|
||||||
|
});
|
||||||