diff --git a/packages/kal-backend/src/lib/cache-keys.ts b/packages/kal-backend/src/lib/cache-keys.ts index 8afc88f..be16f38 100644 --- a/packages/kal-backend/src/lib/cache-keys.ts +++ b/packages/kal-backend/src/lib/cache-keys.ts @@ -73,6 +73,11 @@ export const CacheKeys = { trpcFoodStats: (): string => `${PREFIX}:trpc:food:stats`, + // ============================================ + // tRPC - Request Logs (public) + // ============================================ + trpcPublicMonthlyStats: (): string => `${PREFIX}:trpc:logs:public-monthly`, + trpcFoodAllPaginated: ( cursor: number, limit: number, diff --git a/packages/kal-backend/src/routers/request-logs.ts b/packages/kal-backend/src/routers/request-logs.ts index 6980b6a..0b1a457 100644 --- a/packages/kal-backend/src/routers/request-logs.ts +++ b/packages/kal-backend/src/routers/request-logs.ts @@ -9,7 +9,9 @@ import { z } from "zod"; import { RequestLogService } from "../lib/request-log-service.js"; -import { protectedProcedure, router } from "../lib/trpc.js"; +import { CacheKeys, CacheTTL } from "../lib/cache-keys.js"; +import { cache } from "../lib/cache.js"; +import { protectedProcedure, publicProcedure, router } from "../lib/trpc.js"; export const requestLogsRouter = router({ /** @@ -133,4 +135,25 @@ export const requestLogsRouter = router({ }, }; }), + + /** + * Get total successful requests this month across all users (public, cached 1hr) + */ + publicMonthlyStats: publicProcedure.query(async ({ ctx }) => { + const cacheKey = CacheKeys.trpcPublicMonthlyStats(); + + return cache.wrap(cacheKey, CacheTTL.CATEGORIES, async () => { + const now = new Date(); + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); + + const successfulRequests = await ctx.db + .collection("api_request_logs") + .countDocuments({ + success: true, + timestamp: { $gte: monthStart }, + }); + + return { successfulRequests }; + }); + }), }); diff --git a/packages/kal-frontend/package.json b/packages/kal-frontend/package.json index dce6e84..cb5a204 100644 --- a/packages/kal-frontend/package.json +++ b/packages/kal-frontend/package.json @@ -21,6 +21,7 @@ "next": "^15.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "framer-motion": "^11.18.0", "react-feather": "^2.0.10", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1" diff --git a/packages/kal-frontend/src/app/globals.css b/packages/kal-frontend/src/app/globals.css index 5fde71c..c6e1d10 100644 --- a/packages/kal-frontend/src/app/globals.css +++ b/packages/kal-frontend/src/app/globals.css @@ -1091,3 +1091,117 @@ html { grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); } } + +/* ============================================ + Landing Page — Premium Effects + ============================================ */ + +/* Dot grid background overlay */ +.dot-grid { + background-image: radial-gradient( + circle, + rgba(255, 255, 255, 0.04) 1px, + transparent 1px + ); + background-size: 28px 28px; + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 1) 0%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 1) 0%, + rgba(0, 0, 0, 0.5) 50%, + rgba(0, 0, 0, 0) 100% + ); +} + +/* Code block window chrome */ +.code-window { + position: relative; +} + +.code-window::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent, + rgba(16, 185, 129, 0.2), + transparent + ); +} + +/* Animated gradient border for feature cards */ +.gradient-border-card { + position: relative; + z-index: 1; +} + +.gradient-border-card::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + padding: 1px; + background: conic-gradient( + from var(--gradient-angle, 0deg), + transparent 60%, + rgba(16, 185, 129, 0.3) 80%, + rgba(16, 185, 129, 0.5) 90%, + transparent 100% + ); + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; + opacity: 0; + transition: opacity 0.4s ease; +} + +.gradient-border-card:hover::before { + opacity: 1; + animation: rotateBorder 3s linear infinite; +} + +@keyframes rotateBorder { + to { + --gradient-angle: 360deg; + } +} + +@property --gradient-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +/* Glow effect utility */ +.glow-green-sm { + box-shadow: + 0 0 20px rgba(16, 185, 129, 0.06), + 0 0 40px rgba(16, 185, 129, 0.03); +} + +.glow-green-md { + box-shadow: + 0 0 30px rgba(16, 185, 129, 0.08), + 0 0 60px rgba(16, 185, 129, 0.04); +} + +.glow-green-lg { + box-shadow: + 0 0 40px rgba(16, 185, 129, 0.1), + 0 0 80px rgba(16, 185, 129, 0.05); +} diff --git a/packages/kal-frontend/src/app/layout.tsx b/packages/kal-frontend/src/app/layout.tsx index f9e4e54..7cdc335 100644 --- a/packages/kal-frontend/src/app/layout.tsx +++ b/packages/kal-frontend/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import "./globals.css"; import { ChatWidget } from "@/components/chat/ChatWidget"; +import { ScrollProvider } from "@/components/ScrollProvider"; import { ToastContainer } from "@/components/ui/Toast"; import { ToastProvider } from "@/contexts/ToastContext"; import { AuthProvider } from "@/lib/auth-context"; @@ -88,7 +89,7 @@ export default function RootLayout({
{/* Main content — scrollable, takes remaining space */} -
{children}
+ {children} {/* Right side: activity bar + chat panel (auth-gated internally) */}
diff --git a/packages/kal-frontend/src/app/page.tsx b/packages/kal-frontend/src/app/page.tsx index 408cdf4..846e34a 100644 --- a/packages/kal-frontend/src/app/page.tsx +++ b/packages/kal-frontend/src/app/page.tsx @@ -23,8 +23,13 @@ export default async function LandingPage() {
{/* Background Effects */}
-
-
+ {/* Animated gradient blobs */} +
+
+ {/* Dot grid overlay */} +
+ {/* Center gradient line */} +
diff --git a/packages/kal-frontend/src/atoms/scroll.ts b/packages/kal-frontend/src/atoms/scroll.ts new file mode 100644 index 0000000..4039610 --- /dev/null +++ b/packages/kal-frontend/src/atoms/scroll.ts @@ -0,0 +1,4 @@ +import { atom } from "jotai"; + +/** Whether the main scroll container has scrolled past the threshold */ +export const scrolledAtom = atom(false); diff --git a/packages/kal-frontend/src/components/ScrollProvider.tsx b/packages/kal-frontend/src/components/ScrollProvider.tsx new file mode 100644 index 0000000..602594c --- /dev/null +++ b/packages/kal-frontend/src/components/ScrollProvider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useSetAtom } from "jotai"; +import { useCallback, useRef } from "react"; + +import { scrolledAtom } from "@/atoms/scroll"; + +export function ScrollProvider({ children }: { children: React.ReactNode }) { + const setScrolled = useSetAtom(scrolledAtom); + const lastScrolled = useRef(false); + + const handleScroll = useCallback( + (e: React.UIEvent) => { + const isScrolled = e.currentTarget.scrollTop > 20; + if (isScrolled !== lastScrolled.current) { + lastScrolled.current = isScrolled; + setScrolled(isScrolled); + } + }, + [setScrolled] + ); + + return ( +
+ {children} +
+ ); +} diff --git a/packages/kal-frontend/src/components/landing/CTA.tsx b/packages/kal-frontend/src/components/landing/CTA.tsx index 20f438f..6f49405 100644 --- a/packages/kal-frontend/src/components/landing/CTA.tsx +++ b/packages/kal-frontend/src/components/landing/CTA.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { AnimateIn } from "@/components/ui/AnimateIn"; import { Button } from "@/components/ui/Button"; import { Container } from "@/components/ui/Container"; @@ -5,20 +8,36 @@ export function FinalCTA() { return (
-
-

- Ready to build? -

-

- Get your free API key and start integrating Malaysian food data today. -

- -
+ +
+ {/* Top accent line */} +
+ +

+ Ready to build? +

+

+ Get your free API key and start integrating Malaysian food data + today. +

+ +
+
); diff --git a/packages/kal-frontend/src/components/landing/FAQ.tsx b/packages/kal-frontend/src/components/landing/FAQ.tsx index 7a75bda..dfa7dc6 100644 --- a/packages/kal-frontend/src/components/landing/FAQ.tsx +++ b/packages/kal-frontend/src/components/landing/FAQ.tsx @@ -1,26 +1,32 @@ "use client"; import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { AnimateIn } from "@/components/ui/AnimateIn"; import { Container } from "@/components/ui/Container"; import { SectionHeading } from "@/components/ui/SectionHeading"; const faqs = [ { question: "Is Kal free to use?", - answer: "Yes, Kal is completely free with no hidden costs. You can search our entire food database without paying anything.", + answer: + "Yes, Kal is completely free with no hidden costs. You can search our entire food database without paying anything.", }, { question: "Do I need to create an account?", - answer: "No, you don't need to sign up or create an account. Just start searching for foods immediately — no registration required.", + answer: + "No, you don't need to sign up or create an account. Just start searching for foods immediately — no registration required.", }, { question: "How accurate is the nutritional data?", - answer: "Our database is carefully curated and regularly updated to ensure accuracy. We include verified nutritional information for each food item.", + answer: + "Our database is carefully curated and regularly updated to ensure accuracy. We include verified nutritional information for each food item.", }, { question: "Can I use Kal on my phone?", - answer: "Absolutely! Kal is fully responsive and works beautifully on all devices — phones, tablets, and desktops.", + answer: + "Absolutely! Kal is fully responsive and works beautifully on all devices — phones, tablets, and desktops.", }, ]; @@ -30,41 +36,66 @@ export function FAQ() { return (
- + + + -
- {faqs.map((faq, index) => ( -
- - {openIndex === index && ( -
-

{faq.answer}

-
- )} -
- ))} -
+ + {faq.question} + + + + + + + {openIndex === index && ( + +
+

+ {faq.answer} +

+
+
+ )} +
+
+ ))} +
+ ); diff --git a/packages/kal-frontend/src/components/landing/Features.tsx b/packages/kal-frontend/src/components/landing/Features.tsx index d94f0f5..70f533a 100644 --- a/packages/kal-frontend/src/components/landing/Features.tsx +++ b/packages/kal-frontend/src/components/landing/Features.tsx @@ -2,6 +2,11 @@ import { Check, Code, Database, Globe, Lock, Zap } from "react-feather"; +import { + AnimateIn, + StaggerContainer, + StaggerChild, +} from "@/components/ui/AnimateIn"; import { Container } from "@/components/ui/Container"; import { SectionHeading } from "@/components/ui/SectionHeading"; @@ -9,32 +14,43 @@ const features = [ { icon: , title: "Fast REST API", - description: "Lightning-fast responses with optimized endpoints for search and filtering", + description: + "Lightning-fast responses with optimized endpoints for search and filtering", + span: "md:col-span-2", }, { icon: , title: "Rich Food Data", - description: "Access 100+ Malaysian foods with complete macro nutritional information", + description: + "Access 100+ Malaysian foods with complete macro nutritional information", + span: "", }, { icon: , title: "Halal Certified", - description: "JAKIM certified foods with verified brand and certification details", + description: + "JAKIM certified foods with verified brand and certification details", + span: "", }, { icon: , title: "Simple Integration", - description: "Clean JSON responses that work with any language or framework", + description: + "Clean JSON responses that work with any language or framework", + span: "md:col-span-2", }, { icon: , title: "Free API Keys", description: "Get your API key instantly with generous rate limits", + span: "", }, { icon: , title: "Production Ready", - description: "Reliable infrastructure built for your production applications", + description: + "Reliable infrastructure built for your production applications", + span: "", }, ]; @@ -42,32 +58,36 @@ export function Features() { return (
- + + + -
+ {features.map((feature, index) => ( -
- {/* Icon */} -
- {feature.icon} -
+ +
+ {/* Icon */} +
+ {feature.icon} +
- {/* Content */} -

- {feature.title} -

-

- {feature.description} -

-
+ {/* Content */} +

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} -
+
); diff --git a/packages/kal-frontend/src/components/landing/Footer.tsx b/packages/kal-frontend/src/components/landing/Footer.tsx index c370a59..3237486 100644 --- a/packages/kal-frontend/src/components/landing/Footer.tsx +++ b/packages/kal-frontend/src/components/landing/Footer.tsx @@ -1,39 +1,46 @@ +"use client"; + import Link from "next/link"; +import { AnimateIn } from "@/components/ui/AnimateIn"; import { Container } from "@/components/ui/Container"; export function Footer() { return (
-
- {/* Logo */} - -
- Kal - - - {/* Legal Links */} -
- - Privacy Policy - - - Terms of Service + +
+ {/* Logo */} + +
+ + Kal + -
- {/* Copyright */} -

- © {new Date().getFullYear()} Kal. All rights reserved. -

-
+ {/* Legal Links */} +
+ + Privacy Policy + + + Terms of Service + +
+ + {/* Copyright */} +

+ © {new Date().getFullYear()} Kal. All rights reserved. +

+
+
); diff --git a/packages/kal-frontend/src/components/landing/Hero.tsx b/packages/kal-frontend/src/components/landing/Hero.tsx index db9d253..5304473 100644 --- a/packages/kal-frontend/src/components/landing/Hero.tsx +++ b/packages/kal-frontend/src/components/landing/Hero.tsx @@ -1,100 +1,205 @@ "use client"; +import { motion } from "framer-motion"; + import { Button } from "@/components/ui/Button"; import { Container } from "@/components/ui/Container"; -import { TypewriterNumber } from "@/components/ui/TypewriterNumber"; +import { CountUp } from "@/components/ui/CountUp"; import { trpc } from "@/lib/trpc"; +const fadeUp = { + hidden: { opacity: 0, y: 20 }, + visible: (delay: number) => ({ + opacity: 1, + y: 0, + transition: { duration: 0.6, delay, ease: [0.25, 0.1, 0.25, 1] }, + }), +}; + export function Hero() { const { data: stats } = trpc.food.stats.useQuery(); + const { data: logStats } = trpc.requestLogs.publicMonthlyStats.useQuery(); return ( -
+
-
+
{/* Logo */} -
+
Kal -
+
- {/* Headline */} -

+ {/* Headline — gradient text */} + Malaysian Food Nutrition API -

+ {/* Subheadline */} -

- Access comprehensive nutritional data for Malaysian foods. - Build health apps, track calories, or integrate food data into your projects - with our free REST API. -

+ + Access comprehensive nutritional data for Malaysian foods. Build + health apps, track calories, or integrate food data into your + projects with our free REST API. + {/* CTA Buttons */} -
+ -
+ - {/* Stats */} -
- {/* Foods Count */} -
-
- {stats?.total ? ( - - ) : ( - ... - )} + {/* Code Preview */} + +
+ {/* Window chrome */} +
+
+
+
+ + Terminal +
-
- Foods + {/* Code content */} +
+
+ ${" "} + curl{" "} + + https://api.kal.my/api/v1/foods/search + {" "} + \ +
+
+ -H{" "} + + "x-api-key: your_key" + {" "} + \ +
+
+ -d{" "} + + '{`{"query": "nasi lemak"}`}' + +
+
+ {"// "} + 200 OK + + {" — "} + calories, protein, carbs, fat & more + +
+ - {/* Halal Foods Count */} -
-
- {stats?.halal ? ( - - ) : ( - ... - )} -
-
- Halal Certified + {/* Stats */} + +
+ {/* Foods Count */} +
+
+ {stats?.total ? ( + + ) : ( + + ... + + )} +
+
Foods
-
- {/* Free */} -
-
- Free -
-
- API Access + {/* Halal Foods Count */} +
+
+ {stats?.halal ? ( + + ) : ( + + ... + + )} +
+
+ Halal Certified +
-
- {/* Response Time */} -
-
- <50ms -
-
- Response Time + {/* Requests This Month */} +
+
+ {logStats?.successfulRequests != null ? ( + + ) : ( + + ... + + )} +
+
+ Requests This Month +
-
+
diff --git a/packages/kal-frontend/src/components/landing/HowItWorks.tsx b/packages/kal-frontend/src/components/landing/HowItWorks.tsx index bf48b91..30f0a35 100644 --- a/packages/kal-frontend/src/components/landing/HowItWorks.tsx +++ b/packages/kal-frontend/src/components/landing/HowItWorks.tsx @@ -2,6 +2,11 @@ import { Code, Key, Zap } from "react-feather"; +import { + AnimateIn, + StaggerContainer, + StaggerChild, +} from "@/components/ui/AnimateIn"; import { Container } from "@/components/ui/Container"; import { SectionHeading } from "@/components/ui/SectionHeading"; @@ -10,7 +15,7 @@ const steps = [ number: "1", icon: , title: "Get Your API Key", - description: "Sign in to your dashboard and generate a free API key", + description: "Sign in to your dashboard and generate your API key", }, { number: "2", @@ -30,39 +35,44 @@ export function HowItWorks() { return (
- + + + -
+ {steps.map((step, index) => ( -
- {/* Step number */} -
-
- {step.icon} + +
+ {/* Step icon */} +
+
+ {step.icon} +
+ + {step.number} +
- - {step.number} - -
- {/* Content */} -

- {step.title} -

-

- {step.description} -

+ {/* Content */} +

+ {step.title} +

+

{step.description}

- {/* Connector line (not on last) */} - {index < steps.length - 1 && ( -
- )} -
+ {/* Connector line (not on last) */} + {index < steps.length - 1 && ( +
+ )} +
+
))} -
+
); diff --git a/packages/kal-frontend/src/components/landing/Navbar.tsx b/packages/kal-frontend/src/components/landing/Navbar.tsx index 7293706..0618667 100644 --- a/packages/kal-frontend/src/components/landing/Navbar.tsx +++ b/packages/kal-frontend/src/components/landing/Navbar.tsx @@ -1,9 +1,11 @@ "use client"; +import { useAtomValue } from "jotai"; import Link from "next/link"; import { useState } from "react"; import { Menu, X } from "react-feather"; +import { scrolledAtom } from "@/atoms/scroll"; import { Button } from "@/components/ui/Button"; import { Container } from "@/components/ui/Container"; @@ -21,9 +23,16 @@ interface NavbarProps { export function Navbar({ onSignIn }: NavbarProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const scrolled = useAtomValue(scrolledAtom); return ( -