Skip to content
Merged

Dev #124

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/kal-backend/src/lib/cache-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 24 additions & 1 deletion packages/kal-backend/src/routers/request-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import { z } from "zod";

import { RequestLogService } from "../lib/request-log-service.js";

Check warning on line 11 in packages/kal-backend/src/routers/request-logs.ts

View workflow job for this annotation

GitHub Actions / Lint & Type Check

`../lib/request-log-service.js` import should occur after import of `../lib/cache.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({
/**
Expand Down Expand Up @@ -133,4 +135,25 @@
},
};
}),

/**
* 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 };
});
}),
});
1 change: 1 addition & 0 deletions packages/kal-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
114 changes: 114 additions & 0 deletions packages/kal-frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<angle>";
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);
}
3 changes: 2 additions & 1 deletion packages/kal-frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -88,7 +89,7 @@ export default function RootLayout({
<TRPCProvider>
<div className="flex h-screen overflow-hidden">
{/* Main content β€” scrollable, takes remaining space */}
<div className="flex-1 min-w-0 overflow-y-auto">{children}</div>
<ScrollProvider>{children}</ScrollProvider>
{/* Right side: activity bar + chat panel (auth-gated internally) */}
<ChatWidget />
</div>
Expand Down
9 changes: 7 additions & 2 deletions packages/kal-frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ export default async function LandingPage() {
<main className="min-h-screen bg-dark relative overflow-hidden">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-0 transform -translate-x-1/2 left-1/2 w-[1000px] h-[500px] bg-accent/5 blur-[120px] rounded-full" />
<div className="absolute bottom-0 transform translate-x-1/2 right-1/2 w-[800px] h-[600px] bg-accent/5 blur-[120px] rounded-full" />
{/* Animated gradient blobs */}
<div className="absolute top-0 transform -translate-x-1/2 left-1/2 w-[1000px] h-[500px] bg-accent/5 blur-[120px] rounded-full animate-float-slow" />
<div className="absolute bottom-0 transform translate-x-1/2 right-1/2 w-[800px] h-[600px] bg-accent/5 blur-[120px] rounded-full animate-float-slower" />
{/* Dot grid overlay */}
<div className="absolute inset-0 dot-grid" />
{/* Center gradient line */}
<div className="absolute top-0 bottom-0 left-1/2 w-px bg-gradient-to-b from-transparent via-accent/[0.07] to-transparent" />
</div>

<Navbar onSignIn={onSignIn} />
Expand Down
4 changes: 4 additions & 0 deletions packages/kal-frontend/src/atoms/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { atom } from "jotai";

/** Whether the main scroll container has scrolled past the threshold */
export const scrolledAtom = atom(false);
28 changes: 28 additions & 0 deletions packages/kal-frontend/src/components/ScrollProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>) => {
const isScrolled = e.currentTarget.scrollTop > 20;
if (isScrolled !== lastScrolled.current) {
lastScrolled.current = isScrolled;
setScrolled(isScrolled);
}
},
[setScrolled]
);

return (
<div className="flex-1 min-w-0 overflow-y-auto" onScroll={handleScroll}>
{children}
</div>
);
}
47 changes: 33 additions & 14 deletions packages/kal-frontend/src/components/landing/CTA.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
"use client";

import { AnimateIn } from "@/components/ui/AnimateIn";
import { Button } from "@/components/ui/Button";
import { Container } from "@/components/ui/Container";

export function FinalCTA() {
return (
<section className="py-20">
<Container size="sm">
<div className="text-center p-10 md:p-14 rounded-2xl bg-dark-surface border border-dark-border">
<h2 className="text-3xl md:text-4xl font-bold text-content-primary mb-4">
Ready to build?
</h2>
<p className="text-content-secondary text-lg mb-8 max-w-md mx-auto">
Get your free API key and start integrating Malaysian food data today.
</p>
<Button href="/api-docs" size="lg">
Get Started with the API
<svg className="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Button>
</div>
<AnimateIn>
<div className="text-center p-10 md:p-14 rounded-2xl bg-gradient-to-b from-dark-surface to-dark-surface/80 border border-accent/20 relative overflow-hidden glow-green-md">
{/* Top accent line */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/40 to-transparent" />

<h2 className="text-3xl md:text-4xl font-bold text-content-primary mb-4">
Ready to build?
</h2>
<p className="text-content-secondary text-lg mb-8 max-w-md mx-auto">
Get your free API key and start integrating Malaysian food data
today.
</p>
<Button href="/api-docs" size="lg">
Get Started with the API
<svg
className="w-5 h-5 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</Button>
</div>
</AnimateIn>
</Container>
</section>
);
Expand Down
Loading
Loading