diff --git a/hexa-revanth/app/admin/page.jsx b/app/admin/page.jsx similarity index 100% rename from hexa-revanth/app/admin/page.jsx rename to app/admin/page.jsx diff --git a/hexa-revanth/app/favicon.ico b/app/favicon.ico similarity index 100% rename from hexa-revanth/app/favicon.ico rename to app/favicon.ico diff --git a/hexa-revanth/app/fonts/GeistMonoVF.woff b/app/fonts/GeistMonoVF.woff similarity index 100% rename from hexa-revanth/app/fonts/GeistMonoVF.woff rename to app/fonts/GeistMonoVF.woff diff --git a/hexa-revanth/app/fonts/GeistVF.woff b/app/fonts/GeistVF.woff similarity index 100% rename from hexa-revanth/app/fonts/GeistVF.woff rename to app/fonts/GeistVF.woff diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..9ef7d6d --- /dev/null +++ b/app/globals.css @@ -0,0 +1,349 @@ +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap'); +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ============================================ + BLACK BACKGROUND - MUST BE AFTER @tailwind + ============================================ */ +html { + background-color: #000000 !important; +} + +body { + background-color: #000000 !important; + color: #ffffff; + font-family: var(--font-inter), sans-serif; + overflow-x: hidden; + min-height: 100vh; + margin: 0; + padding: 0; +} + +/* === Material Icons === */ +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +::selection { + background: #B6B2A5; + color: #000000; +} + +/* === Glassmorphism === */ +.glass-panel { + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +/* === Accent Glow === */ +.accent-glow { + filter: drop-shadow(0 0 15px rgba(255, 198, 0, 0.3)); +} + +/* === Scrollbar === */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #111111; +} + +::-webkit-scrollbar-thumb { + background: #333333; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: #B6B2A5; +} + +/* ============================================ + NEW ROADMAP STYLES + ============================================ */ + +/* ------------------------------------------ + ROADMAP LAYOUT — THIS WAS MISSING + .roadmap-section / .roadmap-canvas-container + were referenced in RoadmapSection.jsx but + never defined anywhere, so the container + collapsed to near-zero height. + ------------------------------------------ */ + +.roadmap-section { + position: relative; + width: 100%; + padding: 4rem 1.5rem; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; +} + +.roadmap-header { + position: relative; + z-index: 10; + text-align: center; + max-width: 800px; + margin-bottom: 2rem; +} + +.roadmap-header-tag { + display: inline-block; + font-size: 0.75rem; + letter-spacing: 0.15em; + color: #B6B2A5; + text-transform: uppercase; + margin-bottom: 0.75rem; +} + +.roadmap-header-title { + font-size: clamp(2rem, 5vw, 3.5rem); + font-weight: 700; + line-height: 1.1; + margin-bottom: 1rem; +} + +.roadmap-header-subtitle { + color: rgba(255, 255, 255, 0.6); + font-size: 1rem; + line-height: 1.5; +} + +.roadmap-canvas-container { + position: relative; + width: 100%; + max-width: 1400px; + height: 70vh; + min-height: 500px; + margin-top: 2rem; +} + +.roadmap-legend { + position: absolute; + top: 1.5rem; + right: 1.5rem; + z-index: 20; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 220px; +} + +.roadmap-legend-item { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid transparent; + cursor: pointer; + text-align: left; +} + +.roadmap-legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: block; + background: rgba(255, 255, 255, 0.3); +} + +.roadmap-legend-completed .roadmap-legend-dot { + background: #22c55e; +} + +.roadmap-legend-current .roadmap-legend-dot { + background: #B6B2A5; +} + +.roadmap-legend-locked .roadmap-legend-dot { + background: rgba(255, 255, 255, 0.25); +} + +.roadmap-legend-active { + border-color: rgba(255, 198, 0, 0.4); +} + +.roadmap-legend-phase { + font-size: 0.65rem; + letter-spacing: 0.05em; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.roadmap-legend-name { + font-size: 0.85rem; + color: #ffffff; +} + +.roadmap-loader { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; + letter-spacing: 0.1em; +} + +.roadmap-loader-ring { + width: 32px; + height: 32px; + border: 2px solid rgba(255, 198, 0, 0.15); + border-top-color: #B6B2A5; + border-radius: 50%; +} + +@media (max-width: 768px) { + .roadmap-canvas-container { + height: 60vh; + min-height: 400px; + } + + .roadmap-legend { + position: static; + flex-direction: row; + flex-wrap: wrap; + max-width: 100%; + margin-top: 1rem; + } +} + +@media (max-width: 480px) { + .roadmap-canvas-container { + height: 50vh; + min-height: 350px; + } +} + +/* Carbon fiber background pattern */ +.roadmap-bg-carbon { + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.02) 2px, + rgba(255, 255, 255, 0.02) 4px + ); +} + +/* Scanline overlay */ +.roadmap-scanlines { + background-image: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.05) 2px, + rgba(255, 255, 255, 0.05) 4px + ); +} + +/* Roadmap card transitions */ +.roadmap-card { + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.roadmap-card:hover { + transform: translateY(-4px); +} + +/* Active phase pulse animation */ +@keyframes roadmap-pulse { + 0%, 100% { + box-shadow: 0 0 20px rgba(255, 198, 0, 0.1); + } + 50% { + box-shadow: 0 0 40px rgba(255, 198, 0, 0.2); + } +} + +.roadmap-active-phase { + animation: roadmap-pulse 2s ease-in-out infinite; +} + +/* Subtask scrollbar */ +.roadmap-subtasks::-webkit-scrollbar { + width: 4px; +} + +.roadmap-subtasks::-webkit-scrollbar-track { + background: transparent; +} + +.roadmap-subtasks::-webkit-scrollbar-thumb { + background: rgba(255, 198, 0, 0.3); + border-radius: 2px; +} + +/* Progress bar shimmer */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.roadmap-progress-shimmer { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 198, 0, 0.3) 50%, + transparent 100% + ); + background-size: 200% 100%; + animation: shimmer 2s infinite; +} + +/* ============================================ + RESPONSIVE ROADMAP + ============================================ */ + +@media (max-width: 768px) { + .roadmap-phase-number { + font-size: 3rem; + } + + .roadmap-phase-icon { + font-size: 2rem; + } +} + +@media (max-width: 480px) { + .roadmap-phase-number { + font-size: 2rem; + } + + .roadmap-phase-icon { + font-size: 1.5rem; + } +} + +/* ============================================ + ANIMATIONS + ============================================ */ + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes pulse-dot { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.5; + transform: scale(1.4); + } +} \ No newline at end of file diff --git a/app/layout.js b/app/layout.js new file mode 100644 index 0000000..cab4dd5 --- /dev/null +++ b/app/layout.js @@ -0,0 +1,23 @@ +import "./globals.css"; +import PantherAnimationWrapper from "@/components/PantherAnimationWrapper"; + +export const metadata = { + title: "HEXAWATTS RACING TEAM | ENGINEERED FOR SPEED", + description: + "Hexawatts Racing Team — Engineering India's fastest collegiate electric racing machines. Precision, telemetry, and uncompromising performance.", +}; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +} \ No newline at end of file diff --git a/hexa-revanth/app/page.js b/app/page.js similarity index 94% rename from hexa-revanth/app/page.js rename to app/page.js index 11997bc..970c013 100644 --- a/hexa-revanth/app/page.js +++ b/app/page.js @@ -9,6 +9,8 @@ import Performance from "@/components/Performance"; import Roadmap from "@/components/Roadmap"; import Footer from "@/components/Footer"; import ScrollReveal from "@/components/ScrollReveal"; +import PantherAnimation from '@/components/PantherAnimation'; + export default function Home() { return ( diff --git a/hexa-revanth/components/CarSpecs2D.jsx b/components/CarSpecs2D.jsx similarity index 100% rename from hexa-revanth/components/CarSpecs2D.jsx rename to components/CarSpecs2D.jsx diff --git a/hexa-revanth/components/ContributionModal.jsx b/components/ContributionModal.jsx similarity index 100% rename from hexa-revanth/components/ContributionModal.jsx rename to components/ContributionModal.jsx diff --git a/components/Crowdfunding.jsx b/components/Crowdfunding.jsx new file mode 100644 index 0000000..fc85c7c --- /dev/null +++ b/components/Crowdfunding.jsx @@ -0,0 +1,326 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import supabase from "@/lib/supabase"; +import { motion, AnimatePresence } from "framer-motion"; + +const TOTAL_SEGMENTS = 30; + +export default function Crowdfunding() { + const [stats, setStats] = useState({ + targetGoal: 3000000, + raised: 0, + supporters: 0, + campaignId: null, + }); + const [prevRaised, setPrevRaised] = useState(null); + const [isLiveUpdating, setIsLiveUpdating] = useState(false); + const [showContactForm, setShowContactForm] = useState(false); + const [captains, setCaptains] = useState([]); + const channelRef = useRef(null); + + const [formData, setFormData] = useState({ + name: "", + number: "", + email: "", + message: "", + }); + const [formSubmitted, setFormSubmitted] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + useEffect(() => { + async function fetchCrowdfundingData() { + const { data: campaign } = await supabase + .from("campaigns") + .select("*") + .eq("is_active", true) + .single(); + + if (campaign) { + const { data: supportersData } = await supabase + .from("donations") + .select("donor_email") + .eq("campaign_id", campaign.id) + .eq("status", "SUCCESS"); + + let supportersCount = 0; + if (supportersData) { + const uniqueEmails = new Set( + supportersData.map(d => d.donor_email).filter(email => email && email.trim() !== '') + ); + supportersCount = uniqueEmails.size; + } + + setStats({ + targetGoal: Math.round(campaign.goal_amount_paise / 100), + raised: Math.round(campaign.total_raised_paise / 100), + supporters: supportersCount, + campaignId: campaign.id, + }); + } + + const { data: captainsData } = await supabase + .from("captains") + .select("*") + .order("display_order"); + + if (captainsData) setCaptains(captainsData); + } + + fetchCrowdfundingData(); + }, []); + + useEffect(() => { + // Empty deps: subscribe ONCE. The original code had [stats.raised] which + // caused a re-subscribe on every database update, churning the channel and + // dropping events. Use functional setState to read the previous value. + const campaignChannel = supabase + .channel("campaign-live-updates") + .on("postgres_changes", { event: "UPDATE", schema: "public", table: "campaigns" }, + async (payload) => { + const newData = payload.new; + setStats(prev => { + setPrevRaised(prev.raised); + return { + ...prev, + targetGoal: Math.round(newData.goal_amount_paise / 100), + raised: Math.round(newData.total_raised_paise / 100), + }; + }); + const { data: supportersData } = await supabase.from("donations").select("donor_email").eq("campaign_id", newData.id).eq("status", "SUCCESS"); + let supportersCount = 0; + if (supportersData) { + const uniqueEmails = new Set(supportersData.map(d => d.donor_email).filter(email => email && email.trim() !== '')); + supportersCount = uniqueEmails.size; + } + setStats(prev => ({ ...prev, supporters: supportersCount })); + setIsLiveUpdating(true); + setTimeout(() => setIsLiveUpdating(false), 2000); + } + ).subscribe(); + + const donationChannel = supabase + .channel("donation-live-updates") + .on("postgres_changes", { event: "UPDATE", schema: "public", table: "donations", filter: "status=eq.SUCCESS" }, + async (payload) => { + const newDonation = payload.new; + const { data: supportersData } = await supabase.from("donations").select("donor_email").eq("campaign_id", newDonation.campaign_id).eq("status", "SUCCESS"); + let supportersCount = 0; + if (supportersData) { + const uniqueEmails = new Set(supportersData.map(d => d.donor_email).filter(email => email && email.trim() !== '')); + supportersCount = uniqueEmails.size; + } + setStats(prev => ({ ...prev, supporters: supportersCount })); + } + ).subscribe(); + + channelRef.current = { campaignChannel, donationChannel }; + return () => { + if (channelRef.current) { + supabase.removeChannel(channelRef.current.campaignChannel); + supabase.removeChannel(channelRef.current.donationChannel); + } + }; + }, []); // Empty deps: subscribe once. Re-subscribing on every raised update would cause channel thrash. + + const percentageRaw = stats.targetGoal > 0 ? (stats.raised / stats.targetGoal) * 100 : 0; + const PERCENTAGE = Math.min(100, Math.max(0, Math.round(percentageRaw))); + const ACTIVE_SEGMENTS = Math.round((PERCENTAGE / 100) * TOTAL_SEGMENTS); + const justAdded = prevRaised ? stats.raised - prevRaised : 0; + const remaining = stats.targetGoal - stats.raised; + + const formatCurrency = (amount) => { + return new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 }).format(amount); + }; + + const handleFormChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleFormSubmit = async (e) => { + e.preventDefault(); + setIsSubmitting(true); + setSubmitError(null); + try { + const { error } = await supabase.from('contact_submissions').insert({ + name: formData.name, phone: formData.number, email: formData.email, message: formData.message || null, + }); + if (error) throw error; + setFormSubmitted(true); + setShowContactForm(false); + setFormData({ name: "", number: "", email: "", message: "" }); + setTimeout(() => setFormSubmitted(false), 4000); + } catch (err) { + setSubmitError('Something went wrong. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + const captain = captains.find(c => c.role?.toLowerCase().includes('captain') && !c.role?.toLowerCase().includes('vice')); + const viceCaptain = captains.find(c => c.role?.toLowerCase().includes('vice')); + + return ( +
+
+
+ + + {isLiveUpdating && ( + + + LIVE UPDATE + + )} + + + + {formSubmitted && ( + + + + + + Thank you! We'll get back to you soon. + + + )} + + +
+ + +
+
+ + Crowdfunding Initiative +
+
+ + + + WE ARE ACCEPTING{" "} + CROWDFUNDING + + + + Our journey to the podium isn't just about engineering—it's about the community that fuels our ambition. + + + {/* Stats Cards */} + +
+

Target Goal

+

{formatCurrency(stats.targetGoal)}

+
+ +
+ + {isLiveUpdating && } + +

Raised

+ + {formatCurrency(stats.raised)} + + + {isLiveUpdating && justAdded > 0 && +{formatCurrency(justAdded)} just now!} + +
+ +
+

Supporters

+ + {stats.supporters} + +
+
+ + {/* Progress Bar */} + +
+
+

Progress

+
+ {PERCENTAGE} + % +
+
+
+

Remaining

+

{formatCurrency(remaining)}

+
+
+
+ {Array.from({ length: TOTAL_SEGMENTS }, (_, i) => ( + + ))} +
+
0%50%100%
+
+ + {/* Contact Us */} + + {!showContactForm ? ( + setShowContactForm(true)} className="group relative inline-flex items-center gap-2 sm:gap-3 bg-[#42AACC] text-black px-6 sm:px-8 md:px-10 py-4 sm:py-5 rounded-2xl font-black text-base sm:text-lg md:text-xl uppercase tracking-wider overflow-hidden shadow-[0_8px_30px_rgba(0,200,224,0.3)] hover:shadow-[0_12px_40px_rgba(0,200,224,0.5)] transition-all duration-300 w-full sm:w-auto"> + + + + CONTACT US + + + ) : ( + +
+
+
+

Get In Touch

+ +
+
+
+
+
+