diff --git a/frontend/src/components/contact/ContactChannelCards.jsx b/frontend/src/components/contact/ContactChannelCards.jsx new file mode 100644 index 0000000..328c457 --- /dev/null +++ b/frontend/src/components/contact/ContactChannelCards.jsx @@ -0,0 +1,58 @@ +import { Mail, Bug, Lightbulb, ArrowUpRight } from "lucide-react"; + +const channels = [ + { + icon: Mail, + title: "Email", + description: "Fire off an email and we'll get back to you — fast.", + href: "mailto:hello@codelens.dev", + }, + { + icon: Bug, + title: "GitHub Issues", + description: "Found a bug? Report it and we'll squash it.", + href: "https://github.com/kunalverma2512/CodeLens/issues", + }, + { + icon: Lightbulb, + title: "Feature Request", + description: "Got an idea? Drop it and help us shape the future.", + href: "https://github.com/kunalverma2512/CodeLens/discussions", + }, +]; + +export default function ContactChannelCards() { + return ( +
+
+
+ {channels.map((channel) => { + const Icon = channel.icon; + return ( + +
+
+ +
+ +
+

+ {channel.title} +

+

+ {channel.description} +

+
+ ); + })} +
+
+
+ ); +} diff --git a/frontend/src/components/contact/ContactFAQ.jsx b/frontend/src/components/contact/ContactFAQ.jsx new file mode 100644 index 0000000..7bf26d1 --- /dev/null +++ b/frontend/src/components/contact/ContactFAQ.jsx @@ -0,0 +1,101 @@ +import { useState, useCallback } from "react"; +import { ChevronDown } from "lucide-react"; + +const FAQS = [ + { + q: "Is CodeLens free to use?", + a: "Yes — 100% free and open source. No hidden fees, no restrictions. Developer tools should be accessible to everyone, period.", + }, + { + q: "How do I report a security vulnerability?", + a: "Found a flaw? Email security@codelens.dev or open a GitHub Security Advisory. Keep vulnerabilities private until we fix them.", + }, + { + q: "Can I contribute?", + a: "Absolutely. CodeLens is open source and we welcome contributors with open arms. Head to our GitHub repo for guidelines, open issues, and the roadmap.", + }, + { + q: "How do I request new integrations?", + a: "Open a feature request on GitHub. We listen to the community and prioritize what matters most to you.", + }, + { + q: "Where is my data stored?", + a: "On secure servers with enterprise-grade encryption. Your data stays yours. Check our privacy policy for the full breakdown.", + }, +]; + +export default function ContactFAQ() { + const [openIndex, setOpenIndex] = useState(null); + + const toggle = useCallback( + (index) => { + setOpenIndex((prev) => (prev === index ? null : index)); + }, + [], + ); + + return ( +
+
+
+

+ FAQ +

+

+ Frequently asked questions +

+
+ +
+ {FAQS.map((faq, index) => { + const isOpen = openIndex === index; + const panelId = `faq-panel-${index}`; + const buttonId = `faq-button-${index}`; + + return ( +
+ + +
+
+

+ {faq.a} +

+
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/frontend/src/components/contact/ContactForm.jsx b/frontend/src/components/contact/ContactForm.jsx new file mode 100644 index 0000000..2ac9f38 --- /dev/null +++ b/frontend/src/components/contact/ContactForm.jsx @@ -0,0 +1,378 @@ +import { useState, useRef, useCallback, useMemo } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Check } from "lucide-react"; +import ContactSuccessState from "./ContactSuccessState"; + +const CATEGORIES = ["Bug Report", "Feature Request", "Partnership", "General"]; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +const FIELD_DEFAULTS = { + General: { name: "", email: "", message: "" }, + "Bug Report": { + email: "", + platform: "", + stepsToReproduce: "", + expectedBehavior: "", + actualBehavior: "", + }, + "Feature Request": { + email: "", + platform: "", + description: "", + }, + Partnership: { company: "", email: "", proposal: "" }, +}; + +const FIELD_LABELS = { + General: { + name: "Name", + email: "Email", + message: "Message", + }, + "Bug Report": { + email: "Email", + platform: "Platform", + stepsToReproduce: "Steps to Reproduce", + expectedBehavior: "Expected Behavior", + actualBehavior: "Actual Behavior", + }, + "Feature Request": { + email: "Email", + platform: "Platform", + description: "Description", + }, + Partnership: { + company: "Company", + email: "Email", + proposal: "Proposal", + }, +}; + +const FIELD_TYPES = { + company: "text", + name: "text", + email: "email", + platform: "text", + message: "textarea", + stepsToReproduce: "textarea", + expectedBehavior: "textarea", + actualBehavior: "textarea", + description: "textarea", + proposal: "textarea", +}; + +const FIELD_MAX_LENGTH = { + name: 100, + company: 100, + platform: 100, + email: 254, + message: 2000, + stepsToReproduce: 2000, + expectedBehavior: 1000, + actualBehavior: 1000, + description: 2000, + proposal: 2000, +}; + +function validateEmail(email) { + return EMAIL_RE.test(email); +} + +function buildValidator() { + return function validateField(name, value) { + if (name === "email") { + if (!value) return "Email is required"; + if (!validateEmail(value)) return "Invalid email address"; + } + if (name === "name" && !value) return "Name is required"; + if (name === "company" && !value) return "Company is required"; + if (name === "platform" && !value) return "Platform is required"; + if (name === "message" && !value) return "Message is required"; + if (name === "stepsToReproduce" && !value) + return "Steps to reproduce is required"; + if (name === "expectedBehavior" && !value) + return "Expected behavior is required"; + if (name === "actualBehavior" && !value) + return "Actual behavior is required"; + if (name === "description" && !value) return "Description is required"; + + if (name === "proposal" && !value) return "Proposal is required"; + return null; + }; +} + +function isFormValid(category, values) { + const fields = Object.keys(FIELD_LABELS[category]); + for (const field of fields) { + const trimmed = (values[field] || "").trim(); + if (!trimmed) return false; + if (field === "email" && !validateEmail(trimmed)) return false; + } + return true; +} + +function AutoTextarea({ value, onChange, onBlur, maxLength, id, name, required, placeholder, hasError, hasSuccess }) { + const ref = useRef(null); + const handleInput = useCallback((e) => { + const el = e.target; + el.style.height = "auto"; + el.style.height = el.scrollHeight + "px"; + onChange(e); + }, [onChange]); + + return ( +