@kalikayi/shared 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@kalikayi/shared",
3
+ "version": "0.1.0",
4
+ "description": "Shared components, hooks, and utilities for the Kalikayi ecosystem",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./styles/theme-base.css": "./src/styles/theme-base.css"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "peerDependencies": {
19
+ "lucide-react": ">=0.400.0",
20
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
21
+ "next-themes": ">=0.3.0",
22
+ "react": "^18.0.0 || ^19.0.0",
23
+ "react-dom": "^18.0.0 || ^19.0.0"
24
+ },
25
+ "dependencies": {
26
+ "clsx": "^2.1.0",
27
+ "tailwind-merge": "^3.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^25.3.5",
31
+ "@types/react": "^19.0.0",
32
+ "@types/react-dom": "^19.0.0",
33
+ "typescript": "^5.7.0"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/sunilkalikayi/kalikayi-hub"
39
+ },
40
+ "keywords": [
41
+ "kalikayi",
42
+ "shared",
43
+ "components",
44
+ "next.js"
45
+ ]
46
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const slotHeights: Record<string, string> = {
7
+ "top-leaderboard": "min-h-[90px]",
8
+ "in-content": "min-h-[250px]",
9
+ "bottom-rectangle": "min-h-[280px]",
10
+ sidebar: "min-h-[250px]",
11
+ };
12
+
13
+ interface AdBannerProps {
14
+ slot?: string;
15
+ format?: "auto" | "rectangle" | "horizontal" | "vertical";
16
+ className?: string;
17
+ }
18
+
19
+ export function AdBanner({ slot, format = "auto", className }: AdBannerProps) {
20
+ const adRef = useRef<HTMLDivElement>(null);
21
+ const adsenseId = typeof window !== "undefined"
22
+ ? process.env.NEXT_PUBLIC_ADSENSE_ID
23
+ : undefined;
24
+ const height = slot ? (slotHeights[slot] ?? "min-h-[90px]") : "min-h-[90px]";
25
+
26
+ useEffect(() => {
27
+ if (!adsenseId || !slot) return;
28
+ try {
29
+ const w = window as Window & { adsbygoogle?: unknown[] };
30
+ (w.adsbygoogle = w.adsbygoogle ?? []).push({});
31
+ } catch {
32
+ // AdSense not loaded
33
+ }
34
+ }, [adsenseId, slot]);
35
+
36
+ // Dev placeholder or no slot/adsenseId
37
+ if (!adsenseId || !slot) {
38
+ return (
39
+ <div
40
+ className={cn(
41
+ "flex items-center justify-center rounded-lg border border-dashed border-border/50 bg-muted/30 text-xs text-muted-foreground/40",
42
+ height,
43
+ className
44
+ )}
45
+ aria-hidden="true"
46
+ >
47
+ Ad: {slot || "placeholder"}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ // Production AdSense
53
+ return (
54
+ <div ref={adRef} className={cn("ad-container overflow-hidden", height, className)} aria-hidden="true">
55
+ <ins
56
+ className="adsbygoogle"
57
+ style={{ display: "block" }}
58
+ data-ad-client={adsenseId}
59
+ data-ad-slot={slot}
60
+ data-ad-format={format}
61
+ data-full-width-responsive="true"
62
+ />
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { ExternalLink } from "lucide-react";
4
+ import { getLiveTools } from "../../lib/kalikayi-tools";
5
+ import { cn } from "../../lib/utils";
6
+
7
+ interface ToolPromoBannerProps {
8
+ variant: "horizontal" | "card-grid";
9
+ excludeTool?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export function ToolPromoBanner({ variant, excludeTool, className }: ToolPromoBannerProps) {
14
+ const tools = getLiveTools().filter((t) => t.name !== excludeTool);
15
+
16
+ if (tools.length === 0) return null;
17
+
18
+ if (variant === "horizontal") {
19
+ return (
20
+ <div
21
+ className={cn(
22
+ "rounded-xl border border-primary/20 bg-primary/5 p-4",
23
+ className
24
+ )}
25
+ >
26
+ <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-primary">
27
+ More Free Tools by Kalikayi
28
+ </p>
29
+ <div className="flex flex-wrap gap-3">
30
+ {tools.map((tool) => (
31
+ <a
32
+ key={tool.name}
33
+ href={tool.href}
34
+ target="_blank"
35
+ rel="noopener noreferrer"
36
+ className="flex items-center gap-1.5 rounded-lg border border-border/50 bg-background/60 px-3 py-2 text-sm font-medium text-foreground hover:border-primary/30 hover:bg-primary/5"
37
+ >
38
+ {tool.name}
39
+ <ExternalLink className="h-3 w-3 text-muted-foreground" />
40
+ </a>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <div className={className}>
49
+ <p className="mb-4 text-xs font-semibold uppercase tracking-wider text-primary">
50
+ Explore More Free Tools
51
+ </p>
52
+ <div className="grid gap-3 sm:grid-cols-2">
53
+ {tools.map((tool) => (
54
+ <a
55
+ key={tool.name}
56
+ href={tool.href}
57
+ target="_blank"
58
+ rel="noopener noreferrer"
59
+ className="rounded-lg border border-border/50 p-4 hover:border-primary/30 hover:shadow-md"
60
+ >
61
+ <div className="flex items-center justify-between">
62
+ <div>
63
+ <p className="font-semibold text-foreground">{tool.name}</p>
64
+ <p className="text-sm text-muted-foreground">{tool.tagline}</p>
65
+ </div>
66
+ <ExternalLink className="h-4 w-4 text-muted-foreground" />
67
+ </div>
68
+ </a>
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,119 @@
1
+ import { Github, Linkedin, Globe } from "lucide-react";
2
+
3
+ const QUICK_LINKS = [
4
+ { href: "https://kalikayi.com/tools", label: "Tools" },
5
+ { href: "https://kalikayi.com/blog", label: "Blog" },
6
+ { href: "https://kalikayi.com/about", label: "About" },
7
+ { href: "https://kalikayi.com/contact", label: "Contact" },
8
+ { href: "https://kalikayi.com/creator", label: "Creator" },
9
+ ];
10
+
11
+ const LEGAL_LINKS = [
12
+ { href: "https://kalikayi.com/privacy-policy", label: "Privacy Policy" },
13
+ { href: "https://kalikayi.com/terms", label: "Terms of Service" },
14
+ { href: "https://kalikayi.com/disclaimer", label: "Disclaimer" },
15
+ ];
16
+
17
+ const SOCIAL = {
18
+ github: "https://github.com/sunilkalikayi",
19
+ linkedin: "https://linkedin.com/in/sunilkalikayi",
20
+ portfolio: "https://sunilkalikayi.vercel.app/",
21
+ };
22
+
23
+ export function Footer() {
24
+ const year = new Date().getFullYear();
25
+
26
+ return (
27
+ <footer className="border-t border-border/30 bg-card/50">
28
+ <div className="mx-auto max-w-7xl px-6 py-12">
29
+ <div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
30
+ {/* Brand */}
31
+ <div className="space-y-3">
32
+ <h3 className="text-lg font-bold text-primary">Kalikayi</h3>
33
+ <p className="text-sm leading-relaxed text-muted-foreground">
34
+ Free online tools that just work. No sign-up, no paywalls, no tricks.
35
+ </p>
36
+ </div>
37
+
38
+ {/* Quick Links */}
39
+ <div className="space-y-3">
40
+ <h4 className="text-sm font-semibold">Quick Links</h4>
41
+ <nav className="flex flex-col gap-2">
42
+ {QUICK_LINKS.map((link) => (
43
+ <a
44
+ key={link.href}
45
+ href={link.href}
46
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
47
+ >
48
+ {link.label}
49
+ </a>
50
+ ))}
51
+ </nav>
52
+ </div>
53
+
54
+ {/* Legal */}
55
+ <div className="space-y-3">
56
+ <h4 className="text-sm font-semibold">Legal</h4>
57
+ <nav className="flex flex-col gap-2">
58
+ {LEGAL_LINKS.map((link) => (
59
+ <a
60
+ key={link.href}
61
+ href={link.href}
62
+ className="text-sm text-muted-foreground transition-colors hover:text-foreground"
63
+ >
64
+ {link.label}
65
+ </a>
66
+ ))}
67
+ </nav>
68
+ </div>
69
+
70
+ {/* Connect */}
71
+ <div className="space-y-3">
72
+ <h4 className="text-sm font-semibold">Connect</h4>
73
+ <div className="flex gap-3">
74
+ <a
75
+ href={SOCIAL.github}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="text-muted-foreground transition-colors hover:text-foreground"
79
+ aria-label="GitHub"
80
+ >
81
+ <Github size={20} />
82
+ </a>
83
+ <a
84
+ href={SOCIAL.linkedin}
85
+ target="_blank"
86
+ rel="noopener noreferrer"
87
+ className="text-muted-foreground transition-colors hover:text-foreground"
88
+ aria-label="LinkedIn"
89
+ >
90
+ <Linkedin size={20} />
91
+ </a>
92
+ <a
93
+ href={SOCIAL.portfolio}
94
+ target="_blank"
95
+ rel="noopener noreferrer"
96
+ className="text-muted-foreground transition-colors hover:text-foreground"
97
+ aria-label="Portfolio"
98
+ >
99
+ <Globe size={20} />
100
+ </a>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ {/* Separator */}
106
+ <div className="my-8 h-px bg-border/30" />
107
+
108
+ <div className="flex flex-col items-center justify-between gap-2 text-center sm:flex-row">
109
+ <p className="text-xs text-muted-foreground">
110
+ &copy; {year} Kalikayi. All rights reserved.
111
+ </p>
112
+ <p className="text-xs text-muted-foreground">
113
+ Made with care for everyone
114
+ </p>
115
+ </div>
116
+ </div>
117
+ </footer>
118
+ );
119
+ }
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { AccentPicker } from "../theme/accent-picker";
6
+ import { ThemeToggle } from "../theme/theme-toggle";
7
+ import { MobileNav } from "./mobile-nav";
8
+ import { cn } from "../../lib/utils";
9
+ import type { SubHeaderConfig } from "../../types";
10
+
11
+ const NAV_LINKS = [
12
+ { href: "https://kalikayi.com", label: "Home" },
13
+ { href: "https://kalikayi.com/tools", label: "Tools" },
14
+ { href: "https://kalikayi.com/blog", label: "Blog" },
15
+ { href: "https://kalikayi.com/about", label: "About" },
16
+ { href: "https://kalikayi.com/contact", label: "Contact" },
17
+ ];
18
+
19
+ interface HeaderProps {
20
+ subHeaderConfig?: SubHeaderConfig;
21
+ }
22
+
23
+ export function Header({ subHeaderConfig }: HeaderProps) {
24
+ const pathname = usePathname();
25
+
26
+ return (
27
+ <header className="sticky top-0 z-50 w-full border-b border-border/30 bg-background/80 backdrop-blur-xl">
28
+ <div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
29
+ {/* Logo */}
30
+ <a
31
+ href="https://kalikayi.com"
32
+ className="text-xl font-bold tracking-tight text-primary"
33
+ >
34
+ Kalikayi
35
+ </a>
36
+
37
+ {/* Desktop nav */}
38
+ <nav className="hidden items-center gap-1 md:flex">
39
+ {NAV_LINKS.map((link) => {
40
+ const isActive =
41
+ link.href === "https://kalikayi.com"
42
+ ? pathname === "/"
43
+ : pathname.startsWith(new URL(link.href).pathname);
44
+ return (
45
+ <a
46
+ key={link.href}
47
+ href={link.href}
48
+ className={cn(
49
+ "relative px-3 py-2 text-sm font-medium transition-colors",
50
+ isActive
51
+ ? "text-primary"
52
+ : "text-muted-foreground hover:text-foreground"
53
+ )}
54
+ >
55
+ {link.label}
56
+ {isActive && (
57
+ <span className="absolute inset-x-3 -bottom-[13px] h-0.5 rounded-full bg-primary" />
58
+ )}
59
+ </a>
60
+ );
61
+ })}
62
+ </nav>
63
+
64
+ {/* Right side controls */}
65
+ <div className="flex items-center gap-2">
66
+ <div className="hidden items-center gap-2 md:flex">
67
+ <AccentPicker />
68
+ <ThemeToggle />
69
+ </div>
70
+ <MobileNav subHeaderConfig={subHeaderConfig} />
71
+ </div>
72
+ </div>
73
+ </header>
74
+ );
75
+ }
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import { usePathname } from "next/navigation";
6
+ import { Menu, X } from "lucide-react";
7
+ import { AccentPicker } from "../theme/accent-picker";
8
+ import { ThemeToggle } from "../theme/theme-toggle";
9
+ import { cn } from "../../lib/utils";
10
+ import type { SubHeaderConfig } from "../../types";
11
+
12
+ const HUB_NAV_LINKS = [
13
+ { href: "https://kalikayi.com", label: "Home" },
14
+ { href: "https://kalikayi.com/tools", label: "Tools" },
15
+ { href: "https://kalikayi.com/blog", label: "Blog" },
16
+ { href: "https://kalikayi.com/about", label: "About" },
17
+ { href: "https://kalikayi.com/contact", label: "Contact" },
18
+ ];
19
+
20
+ interface MobileNavProps {
21
+ subHeaderConfig?: SubHeaderConfig;
22
+ }
23
+
24
+ export function MobileNav({ subHeaderConfig }: MobileNavProps) {
25
+ const [open, setOpen] = useState(false);
26
+ const pathname = usePathname();
27
+
28
+ return (
29
+ <>
30
+ <button
31
+ className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground md:hidden"
32
+ onClick={() => setOpen(true)}
33
+ aria-label="Open menu"
34
+ >
35
+ <Menu className="h-5 w-5" />
36
+ </button>
37
+
38
+ {/* Overlay */}
39
+ {open && (
40
+ <div className="fixed inset-0 z-50 md:hidden">
41
+ {/* Backdrop */}
42
+ <div
43
+ className="fixed inset-0 bg-background/80 backdrop-blur-sm"
44
+ onClick={() => setOpen(false)}
45
+ />
46
+
47
+ {/* Sheet */}
48
+ <div className="fixed inset-y-0 right-0 w-[280px] border-l border-border/30 bg-background p-0 shadow-lg">
49
+ {/* Header */}
50
+ <div className="flex items-center justify-between border-b border-border/30 px-6 py-4">
51
+ <span className="font-bold text-primary">Kalikayi</span>
52
+ <button
53
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
54
+ onClick={() => setOpen(false)}
55
+ aria-label="Close menu"
56
+ >
57
+ <X className="h-4 w-4" />
58
+ </button>
59
+ </div>
60
+
61
+ {/* Hub nav */}
62
+ <nav className="flex flex-col gap-1 p-4">
63
+ <p className="mb-1 px-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
64
+ Kalikayi
65
+ </p>
66
+ {HUB_NAV_LINKS.map((link) => (
67
+ <a
68
+ key={link.href}
69
+ href={link.href}
70
+ onClick={() => setOpen(false)}
71
+ className={cn(
72
+ "rounded-lg px-4 py-2.5 text-sm font-medium transition-colors",
73
+ "text-muted-foreground hover:bg-accent hover:text-foreground"
74
+ )}
75
+ >
76
+ {link.label}
77
+ </a>
78
+ ))}
79
+ </nav>
80
+
81
+ {/* Tool nav */}
82
+ {subHeaderConfig && (
83
+ <nav className="flex flex-col gap-1 border-t border-border/30 p-4">
84
+ <p className="mb-1 px-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
85
+ {subHeaderConfig.name}
86
+ </p>
87
+
88
+ {subHeaderConfig.dropdowns?.map((dropdown) => (
89
+ <div key={dropdown.label}>
90
+ <p className="px-4 py-1.5 text-xs font-medium text-primary">
91
+ {dropdown.label}
92
+ </p>
93
+ {dropdown.items.map((item) => (
94
+ <Link
95
+ key={item.href}
96
+ href={item.href}
97
+ onClick={() => setOpen(false)}
98
+ className="block rounded-lg px-6 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
99
+ >
100
+ {item.label}
101
+ </Link>
102
+ ))}
103
+ </div>
104
+ ))}
105
+
106
+ {subHeaderConfig.navLinks?.map((link) => {
107
+ const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
108
+ return (
109
+ <Link
110
+ key={link.href}
111
+ href={link.href}
112
+ onClick={() => setOpen(false)}
113
+ className={cn(
114
+ "rounded-lg px-4 py-2.5 text-sm font-medium transition-colors",
115
+ isActive
116
+ ? "bg-primary/10 text-primary"
117
+ : "text-muted-foreground hover:bg-accent hover:text-foreground"
118
+ )}
119
+ >
120
+ {link.label}
121
+ </Link>
122
+ );
123
+ })}
124
+ </nav>
125
+ )}
126
+
127
+ {/* Controls */}
128
+ <div className="mt-auto border-t border-border/30 px-6 py-4">
129
+ <div className="flex items-center justify-between">
130
+ <AccentPicker />
131
+ <ThemeToggle />
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ )}
137
+ </>
138
+ );
139
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import Link from "next/link";
5
+ import { usePathname } from "next/navigation";
6
+ import { ChevronDown } from "lucide-react";
7
+ import { cn } from "../../lib/utils";
8
+ import type { SubHeaderConfig } from "../../types";
9
+
10
+ interface SubHeaderProps {
11
+ config: SubHeaderConfig;
12
+ }
13
+
14
+ export function SubHeader({ config }: SubHeaderProps) {
15
+ const pathname = usePathname();
16
+ const [openDropdown, setOpenDropdown] = useState<string | null>(null);
17
+ const dropdownRef = useRef<HTMLDivElement>(null);
18
+
19
+ useEffect(() => {
20
+ function handleClickOutside(e: MouseEvent) {
21
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
22
+ setOpenDropdown(null);
23
+ }
24
+ }
25
+ document.addEventListener("mousedown", handleClickOutside);
26
+ return () => document.removeEventListener("mousedown", handleClickOutside);
27
+ }, []);
28
+
29
+ return (
30
+ <div className="sticky top-14 z-40 w-full border-b border-border/20 bg-card/50 backdrop-blur-lg">
31
+ <div className="mx-auto flex h-12 max-w-7xl items-center gap-6 px-6" ref={dropdownRef}>
32
+ {/* Tool logo + name */}
33
+ <Link
34
+ href={config.href}
35
+ className="flex items-center gap-2 text-sm font-bold text-foreground"
36
+ >
37
+ {config.logo}
38
+ <span>{config.name}</span>
39
+ </Link>
40
+
41
+ {/* Desktop nav */}
42
+ <nav className="hidden items-center gap-1 md:flex">
43
+ {/* Dropdowns */}
44
+ {config.dropdowns?.map((dropdown) => (
45
+ <div key={dropdown.label} className="relative">
46
+ <button
47
+ className={cn(
48
+ "flex items-center gap-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
49
+ openDropdown === dropdown.label
50
+ ? "text-primary"
51
+ : "text-muted-foreground hover:text-foreground"
52
+ )}
53
+ onClick={() =>
54
+ setOpenDropdown(
55
+ openDropdown === dropdown.label ? null : dropdown.label
56
+ )
57
+ }
58
+ >
59
+ {dropdown.label}
60
+ <ChevronDown
61
+ className={cn(
62
+ "h-3.5 w-3.5 transition-transform",
63
+ openDropdown === dropdown.label && "rotate-180"
64
+ )}
65
+ />
66
+ </button>
67
+ {openDropdown === dropdown.label && (
68
+ <div className="absolute left-0 top-full z-50 mt-1 w-64 rounded-lg border border-border/50 bg-popover p-2 shadow-lg">
69
+ {dropdown.items.map((item) => (
70
+ <Link
71
+ key={item.href}
72
+ href={item.href}
73
+ onClick={() => setOpenDropdown(null)}
74
+ className="block rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
75
+ >
76
+ <span className="font-medium text-foreground">
77
+ {item.label}
78
+ </span>
79
+ {item.description && (
80
+ <span className="mt-0.5 block text-xs text-muted-foreground">
81
+ {item.description}
82
+ </span>
83
+ )}
84
+ </Link>
85
+ ))}
86
+ </div>
87
+ )}
88
+ </div>
89
+ ))}
90
+
91
+ {/* Nav links */}
92
+ {config.navLinks?.map((link) => {
93
+ const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
94
+ return (
95
+ <Link
96
+ key={link.href}
97
+ href={link.href}
98
+ className={cn(
99
+ "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
100
+ isActive
101
+ ? "text-primary"
102
+ : "text-muted-foreground hover:text-foreground"
103
+ )}
104
+ >
105
+ {link.label}
106
+ </Link>
107
+ );
108
+ })}
109
+ </nav>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,81 @@
1
+ import type { FAQ, BreadcrumbItem } from "../../types";
2
+
3
+ interface JsonLdProps {
4
+ data: Record<string, unknown>;
5
+ }
6
+
7
+ export function JsonLd({ data }: JsonLdProps) {
8
+ return (
9
+ <script
10
+ type="application/ld+json"
11
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
12
+ />
13
+ );
14
+ }
15
+
16
+ export function WebAppJsonLd({
17
+ name,
18
+ url,
19
+ description,
20
+ }: {
21
+ name: string;
22
+ url: string;
23
+ description: string;
24
+ }) {
25
+ return (
26
+ <JsonLd
27
+ data={{
28
+ "@context": "https://schema.org",
29
+ "@type": "WebApplication",
30
+ name,
31
+ url,
32
+ description,
33
+ applicationCategory: "UtilityApplication",
34
+ operatingSystem: "All",
35
+ offers: {
36
+ "@type": "Offer",
37
+ price: "0",
38
+ priceCurrency: "USD",
39
+ },
40
+ }}
41
+ />
42
+ );
43
+ }
44
+
45
+ export function FAQJsonLd({ faqs }: { faqs: FAQ[] }) {
46
+ if (faqs.length === 0) return null;
47
+
48
+ return (
49
+ <JsonLd
50
+ data={{
51
+ "@context": "https://schema.org",
52
+ "@type": "FAQPage",
53
+ mainEntity: faqs.map((faq) => ({
54
+ "@type": "Question",
55
+ name: faq.question,
56
+ acceptedAnswer: {
57
+ "@type": "Answer",
58
+ text: faq.answer,
59
+ },
60
+ })),
61
+ }}
62
+ />
63
+ );
64
+ }
65
+
66
+ export function BreadcrumbJsonLd({ items }: { items: BreadcrumbItem[] }) {
67
+ return (
68
+ <JsonLd
69
+ data={{
70
+ "@context": "https://schema.org",
71
+ "@type": "BreadcrumbList",
72
+ itemListElement: items.map((item, i) => ({
73
+ "@type": "ListItem",
74
+ position: i + 1,
75
+ name: item.name,
76
+ item: item.url,
77
+ })),
78
+ }}
79
+ />
80
+ );
81
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const ACCENTS = [
7
+ { name: "teal", color: "oklch(0.55 0.15 165)" },
8
+ { name: "blue", color: "oklch(0.55 0.20 260)" },
9
+ { name: "purple", color: "oklch(0.55 0.22 290)" },
10
+ { name: "orange", color: "oklch(0.65 0.18 55)" },
11
+ { name: "rose", color: "oklch(0.55 0.20 10)" },
12
+ ] as const;
13
+
14
+ type AccentName = (typeof ACCENTS)[number]["name"];
15
+
16
+ function getStoredAccent(): AccentName {
17
+ if (typeof window === "undefined") return "teal";
18
+ return (localStorage.getItem("accent") as AccentName) || "teal";
19
+ }
20
+
21
+ function applyAccent(accent: AccentName) {
22
+ const root = document.documentElement;
23
+ if (accent === "teal") {
24
+ root.removeAttribute("data-accent");
25
+ } else {
26
+ root.setAttribute("data-accent", accent);
27
+ }
28
+ localStorage.setItem("accent", accent);
29
+ }
30
+
31
+ export function AccentPicker() {
32
+ const [active, setActive] = useState<AccentName>("teal");
33
+
34
+ useEffect(() => {
35
+ const stored = getStoredAccent();
36
+ setActive(stored);
37
+ applyAccent(stored);
38
+ }, []);
39
+
40
+ return (
41
+ <div className="flex items-center gap-1.5" role="radiogroup" aria-label="Accent color">
42
+ {ACCENTS.map(({ name, color }) => (
43
+ <button
44
+ key={name}
45
+ role="radio"
46
+ aria-checked={active === name}
47
+ aria-label={`${name} accent`}
48
+ className={cn(
49
+ "h-5 w-5 rounded-full border-2 transition-transform duration-200 hover:scale-110",
50
+ active === name
51
+ ? "border-foreground scale-110"
52
+ : "border-transparent"
53
+ )}
54
+ style={{ backgroundColor: color }}
55
+ onClick={() => {
56
+ setActive(name);
57
+ applyAccent(name);
58
+ }}
59
+ />
60
+ ))}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
4
+ import type { ReactNode } from "react";
5
+
6
+ export function ThemeProvider({ children }: { children: ReactNode }) {
7
+ return (
8
+ <NextThemesProvider
9
+ attribute="class"
10
+ defaultTheme="system"
11
+ enableSystem
12
+ disableTransitionOnChange={false}
13
+ >
14
+ {children}
15
+ </NextThemesProvider>
16
+ );
17
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { useTheme } from "next-themes";
4
+ import { useEffect, useState } from "react";
5
+ import { Moon, Sun } from "lucide-react";
6
+
7
+ export function ThemeToggle() {
8
+ const { resolvedTheme, setTheme } = useTheme();
9
+ const [mounted, setMounted] = useState(false);
10
+
11
+ useEffect(() => setMounted(true), []);
12
+
13
+ if (!mounted) {
14
+ return (
15
+ <button
16
+ className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground opacity-50"
17
+ disabled
18
+ aria-label="Toggle theme"
19
+ >
20
+ <Sun className="h-[18px] w-[18px]" />
21
+ </button>
22
+ );
23
+ }
24
+
25
+ const isDark = resolvedTheme === "dark";
26
+
27
+ return (
28
+ <button
29
+ className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
30
+ onClick={() => setTheme(isDark ? "light" : "dark")}
31
+ aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
32
+ >
33
+ {isDark ? (
34
+ <Sun className="h-[18px] w-[18px]" />
35
+ ) : (
36
+ <Moon className="h-[18px] w-[18px]" />
37
+ )}
38
+ </button>
39
+ );
40
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+
5
+ export function useCopyToClipboard(resetDelay = 2000) {
6
+ const [copied, setCopied] = useState(false);
7
+
8
+ const copy = useCallback(
9
+ async (text: string) => {
10
+ try {
11
+ await navigator.clipboard.writeText(text);
12
+ setCopied(true);
13
+ setTimeout(() => setCopied(false), resetDelay);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ },
19
+ [resetDelay]
20
+ );
21
+
22
+ return { copy, copied };
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ // Types
2
+ export type {
3
+ FAQ,
4
+ BreadcrumbItem,
5
+ KalikayiTool,
6
+ NavLink,
7
+ NavDropdown,
8
+ SubHeaderConfig,
9
+ } from "./types";
10
+
11
+ // Lib
12
+ export { cn } from "./lib/utils";
13
+ export {
14
+ kalikayiTools,
15
+ getLiveTools,
16
+ getRandomTools,
17
+ } from "./lib/kalikayi-tools";
18
+
19
+ // SEO
20
+ export {
21
+ JsonLd,
22
+ WebAppJsonLd,
23
+ FAQJsonLd,
24
+ BreadcrumbJsonLd,
25
+ } from "./components/seo/json-ld";
26
+
27
+ // Ads
28
+ export { AdBanner } from "./components/ads/ad-banner";
29
+ export { ToolPromoBanner } from "./components/ads/tool-promo-banner";
30
+
31
+ // Theme
32
+ export { ThemeProvider } from "./components/theme/theme-provider";
33
+ export { AccentPicker } from "./components/theme/accent-picker";
34
+ export { ThemeToggle } from "./components/theme/theme-toggle";
35
+
36
+ // Layout
37
+ export { Header } from "./components/layout/header";
38
+ export { SubHeader } from "./components/layout/sub-header";
39
+ export { MobileNav } from "./components/layout/mobile-nav";
40
+ export { Footer } from "./components/layout/footer";
41
+
42
+ // Hooks
43
+ export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
@@ -0,0 +1,35 @@
1
+ import type { KalikayiTool } from "../types";
2
+
3
+ export const kalikayiTools: KalikayiTool[] = [
4
+ {
5
+ name: "FreeResumePick",
6
+ tagline: "Free resume builder & ATS scanner",
7
+ href: "https://kalikayi.com/tools/free-resume-pick",
8
+ icon: "FileText",
9
+ isLive: true,
10
+ },
11
+ {
12
+ name: "FreeTypingBoard",
13
+ tagline: "Free typing tutor with 30+ lessons",
14
+ href: "https://kalikayi.com/tools/free-typing-board",
15
+ icon: "Keyboard",
16
+ isLive: true,
17
+ },
18
+ {
19
+ name: "FreeCalcKit",
20
+ tagline: "50+ free calculators & converters",
21
+ href: "https://freecalckit.com",
22
+ icon: "Calculator",
23
+ isLive: true,
24
+ },
25
+ ];
26
+
27
+ export function getLiveTools(): KalikayiTool[] {
28
+ return kalikayiTools.filter((tool) => tool.isLive);
29
+ }
30
+
31
+ export function getRandomTools(exclude?: string, count = 3): KalikayiTool[] {
32
+ const live = getLiveTools().filter((t) => t.name !== exclude);
33
+ const shuffled = [...live].sort(() => Math.random() - 0.5);
34
+ return shuffled.slice(0, count);
35
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,159 @@
1
+ /* ========================================
2
+ KALIKAYI BASE THEME — OKLCH Color System
3
+ 5 accents (Teal, Blue, Purple, Orange, Rose) x 2 modes (Light/Dark)
4
+ ======================================== */
5
+
6
+ /* ========================================
7
+ BASE LIGHT THEME (Teal accent — default)
8
+ ======================================== */
9
+ :root {
10
+ --radius: 0.625rem;
11
+ --background: oklch(0.985 0.002 250);
12
+ --foreground: oklch(0.13 0.03 265);
13
+ --card: oklch(0.97 0.003 250);
14
+ --card-foreground: oklch(0.13 0.03 265);
15
+ --popover: oklch(0.985 0.002 250);
16
+ --popover-foreground: oklch(0.13 0.03 265);
17
+ --primary: oklch(0.55 0.15 165);
18
+ --primary-foreground: oklch(0.98 0.005 165);
19
+ --secondary: oklch(0.95 0.005 250);
20
+ --secondary-foreground: oklch(0.2 0.03 265);
21
+ --muted: oklch(0.95 0.005 250);
22
+ --muted-foreground: oklch(0.45 0.03 260);
23
+ --accent: oklch(0.93 0.01 250);
24
+ --accent-foreground: oklch(0.2 0.03 265);
25
+ --destructive: oklch(0.577 0.245 27.325);
26
+ --border: oklch(0.9 0.01 255);
27
+ --input: oklch(0.9 0.01 255);
28
+ --ring: oklch(0.55 0.15 165);
29
+ --glow: oklch(0.55 0.15 165);
30
+ --glow-muted: oklch(0.55 0.15 165 / 15%);
31
+ --chart-1: oklch(0.646 0.222 41.116);
32
+ --chart-2: oklch(0.6 0.118 184.704);
33
+ --chart-3: oklch(0.398 0.07 227.392);
34
+ --chart-4: oklch(0.828 0.189 84.429);
35
+ --chart-5: oklch(0.769 0.188 70.08);
36
+ --sidebar: oklch(0.984 0.003 247.858);
37
+ --sidebar-foreground: oklch(0.129 0.042 264.695);
38
+ --sidebar-primary: oklch(0.208 0.042 265.755);
39
+ --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
40
+ --sidebar-accent: oklch(0.968 0.007 247.896);
41
+ --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
42
+ --sidebar-border: oklch(0.929 0.013 255.508);
43
+ --sidebar-ring: oklch(0.704 0.04 256.788);
44
+ }
45
+
46
+ /* ========================================
47
+ BASE DARK THEME (Teal accent — default)
48
+ ======================================== */
49
+ .dark {
50
+ --background: oklch(0.07 0.01 260);
51
+ --foreground: oklch(0.96 0.005 250);
52
+ --card: oklch(0.12 0.015 260);
53
+ --card-foreground: oklch(0.96 0.005 250);
54
+ --popover: oklch(0.12 0.015 260);
55
+ --popover-foreground: oklch(0.96 0.005 250);
56
+ --primary: oklch(0.8 0.17 165);
57
+ --primary-foreground: oklch(0.07 0.01 260);
58
+ --secondary: oklch(0.17 0.02 260);
59
+ --secondary-foreground: oklch(0.96 0.005 250);
60
+ --muted: oklch(0.17 0.02 260);
61
+ --muted-foreground: oklch(0.73 0.015 255);
62
+ --accent: oklch(0.17 0.02 260);
63
+ --accent-foreground: oklch(0.96 0.005 250);
64
+ --destructive: oklch(0.704 0.191 22.216);
65
+ --border: oklch(1 0 0 / 14%);
66
+ --input: oklch(1 0 0 / 18%);
67
+ --ring: oklch(0.8 0.17 165);
68
+ --glow: oklch(0.8 0.17 165);
69
+ --glow-muted: oklch(0.8 0.17 165 / 18%);
70
+ --chart-1: oklch(0.488 0.243 264.376);
71
+ --chart-2: oklch(0.696 0.17 162.48);
72
+ --chart-3: oklch(0.769 0.188 70.08);
73
+ --chart-4: oklch(0.627 0.265 303.9);
74
+ --chart-5: oklch(0.645 0.246 16.439);
75
+ --sidebar: oklch(0.1 0.015 260);
76
+ --sidebar-foreground: oklch(0.93 0.008 250);
77
+ --sidebar-primary: oklch(0.72 0.19 165);
78
+ --sidebar-primary-foreground: oklch(0.93 0.008 250);
79
+ --sidebar-accent: oklch(0.15 0.02 260);
80
+ --sidebar-accent-foreground: oklch(0.93 0.008 250);
81
+ --sidebar-border: oklch(1 0 0 / 8%);
82
+ --sidebar-ring: oklch(0.72 0.19 165);
83
+ }
84
+
85
+ /* ========================================
86
+ ACCENT: BLUE
87
+ ======================================== */
88
+ [data-accent="blue"] {
89
+ --primary: oklch(0.55 0.20 260);
90
+ --primary-foreground: oklch(0.98 0.005 260);
91
+ --ring: oklch(0.55 0.20 260);
92
+ --glow: oklch(0.55 0.20 260);
93
+ --glow-muted: oklch(0.55 0.20 260 / 15%);
94
+ }
95
+ [data-accent="blue"].dark,
96
+ .dark[data-accent="blue"] {
97
+ --primary: oklch(0.70 0.18 260);
98
+ --primary-foreground: oklch(0.07 0.01 260);
99
+ --ring: oklch(0.70 0.18 260);
100
+ --glow: oklch(0.70 0.18 260);
101
+ --glow-muted: oklch(0.70 0.18 260 / 18%);
102
+ }
103
+
104
+ /* ========================================
105
+ ACCENT: PURPLE
106
+ ======================================== */
107
+ [data-accent="purple"] {
108
+ --primary: oklch(0.55 0.22 290);
109
+ --primary-foreground: oklch(0.98 0.005 290);
110
+ --ring: oklch(0.55 0.22 290);
111
+ --glow: oklch(0.55 0.22 290);
112
+ --glow-muted: oklch(0.55 0.22 290 / 15%);
113
+ }
114
+ [data-accent="purple"].dark,
115
+ .dark[data-accent="purple"] {
116
+ --primary: oklch(0.70 0.20 290);
117
+ --primary-foreground: oklch(0.07 0.01 260);
118
+ --ring: oklch(0.70 0.20 290);
119
+ --glow: oklch(0.70 0.20 290);
120
+ --glow-muted: oklch(0.70 0.20 290 / 18%);
121
+ }
122
+
123
+ /* ========================================
124
+ ACCENT: ORANGE
125
+ ======================================== */
126
+ [data-accent="orange"] {
127
+ --primary: oklch(0.65 0.18 55);
128
+ --primary-foreground: oklch(0.98 0.005 55);
129
+ --ring: oklch(0.65 0.18 55);
130
+ --glow: oklch(0.65 0.18 55);
131
+ --glow-muted: oklch(0.65 0.18 55 / 15%);
132
+ }
133
+ [data-accent="orange"].dark,
134
+ .dark[data-accent="orange"] {
135
+ --primary: oklch(0.78 0.16 55);
136
+ --primary-foreground: oklch(0.07 0.01 260);
137
+ --ring: oklch(0.78 0.16 55);
138
+ --glow: oklch(0.78 0.16 55);
139
+ --glow-muted: oklch(0.78 0.16 55 / 18%);
140
+ }
141
+
142
+ /* ========================================
143
+ ACCENT: ROSE
144
+ ======================================== */
145
+ [data-accent="rose"] {
146
+ --primary: oklch(0.55 0.20 10);
147
+ --primary-foreground: oklch(0.98 0.005 10);
148
+ --ring: oklch(0.55 0.20 10);
149
+ --glow: oklch(0.55 0.20 10);
150
+ --glow-muted: oklch(0.55 0.20 10 / 15%);
151
+ }
152
+ [data-accent="rose"].dark,
153
+ .dark[data-accent="rose"] {
154
+ --primary: oklch(0.72 0.18 10);
155
+ --primary-foreground: oklch(0.07 0.01 260);
156
+ --ring: oklch(0.72 0.18 10);
157
+ --glow: oklch(0.72 0.18 10);
158
+ --glow-muted: oklch(0.72 0.18 10 / 18%);
159
+ }
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface FAQ {
4
+ question: string;
5
+ answer: string;
6
+ }
7
+
8
+ export interface BreadcrumbItem {
9
+ name: string;
10
+ url: string;
11
+ }
12
+
13
+ export interface KalikayiTool {
14
+ name: string;
15
+ tagline: string;
16
+ href: string;
17
+ icon: string;
18
+ isLive: boolean;
19
+ }
20
+
21
+ export interface NavLink {
22
+ href: string;
23
+ label: string;
24
+ description?: string;
25
+ }
26
+
27
+ export interface NavDropdown {
28
+ label: string;
29
+ items: NavLink[];
30
+ }
31
+
32
+ export interface SubHeaderConfig {
33
+ name: string;
34
+ href: string;
35
+ logo: ReactNode;
36
+ navLinks?: NavLink[];
37
+ dropdowns?: NavDropdown[];
38
+ }