- {/* Primary aurora */}
-
-
+ {/* Static gradient base */}
+
+
+ {/* Subtle animated gradient overlay */}
+
- {/* Secondary aurora */}
- {/*
*/}
+ {/* Optional subtle particles for visual interest */}
+
- {/* Overlay gradient */}
-
+ {/* Content overlay gradient */}
+
{/* Content */}
diff --git a/nextjs-web-app/src/components/ui/browser-container.tsx b/nextjs-web-app/src/components/ui/browser-container.tsx
index 45f35b0..d77c93d 100644
--- a/nextjs-web-app/src/components/ui/browser-container.tsx
+++ b/nextjs-web-app/src/components/ui/browser-container.tsx
@@ -58,6 +58,11 @@ const Circle = styled.div<{ color: string; $clickable?: boolean }>`
height: 12px;
}
+ @media (min-width: 640px) {
+ width: 12px;
+ height: 12px;
+ }
+
&:hover {
opacity: 1;
transform: scale(${(props) => props.$clickable ? '1.2' : '1.1'});
diff --git a/nextjs-web-app/src/components/ui/shape-landing-hero.tsx b/nextjs-web-app/src/components/ui/shape-landing-hero.tsx
index aca553d..2c7e84b 100644
--- a/nextjs-web-app/src/components/ui/shape-landing-hero.tsx
+++ b/nextjs-web-app/src/components/ui/shape-landing-hero.tsx
@@ -75,11 +75,13 @@ function HeroGeometric({
badge = "Design Collective",
title1 = "Elevate Your Digital Vision",
title2 = "Crafting Exceptional Websites",
+ numWebsites = 9,
children,
}: {
badge?: string;
title1?: string;
title2?: string;
+ numWebsites?: number;
children?: React.ReactNode;
}) {
const fadeUpVariants = {
@@ -187,7 +189,7 @@ function HeroGeometric({
animate="visible"
>
- Enter your prompt and we will spin up 9 separate websites
+ Enter your prompt and we will spin up {numWebsites} separate websites
{" "}
IN SECONDS
diff --git a/nextjs-web-app/src/hooks/useDebounce.ts b/nextjs-web-app/src/hooks/useDebounce.ts
deleted file mode 100644
index 3178cc1..0000000
--- a/nextjs-web-app/src/hooks/useDebounce.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useState, useEffect } from 'react';
-
-/**
- * Custom hook for debouncing values to prevent excessive API calls
- * @param value - The value to debounce
- * @param delay - The delay in milliseconds
- * @returns The debounced value
- */
-export function useDebounce(value: T, delay: number): T {
- const [debouncedValue, setDebouncedValue] = useState(value);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
-
- return debouncedValue;
-}
-
-/**
- * Custom hook for debouncing callbacks to prevent excessive function calls
- * @param callback - The callback function to debounce
- * @param delay - The delay in milliseconds
- * @param deps - Dependencies array for the callback
- * @returns The debounced callback function
- */
-export function useDebouncedCallback any>(
- callback: T,
- delay: number,
- deps: React.DependencyList = []
-): T {
- const [debouncedCallback, setDebouncedCallback] = useState(() => callback);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedCallback(() => callback);
- }, delay);
-
- return () => {
- clearTimeout(handler);
- };
- }, [callback, delay, ...deps]);
-
- return debouncedCallback;
-}
diff --git a/nextjs-web-app/src/services/rateLimiter.ts b/nextjs-web-app/src/services/rateLimiter.ts
deleted file mode 100644
index c52c572..0000000
--- a/nextjs-web-app/src/services/rateLimiter.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Simple rate limiter service for tracking API usage
- * In production, this should be replaced with Redis or another persistent store
- */
-export class RateLimiter {
- private static instance: RateLimiter;
- private counts = new Map();
- private intervalId: NodeJS.Timeout | null = null;
-
- constructor() {
- // Reset counts every hour - store interval ID for cleanup
- this.intervalId = setInterval(() => this.counts.clear(), 60 * 60 * 1000);
- }
-
- /**
- * Clean up resources when the instance is no longer needed
- */
- destroy() {
- if (this.intervalId) {
- clearInterval(this.intervalId);
- this.intervalId = null;
- }
- this.counts.clear();
- }
-
- static getInstance() {
- if (!RateLimiter.instance) {
- RateLimiter.instance = new RateLimiter();
- }
- return RateLimiter.instance;
- }
-
- /**
- * Check if a key has exceeded its rate limit
- * @param key - Unique identifier (usually IP address)
- * @param limit - Maximum number of requests allowed
- * @returns boolean - true if under limit, false if exceeded
- */
- check(key: string, limit: number): boolean {
- const count = this.counts.get(key) || 0;
- if (count >= limit) return false;
- this.counts.set(key, count + 1);
- return true;
- }
-
- /**
- * Get remaining requests for a key
- * @param key - Unique identifier (usually IP address)
- * @param limit - Maximum number of requests allowed
- * @returns number - Remaining requests
- */
- getRemainingRequests(key: string, limit: number): number {
- const count = this.counts.get(key) || 0;
- return Math.max(0, limit - count);
- }
-}
diff --git a/nextjs-web-app/tailwind.config.js b/nextjs-web-app/tailwind.config.js
index ac3ef59..4d6bfb5 100644
--- a/nextjs-web-app/tailwind.config.js
+++ b/nextjs-web-app/tailwind.config.js
@@ -19,25 +19,58 @@ module.exports = {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
},
animation: {
- aurora: "aurora 15s linear infinite",
+ "gentle-shift": "gentle-shift 20s ease-in-out infinite",
+ "float-slow": "float-slow 15s ease-in-out infinite",
+ "float-delayed": "float-delayed 18s ease-in-out infinite 3s",
+ "float-gentle": "float-gentle 22s ease-in-out infinite 6s",
"text-gradient": "text-gradient 1.5s linear infinite",
"background-shine": "background-shine 2s linear infinite",
shimmer: "shimmer 2s linear infinite",
rainbow: "rainbow var(--speed, 2s) infinite linear",
},
keyframes: {
- aurora: {
- "0%": {
- transform: "translate(0, 0) scale(1)",
+ "gentle-shift": {
+ "0%, 100%": {
+ transform: "translateX(0) translateY(0)",
+ opacity: "0.3",
+ },
+ "50%": {
+ transform: "translateX(10px) translateY(-5px)",
+ opacity: "0.5",
+ },
+ },
+ "float-slow": {
+ "0%, 100%": {
+ transform: "translateY(0px) scale(1)",
+ opacity: "0.2",
+ },
+ "50%": {
+ transform: "translateY(-20px) scale(1.1)",
+ opacity: "0.4",
+ },
+ },
+ "float-delayed": {
+ "0%, 100%": {
+ transform: "translateY(0px) translateX(0px)",
+ opacity: "0.1",
},
"33%": {
- transform: "translate(-50%, 50%) scale(1.2)",
+ transform: "translateY(-15px) translateX(5px)",
+ opacity: "0.3",
},
"66%": {
- transform: "translate(50%, 50%) scale(0.9)",
+ transform: "translateY(-10px) translateX(-3px)",
+ opacity: "0.2",
+ },
+ },
+ "float-gentle": {
+ "0%, 100%": {
+ transform: "translateY(0px) rotate(0deg)",
+ opacity: "0.15",
},
- "100%": {
- transform: "translate(0, 0) scale(1)",
+ "50%": {
+ transform: "translateY(-12px) rotate(180deg)",
+ opacity: "0.35",
},
},
"text-gradient": {