diff --git a/backend/app/classification/cache_helpers.py b/backend/app/classification/cache_helpers.py new file mode 100644 index 00000000..48f6a27f --- /dev/null +++ b/backend/app/classification/cache_helpers.py @@ -0,0 +1,222 @@ +import re +import time +import asyncio +import logging +from typing import Optional, Dict, Any +from cachetools import TTLCache +from collections import Counter +from langchain_core.messages import HumanMessage +import xxhash +import hashlib +import json + +logger = logging.getLogger(__name__) + +try: + _HAS_XXHASH = True +except Exception: + xxhash = None + _HAS_XXHASH = False + +# Config +CACHE_MAXSIZE = 4096 +CACHE_TTL_SECONDS = 60 * 60 +MAX_MESSAGE_LENGTH = 10000 # Max message length to process (prevents DoS via large payloads) + +# Patterns for fast-path classification (concise to reduce memory) +# Merge related intents into fewer regexes and add common Discord patterns +_PATTERNS = { + # common salutations + "greeting": re.compile(r"^\s*(?:hi|hello|hey|good\s+morning|good\s+afternoon|good\s+evening)\b", re.I), + # explicit help / action requests + "action_request": re.compile(r".*\b(?:help|please\s+help|plz\s+help|need\s+help|support|assist|request)\b", re.I), + # bug / error reports + "bug_report": re.compile(r".*\b(?:bug|error|exception|stack\s*trace|crash|failed|traceback|segfault)\b", re.I), + # thanks and short acknowledgements (shared fast-path) + "thanks_ack": re.compile(r"^\s*(?:thanks|thank\s+you|thx|ty|ok|okay|got\s+it|roger|ack)\b", re.I), + # modern short responses / slang that are non-actionable + "slang": re.compile(r"^\s*(?:brb|lol|lmao|rofl|omg|wtf|smh|idk|np|yw|pls|plz|bump|ping|fyi|imo|idc)\b", re.I), + # general intent bucket for optimization/performance/docs/feature keywords + "intent_general": re.compile( + r".*\b(?:optimi[sz]e|improve|speed\s*up|performance|memory|resource|efficient|documentation|docs|guide|tutorial|example|feature|suggest|idea)\b", + re.I, + ), + # Discord-specific: user mentions (@user) + "discord_mention": re.compile(r"(?:<@!?\d+>|@\w+)\b"), + # Channel mentions (#channel or <#123456>) + "channel_mention": re.compile(r"(?:<#\d+>|#\w+)\b"), + # Bot/CLI-like commands commonly used on Discord (prefix-based) + "command": re.compile(r"^\s*(?:/|!|\?|\.|\$)[A-Za-z0-9_\-]+"), + # Code snippets or blocks (inline or triple backticks) + "code_block": re.compile(r"```[\s\S]*?```|`[^`]+`", re.S), + # URLs (simple detection) + "url": re.compile(r"https?://\S+|www\.\S+"), + # GitHub/issue/PR references (#123, owner/repo#123, PR #123) + "pr_issue_ref": re.compile(r"(?:\b#\d+\b|\b[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#\d+\b|\bPR\s*#\d+\b)", re.I), + # Emoji shortname like :emoji: + "emoji_short": re.compile(r":[a-zA-Z0-9_+\-]+:"), +} + +# Simple deterministic classifications for the patterns +# Keep mapping concise and reflect combined pattern keys +_PATTERN_CLASSIFICATION = { + "greeting": {"needs_devrel": False, "priority": "low", "reasoning": "greeting"}, + "thanks_ack": {"needs_devrel": False, "priority": "low", "reasoning": "thanks/acknowledgement"}, + "slang": {"needs_devrel": False, "priority": "low", "reasoning": "short/slang response"}, + "action_request": {"needs_devrel": True, "priority": "high", "reasoning": "explicit help/request keywords"}, + "bug_report": {"needs_devrel": True, "priority": "high", "reasoning": "error or bug report"}, + "integration": {"needs_devrel": True, "priority": "high", "reasoning": "Discord/GitHub/integration requests (OAuth, commands, threads, repo ops)"}, + "architecture": {"needs_devrel": True, "priority": "medium", "reasoning": "architecture/infra mentions (queues, DBs, LLMs)"}, + "intent_general": {"needs_devrel": True, "priority": "medium", "reasoning": "optimization/docs/feature requests"}, + + # Discord/GitHub specific quick classifications + "discord_mention": {"needs_devrel": False, "priority": "low", "reasoning": "user mention"}, + "channel_mention": {"needs_devrel": False, "priority": "low", "reasoning": "channel mention"}, + "command": {"needs_devrel": False, "priority": "medium", "reasoning": "bot/CLI command invocation"}, + "code_block": {"needs_devrel": False, "priority": "low", "reasoning": "code snippet or block"}, + "url": {"needs_devrel": False, "priority": "low", "reasoning": "contains URL"}, + "pr_issue_ref": {"needs_devrel": True, "priority": "medium", "reasoning": "reference to issue or PR"}, + "emoji_short": {"needs_devrel": False, "priority": "low", "reasoning": "emoji shortname"}, +} + +_cache = TTLCache(maxsize=CACHE_MAXSIZE, ttl=CACHE_TTL_SECONDS) +# In-flight calls to dedupe concurrent identical requests (bounded with TTL to prevent leaks) +_inflight: TTLCache = TTLCache(maxsize=1000, ttl=120) # Max 1000 concurrent, 2min timeout + +# Simple metrics +metrics = Counter({"total": 0, "cache_hits": 0, "cache_misses": 0, "skipped_llm": 0}) + + +# Simple cache key generation +def make_key(model: str, prompt: str, params: Dict[str, Any]) -> str: + """ + Create a stable cache key using XXHash128 for speed. + - normalize prompt to reduce trivial differences + - serialize params with sorted keys and compact separators + - use blake2b as a fallback if xxhash unavailable + """ + norm_prompt = normalize_message(prompt) + + # Serialize params once; for very large params consider hashing only relevant fields + try: + params_blob = json.dumps(params or {}, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8") + except Exception: + params_blob = str(params).encode("utf-8") + + payload = b"|".join([model.encode("utf-8"), norm_prompt.encode("utf-8"), params_blob]) + + # Use XXHash128 for better collision resistance (if available), otherwise fallback + if _HAS_XXHASH: + return xxhash.xxh3_128_hexdigest(payload) + else: + return hashlib.blake2b(payload, digest_size=16).hexdigest() + + +def _compose_prompt_with_context(normalized: str, context_id: Optional[str]) -> str: + if context_id: + return f"{normalized}|ctx:{context_id}" + return normalized + + +def key_for_normalized(normalized: str, context_id: Optional[str], model: str, params: Dict[str, Any]) -> str: + """ + Compute cache key from a normalized message and optional context id. + """ + prompt = _compose_prompt_with_context(normalized, context_id) + return make_key(model, prompt, params) + + +def get_cached_by_normalized(normalized: str, context_id: Optional[str], model: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Retrieve cached payload for a normalized message + context.""" + key = key_for_normalized(normalized, context_id, model, params) + return cache_get(key) + + +def set_cached_by_normalized(normalized: str, context_id: Optional[str], model: str, params: Dict[str, Any], payload: Dict[str, Any]) -> None: + """Store payload for normalized message + context.""" + key = key_for_normalized(normalized, context_id, model, params) + cache_set(key, payload) + + +# Cache wrapper for LLM calls (async - uses llm.ainvoke) +async def cached_llm_call(prompt: str, model: str, params: Dict[str, Any], llm): + """ + Cached wrapper for async LLM calls with: + - fast-path simple pattern classification to avoid LLM cost + - cache hit/miss metrics + - in-flight deduplication so concurrent identical requests share one LLM call + """ + # Fast-path: simple deterministic classification (avoid LLM) + normalized = normalize_message(prompt) + simple = is_simple_message(normalized) + if simple is not None: + metrics["skipped_llm"] += 1 + return simple + + metrics["total"] += 1 + key = make_key(model, prompt, params) + + # Quick cache check + cached = cache_get(key) + if cached is not None: + metrics["cache_hits"] += 1 + return cached + + metrics["cache_misses"] += 1 + + # Deduplicate in-flight identical calls so only one LLM request is made + loop = asyncio.get_running_loop() + # Attempt to install a future atomically to dedupe concurrent callers + future = loop.create_future() + prev = _inflight.setdefault(key, future) + if prev is not future: + # another caller is in-flight; await its result/failure + return await prev + + # we are the owner; perform the fetch and set the future result/exception + async def _owner_fetch(): + try: + start = time.time() + response = await llm.ainvoke([HumanMessage(content=prompt)]) + elapsed = time.time() - start + # store response content or small payload rather than full object + result = response.content if hasattr(response, "content") else response + _cache[key] = result + future.set_result(result) + return result + except asyncio.CancelledError: + future.cancel() + raise + except Exception as e: + future.set_exception(e) + raise + finally: + # ensure inflight entry removed + _inflight.pop(key, None) + + # schedule owner fetch and await its result + loop.create_task(_owner_fetch()) + return await future + +def normalize_message(msg: str) -> str: + """Normalize message for caching. Truncates to MAX_MESSAGE_LENGTH to prevent DoS.""" + s = (msg or "")[:MAX_MESSAGE_LENGTH].strip().lower() + s = re.sub(r"\s+", " ", s) + return s + +def is_simple_message(normalized: str) -> Optional[Dict[str, Any]]: + for name, pattern in _PATTERNS.items(): + if pattern.match(normalized): + return dict(_PATTERN_CLASSIFICATION[name], original_message=normalized) + return None + +def cache_get(key: str) -> Optional[Dict[str, Any]]: + try: + return _cache[key] + except KeyError: + return None + + +def cache_set(key: str, value: Dict[str, Any]) -> None: + """Store value in cache.""" + _cache[key] = value \ No newline at end of file diff --git a/backend/app/classification/classification_router.py b/backend/app/classification/classification_router.py index 1708dced..97032ca3 100644 --- a/backend/app/classification/classification_router.py +++ b/backend/app/classification/classification_router.py @@ -1,12 +1,26 @@ +import asyncio import logging +import json from typing import Dict, Any from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.messages import HumanMessage from app.core.config import settings from .prompt import DEVREL_TRIAGE_PROMPT +from app.classification.cache_helpers import ( + normalize_message, + is_simple_message, + get_cached_by_normalized, + set_cached_by_normalized, + metrics, + MAX_MESSAGE_LENGTH, +) logger = logging.getLogger(__name__) +# Limit concurrent LLM calls to prevent rate limiting and cost explosions +_LLM_SEMAPHORE = asyncio.Semaphore(10) + + class ClassificationRouter: """Simple DevRel triage - determines if message needs DevRel assistance""" @@ -20,28 +34,59 @@ def __init__(self, llm_client=None): async def should_process_message(self, message: str, context: Dict[str, Any] = None) -> Dict[str, Any]: """Simple triage: Does this message need DevRel assistance?""" try: + # Early return for oversized messages to prevent DoS + if len(message) > MAX_MESSAGE_LENGTH: + logger.warning(f"Message exceeds max length ({len(message)} > {MAX_MESSAGE_LENGTH}), using fallback") + return self._fallback_triage(message[:MAX_MESSAGE_LENGTH]) + + metrics["total"] += 1 + normalized = normalize_message(message) + + # fast-path: simple pattern match (no LLM) + simple = is_simple_message(normalized) + + if simple is not None: + metrics["skipped_llm"] += 1 + return simple + + # cache lookup (include a light context fingerprint if present) + ctx_id = None + if context: + ctx_id = context.get("channel_id") or context.get("thread_id") or "" + if not ctx_id: + ctx_id = None + + cached = get_cached_by_normalized(normalized, ctx_id, settings.classification_agent_model, {"temperature": 0.1}) + if cached is not None: + metrics["cache_hits"] += 1 + return cached + + metrics["cache_misses"] += 1 + triage_prompt = DEVREL_TRIAGE_PROMPT.format( message=message, context=context or 'No additional context' ) - response = await self.llm.ainvoke([HumanMessage(content=triage_prompt)]) - + # Use semaphore to limit concurrent LLM calls + async with _LLM_SEMAPHORE: + response = await self.llm.ainvoke([HumanMessage(content=triage_prompt)]) response_text = response.content.strip() + if '{' in response_text: json_start = response_text.find('{') json_end = response_text.rfind('}') + 1 json_str = response_text[json_start:json_end] - - import json result = json.loads(json_str) - return { + payload = { "needs_devrel": result.get("needs_devrel", True), "priority": result.get("priority", "medium"), "reasoning": result.get("reasoning", "LLM classification"), "original_message": message } + set_cached_by_normalized(normalized, ctx_id, settings.classification_agent_model, {"temperature": 0.1}, payload) + return payload return self._fallback_triage(message) diff --git a/frontend/src/components/pages/index.ts b/frontend/src/components/pages/index.ts deleted file mode 100644 index 43a11610..00000000 --- a/frontend/src/components/pages/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// export { default as Dashboard } from '../components/dashboard/Dashboard'; -// export { default as BotIntegrationPage } from '../components/integration/BotIntegrationPage'; -// export { default as ContributorsPage } from '../components/contributors/ContributorsPage'; -// export { default as PullRequestsPage } from './PullRequestsPage'; \ No newline at end of file diff --git a/landing/package-lock.json b/landing/package-lock.json index 7c4191de..8ed81366 100644 --- a/landing/package-lock.json +++ b/landing/package-lock.json @@ -8,6 +8,10 @@ "name": "devrai-landing", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.7", + "@mui/material": "^7.3.7", "@studio-freight/lenis": "^1.0.42", "framer-motion": "^10.16.4", "lucide-react": "^0.344.0", @@ -63,7 +67,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -119,7 +122,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.27.0", @@ -153,7 +155,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -195,7 +196,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -205,7 +205,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -239,7 +238,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.0" @@ -283,11 +281,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -302,7 +308,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -321,7 +326,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -331,7 +335,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -341,6 +344,62 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -358,6 +417,120 @@ "license": "MIT", "optional": true }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -897,7 +1070,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -912,7 +1084,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -922,7 +1093,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -932,20 +1102,251 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz", + "integrity": "sha512-8jWwS6FweMkpyRkrJooamUGe1CQfO1yJ+lM43IyUJbrhHW/ObES+6ry4vfGi8EKaldHL3t3BG1bcLcERuJPcjg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.7.tgz", + "integrity": "sha512-3Q+ulAqG+A1+R4ebgoIs7AccaJhIGy+Xi/9OnvX376jQ6wcy+rz4geDGrxQxCGzdjOQr4Z3NgyFSZCz4T999lA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.7", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.7.tgz", + "integrity": "sha512-6bdIxqzeOtBAj2wAsfhWCYyMKPLkRO9u/2o5yexcL0C3APqyy91iGSWgT3H7hg+zR2XgE61+WAu12wXPON8b6A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.7", + "@mui/system": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.7", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.7.tgz", + "integrity": "sha512-w7r1+CYhG0syCAQUWAuV5zSaU2/67WA9JXUderdb7DzCIJdp/5RmJv6L85wRjgKCMsxFF0Kfn0kPgPbPgw/jdw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.7", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.7.tgz", + "integrity": "sha512-y/QkNXv6cF6dZ5APztd/dFWfQ6LHKPx3skyYO38YhQD4+Cxd6sFAL3Z38WMSSC8LQz145Mpp3CcLrSCLKPwYAg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", + "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.7", + "@mui/styled-engine": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.10.tgz", + "integrity": "sha512-0+4mSjknSu218GW3isRqoxKRTOrTLd/vHi/7UC4+wZcUrOAqD9kRk7UQRL1mcrzqRoe7s3UT6rsRpbLkW5mHpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -995,6 +1396,16 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@studio-freight/lenis": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/@studio-freight/lenis/-/lenis-1.0.42.tgz", @@ -1060,18 +1471,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1088,6 +1503,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -1848,6 +2272,21 @@ "postcss": "^8.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1929,7 +2368,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2021,6 +2459,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2074,6 +2521,31 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2103,16 +2575,15 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2173,6 +2644,16 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2194,6 +2675,15 @@ "dev": true, "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -2246,7 +2736,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2529,6 +3018,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2649,7 +3144,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2767,7 +3261,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2776,6 +3269,21 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2790,7 +3298,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2832,6 +3339,12 @@ "dev": true, "license": "ISC" }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2849,7 +3362,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -2970,7 +3482,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2986,6 +3497,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3054,7 +3571,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -3162,7 +3678,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -3234,7 +3749,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3321,7 +3835,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3330,6 +3843,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3364,7 +3895,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -3395,7 +3925,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3405,7 +3934,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3601,6 +4129,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3674,6 +4219,12 @@ "react-dom": ">=16" } }, + "node_modules/react-is": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3724,6 +4275,22 @@ "react-dom": ">=18" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3751,7 +4318,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -3772,7 +4338,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3918,6 +4483,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4038,6 +4612,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4125,7 +4705,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/landing/package.json b/landing/package.json index 239c29b0..331595e0 100644 --- a/landing/package.json +++ b/landing/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.3.7", + "@mui/material": "^7.3.7", "@studio-freight/lenis": "^1.0.42", "framer-motion": "^10.16.4", "lucide-react": "^0.344.0", diff --git a/landing/src/components/layout/Footer.tsx b/landing/src/components/layout/Footer.tsx index 4d54c4d1..2cfa5e21 100644 --- a/landing/src/components/layout/Footer.tsx +++ b/landing/src/components/layout/Footer.tsx @@ -1,76 +1,448 @@ import React from 'react'; -import { Github, Twitter, Linkedin } from 'lucide-react'; -import { useLocation, Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { + Box, + Container, + Typography, + Link as MuiLink, + IconButton, + Divider, + Stack +} from '@mui/material'; +import { + GitHub as GitHubIcon, + Twitter as TwitterIcon, + LinkedIn as LinkedInIcon, + KeyboardArrowUp as ArrowUpIcon +} from '@mui/icons-material'; +import { useLocation, Link, useNavigate } from 'react-router-dom'; const Footer: React.FC = () => { const currentYear = new Date().getFullYear(); const location = useLocation(); const isHomePage = location.pathname === '/'; + const navigate = useNavigate(); + + // QuickLink component to handle hash navigation/smooth scroll via router + const QuickLink: React.FC<{ link: { label: string; href: string }; isHomePage: boolean }> = ({ link, isHomePage }) => { + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + + // Construct target href (preserve hash). On non-home pages we prefix with '/' + const target = isHomePage ? link.href : `/${link.href}`; + + // Navigate so location.hash updates; scrolling is handled by Navbar's useEffect + navigate(target); + }; + + return ( + handleClick(e)} + className="text-gray-400 hover:text-green-400 text-sm transition-colors duration-300 inline-flex items-center gap-2 group" + sx={{ + color: 'rgba(161, 161, 170, 1)', + fontSize: '0.875rem', + textDecoration: 'none', + display: 'inline-flex', + alignItems: 'center', + gap: '8px', + position: 'relative', + transition: 'color 0.3s ease', + '&:hover': { + color: 'rgba(34, 197, 94, 1)', + }, + '&::before': { + content: '""', + width: 0, + height: '2px', + background: 'linear-gradient(to right, #4ade80, #22d3ee)', + transition: 'width 0.3s ease', + position: 'absolute', + left: 0, + bottom: '-2px', + }, + '&:hover::before': { + width: '16px', + }, + }} + > + {link.label} + + ); + }; + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, + }; + + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, + }; + + const socialLinks = [ + { + icon: , + href: 'https://github.com/AOSSIE-Org/Devr.AI/', + label: 'GitHub', + color: 'hover:text-green-400' + }, + { + icon: , + href: 'https://x.com/aossie_org?lang=en', + label: 'Twitter', + color: 'hover:text-cyan-400' + }, + { + icon: , + href: 'https://www.linkedin.com/company/aossie/?originalSubdomain=au', + label: 'LinkedIn', + color: 'hover:text-green-400' + }, + ]; + + const quickLinks = [ + { label: 'Features', href: '#features' }, + { label: 'How It Works', href: '#how-it-works' }, + { label: 'Integrations', href: '#integrations' }, + { label: 'Join Waitlist', href: '#waitlist' }, + ]; + + const legalLinks = [ + { label: 'Privacy Policy', to: '/privacy-policy' }, + { label: 'Terms of Service', to: '/terms-of-service' }, + ]; + return ( -
- {/* Background gradient overlays */} -
-
-
-
-
- -
-
-
-

Devr.AI

-

- Revolutionizing developer relations with AI-powered community management. -

-
-
- -
-

Legal

-
    -
  • Privacy Policy
  • -
  • Terms of Service
  • -
-
-
-
- -
-

- © {currentYear} Devr.AI. All rights reserved. -

- -
-
-
+ + {/* Enhanced Background Effects */} + + + + + + + + + + + {/* Brand Section */} + + + + Devr.AI + + + Revolutionizing developer relations with AI-powered community management. + Automate engagement, streamline onboarding, and deliver real-time updates. + + + {socialLinks.map((social) => ( + + + {social.icon} + + + ))} + + + + + {/* Quick Links Section */} + + + + Quick Links + + + {quickLinks.map((link) => ( + + ))} + + + + + {/* Legal Section */} + + + + Legal + + + {legalLinks.map((link) => ( + + {link.label} + + ))} + + + + + + {/* Bottom Bar */} + + + + + © {currentYear} Devr.AI. All rights reserved. + + + {/* Scroll to Top Button */} + + + + Back to top + + + + + + + + + ); }; diff --git a/landing/src/components/layout/Navbar.tsx b/landing/src/components/layout/Navbar.tsx index 42a0a744..cd32b695 100644 --- a/landing/src/components/layout/Navbar.tsx +++ b/landing/src/components/layout/Navbar.tsx @@ -1,12 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Menu, X, Github } from 'lucide-react'; +import { Menu, X} from 'lucide-react'; import { motion } from 'framer-motion'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const location = useLocation(); + const navigate = useNavigate(); const isHomePage = location.pathname === '/'; useEffect(() => { @@ -20,6 +21,44 @@ const Navbar: React.FC = () => { }; }, []); + // Handle smooth scroll on page load with hash (consolidated and cleaned up) + useEffect(() => { + let timer: ReturnType | null = null; + + if (isHomePage && location.hash) { + const hash = location.hash.substring(1); // Remove the # symbol + timer = setTimeout(() => { + const element = document.getElementById(hash); + if (element) { + const offset = 80; // Account for fixed navbar height + const elementPosition = element.getBoundingClientRect().top + window.pageYOffset; + const offsetPosition = elementPosition - offset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + } + }, 100); + } + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [location.hash, isHomePage]); + + // Smooth scroll handler - let routing update the hash and useEffect handle scrolling + const handleSmoothScroll = (e:React.MouseEvent, href: string) => { + e.preventDefault(); + // Do not prevent default navigation; close mobile menu then navigate so location.hash updates + setIsOpen(false); + + // Navigate to the link (includes hash when present). The useEffect above will handle scrolling. + navigate(href); + }; + const navLinks = [ { name: 'Features', href: isHomePage ? '#features' : '/#features' }, { name: 'How It Works', href: isHomePage ? '#how-it-works' : '/#how-it-works' }, @@ -39,7 +78,17 @@ const Navbar: React.FC = () => { className="flex items-center" > - Devr.AI + + Devr.AI + @@ -52,6 +101,7 @@ const Navbar: React.FC = () => { handleSmoothScroll(e, link.href)} className="text-sm font-medium text-gray-300 transition-colors hover:text-white" > {link.name} @@ -59,6 +109,7 @@ const Navbar: React.FC = () => { ))} handleSmoothScroll(e, isHomePage ? "#waitlist" : "/#waitlist")} className="text-sm font-medium px-4 py-2 rounded-lg bg-primary hover:bg-primary-hover text-white transition-colors" > Join Waitlist @@ -87,7 +138,7 @@ const Navbar: React.FC = () => { setIsOpen(false)} + onClick={(e) => handleSmoothScroll(e, link.href)} className="block py-3 text-gray-300 hover:text-white" > {link.name} @@ -95,7 +146,7 @@ const Navbar: React.FC = () => { ))} setIsOpen(false)} + onClick={(e) => handleSmoothScroll(e, isHomePage ? "#waitlist" : "/#waitlist")} className="block py-3 text-primary hover:text-primary-hover font-medium" > Join Waitlist diff --git a/landing/src/components/sections/Hero.tsx b/landing/src/components/sections/Hero.tsx index 42a120c7..ba3c1672 100644 --- a/landing/src/components/sections/Hero.tsx +++ b/landing/src/components/sections/Hero.tsx @@ -1,6 +1,17 @@ import React from 'react'; import { motion } from 'framer-motion'; -import { Users } from 'lucide-react'; +import { + Box, + Container, + Typography, + Button, + Chip, + Stack +} from '@mui/material'; +import { + People as UsersIcon, + Explore as ExploreIcon +} from '@mui/icons-material'; const Hero: React.FC = () => { const container = { @@ -19,81 +30,359 @@ const Hero: React.FC = () => { }; return ( -
- {/* Background elements */} -
-
+ + {/* Simplified Background - Modern & Clean */} + + {/* Base gradient */} + -
-
-
+ {/* Subtle accent gradients with blue */} + + + {/* Additional blue accent */} + -
+ {/* Grid pattern for texture */} + -
+ {/* Top gradient fade with blue */} + + -
-
+ {/* Bottom fade for smooth transition */} + -
- -
- + - - - Developer Experience Reimagined - - - - AI-Powered Developer Relations Assistant - + + - + AI-Powered{' '} + + Developer Relations + {' '} + Assistant + + + {/* Description - Better spacing and readability */} + Devr.AI revolutionizes open-source community management by automating engagement, streamlining onboarding, and delivering real-time project updates across Discord, - Slack, GitHub, CLI, Web Widget, and more.
- Personalized, multi-platform, and analytics-driven. -
- + + Personalized, multi-platform, and analytics-driven. + + + + {/* CTAs - Enhanced styling */} + -
- + + + - -
-
- + {/* Subtle glow effect with blue */} + + + {/* Bottom fade */} + + + -
-
- -
-
+
+
+
+ +
); }; diff --git a/landing/src/components/sections/Waitlist.tsx b/landing/src/components/sections/Waitlist.tsx index 5bf3f2b7..9808e924 100644 --- a/landing/src/components/sections/Waitlist.tsx +++ b/landing/src/components/sections/Waitlist.tsx @@ -1,13 +1,33 @@ import React, { useState } from 'react'; import { motion } from 'framer-motion'; import { toast } from 'react-hot-toast'; -import { Send, AlertCircle, CheckCircle, Sparkles } from 'lucide-react'; +import { + Box, + Container, + Typography, + TextField, + Select, + SelectChangeEvent, + MenuItem, + FormControl, + InputLabel, + Button, + Alert, + CircularProgress, + Stack, + Paper +} from '@mui/material'; +import { + Send as SendIcon, + CheckCircle as CheckCircleIcon, + AutoAwesome as SparklesIcon +} from '@mui/icons-material'; const Waitlist: React.FC = () => { const [email, setEmail] = useState(''); const [name, setName] = useState(''); const [organization, setOrganization] = useState(''); - const [role, setRole] = useState(''); + const [role, setRole] = useState(''); const [suggestions, setSuggestions] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -67,13 +87,13 @@ const Waitlist: React.FC = () => { setSubmitted(true); toast.success('You have been added to the waitlist!'); - + setEmail(''); setName(''); setOrganization(''); setRole(''); setSuggestions(''); - + } catch (err) { const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMessage); @@ -84,222 +104,634 @@ const Waitlist: React.FC = () => { }; return ( -
- {/* Background decoration */} -
-
- -
-
+ + {/* Background decoration with blue */} + + + {/* Additional blue accent */} + + + + {/* Gradient border container */} -
-
+ + -
- -

- Join the Waitlist -

- -
-

+ + + + Join the{' '} + + Waitlist + + + + + Be among the first to experience Devr.AI and revolutionize your open-source community management. -

+
{submitted ? ( -
-
-
- -
-

Welcome to the future!

-

- You're now on our exclusive waitlist. We'll notify you the moment early access becomes available. -

-
-

- 🎉 Keep an eye on your inbox for exciting updates! -

-
-
+ + + + + Welcome to the future! + + + You're now on our exclusive waitlist. We'll notify you the moment early access becomes available. + + + + 🎉 Keep an eye on your inbox for exciting updates! + + + + ) : ( - {error && ( -
-
- -

{error}

-
+ + {error} + )} -
-
- -
-
- setName(e.target.value)} - required - placeholder="Enter your full name" - className="relative w-full px-4 py-4 bg-dark-card/80 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 text-white placeholder-gray-500 backdrop-blur-sm transition-all duration-300 hover:border-gray-600" - /> -
-
+ + setName(e.target.value)} + required + placeholder="Enter your full name" + fullWidth + className="relative group" + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'rgba(39, 39, 42, 0.8)', + backdropFilter: 'blur(8px)', + color: 'white', + borderRadius: '12px', + border: '1px solid rgba(63, 63, 70, 1)', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: 'rgba(82, 82, 91, 1)', + }, + '&.Mui-focused': { + borderColor: 'rgba(34, 197, 94, 0.5)', + boxShadow: '0 0 0 2px rgba(34, 197, 94, 0.1)', + }, + '& fieldset': { + border: 'none', + }, + }, + '& .MuiInputLabel-root': { + color: 'rgba(209, 213, 219, 1)', + fontWeight: 600, + fontSize: '0.875rem', + '&.Mui-focused': { + color: 'rgba(34, 197, 94, 1)', + }, + }, + '& .MuiInputBase-input': { + color: 'white', + '&::placeholder': { + color: 'rgba(113, 113, 122, 1)', + opacity: 1, + }, + }, + }} + /> -
- -
-
- setEmail(e.target.value)} - required - placeholder="your@email.com" - className="relative w-full px-4 py-4 bg-dark-card/80 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 text-white placeholder-gray-500 backdrop-blur-sm transition-all duration-300 hover:border-gray-600" - /> -
-
-
+ setEmail(e.target.value)} + required + placeholder="your@email.com" + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'rgba(39, 39, 42, 0.8)', + backdropFilter: 'blur(8px)', + color: 'white', + borderRadius: '12px', + border: '1px solid rgba(63, 63, 70, 1)', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: 'rgba(82, 82, 91, 1)', + }, + '&.Mui-focused': { + borderColor: 'rgba(34, 197, 94, 0.5)', + boxShadow: '0 0 0 2px rgba(34, 197, 94, 0.1)', + }, + '& fieldset': { + border: 'none', + }, + }, + '& .MuiInputLabel-root': { + color: 'rgba(209, 213, 219, 1)', + fontWeight: 600, + fontSize: '0.875rem', + '&.Mui-focused': { + color: 'rgba(34, 197, 94, 1)', + }, + }, + '& .MuiInputBase-input': { + color: 'white', + '&::placeholder': { + color: 'rgba(113, 113, 122, 1)', + opacity: 1, + }, + }, + }} + /> +
-
- -
-
- setOrganization(e.target.value)} - required - placeholder="Your company, university, or organization" - className="relative w-full px-4 py-4 bg-dark-card/80 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 text-white placeholder-gray-500 backdrop-blur-sm transition-all duration-300 hover:border-gray-600" - /> -
-
+ setOrganization(e.target.value)} + required + placeholder="Your company, university, or organization" + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'rgba(39, 39, 42, 0.8)', + backdropFilter: 'blur(8px)', + color: 'white', + borderRadius: '12px', + border: '1px solid rgba(63, 63, 70, 1)', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: 'rgba(82, 82, 91, 1)', + }, + '&.Mui-focused': { + borderColor: 'rgba(59, 130, 246, 0.5)', + boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.15), 0 0 0 4px rgba(34, 197, 94, 0.1)', + }, + '& fieldset': { + border: 'none', + }, + }, + '& .MuiInputLabel-root': { + color: 'rgba(209, 213, 219, 1)', + fontWeight: 600, + fontSize: '0.875rem', + '&.Mui-focused': { + color: 'rgba(59, 130, 246, 1)', + }, + }, + '& .MuiInputBase-input': { + color: 'white', + '&::placeholder': { + color: 'rgba(113, 113, 122, 1)', + opacity: 1, + }, + }, + }} + /> -
- -
-
- -
-
+ + Your Role + + + + + Suggestions & Comments{' '} + + (optional) + + + } + multiline + rows={4} + value={suggestions} + onChange={(e) => setSuggestions(e.target.value)} + placeholder="Share your thoughts, feature requests, or any feedback..." + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'rgba(39, 39, 42, 0.8)', + backdropFilter: 'blur(8px)', + color: 'white', + borderRadius: '12px', + border: '1px solid rgba(63, 63, 70, 1)', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: 'rgba(82, 82, 91, 1)', + }, + '&.Mui-focused': { + borderColor: 'rgba(59, 130, 246, 0.5)', + boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.15), 0 0 0 4px rgba(34, 197, 94, 0.1)', + }, + '& fieldset': { + border: 'none', + }, + }, + '& .MuiInputLabel-root': { + color: 'rgba(209, 213, 219, 1)', + fontWeight: 600, + fontSize: '0.875rem', + '&.Mui-focused': { + color: 'rgba(59, 130, 246, 1)', + }, + }, + '& .MuiInputBase-input': { + color: 'white', + '&::placeholder': { + color: 'rgba(113, 113, 122, 1)', + opacity: 1, + }, + }, + }} + /> + + + {/* Enhanced Button with Premium Design */} + + {/* Animated gradient border */} + -
- -
-
-