Spaces:
Sleeping
Sleeping
| const fs = require('fs'); | |
| let content = fs.readFileSync('src/components/landing/Sections.tsx', 'utf8'); | |
| // 1. Remove all em-dashes | |
| content = content.replace(/β/g, '-'); | |
| // 2. update useCountUp to accept decimals | |
| content = content.replace( | |
| `function useCountUp(target: number, duration = 1400) { | |
| const ref = useRef<HTMLSpanElement | null>(null) | |
| const [val, setVal] = useState(0)`, | |
| `function useCountUp(target: number, duration = 1400, decimals = 0) { | |
| const ref = useRef<HTMLSpanElement | null>(null) | |
| const [val, setVal] = useState(0)`); | |
| content = content.replace( | |
| ` const eased = 1 - Math.pow(1 - p, 3) | |
| setVal(Math.round(target * eased))`, | |
| ` const eased = 1 - Math.pow(1 - p, 3) | |
| const currentVal = target * eased | |
| setVal(decimals > 0 ? Number(currentVal.toFixed(decimals)) : Math.round(currentVal))`); | |
| // 3. update Stat to accept decimals | |
| content = content.replace( | |
| `function Stat({ value, label }: { value: number; label: string }) { | |
| const { ref, val } = useCountUp(value)`, | |
| `function Stat({ value, label, decimals = 0 }: { value: number; label: string; decimals?: number }) { | |
| const { ref, val } = useCountUp(value, 1400, decimals)`); | |
| content = content.replace( | |
| ` {val.toLocaleString()} | |
| </div>`, | |
| ` {decimals > 0 ? val.toFixed(decimals) : val.toLocaleString()} | |
| </div>`); | |
| // 4. HeroSection text modification | |
| content = content.replace( | |
| ` Know where to | |
| <br /> | |
| <span | |
| className="text-shimmer"`, | |
| ` You have been enforcing where the | |
| <br /> | |
| <span | |
| className="text-shimmer"`); | |
| content = content.replace( | |
| ` step next. | |
| </span>`, | |
| ` cameras already point. | |
| </span>`); | |
| content = content.replace( | |
| ` A living digital twin of Bengaluru's streets - surfacing | |
| illegal-parking hotspots before they choke the road. Built for | |
| the officers who decide where to look first.`, | |
| ` GridLoc separates where parking is recorded from where it actually happens, then hands BTP the gap.`); | |
| content = content.replace( | |
| ` <Stat value={2534} label="cells scored" /> | |
| <Stat value={338} label="flagged zones" /> | |
| <Stat value={11761} label="roads mapped" />`, | |
| ` <Stat value={292768} label="violations cleaned" /> | |
| <Stat value={2534} label="cells scored" /> | |
| <Stat value={5224} label="cell by time-block rows" />`); | |
| content = content.replace( | |
| ` Know where to | |
| <br /> | |
| <span | |
| className="text-shimmer"`, | |
| ` You have been enforcing where the | |
| <br /> | |
| <span | |
| className="text-shimmer"`); | |
| content = content.replace( | |
| ` step next. | |
| </span>`, | |
| ` cameras already point. | |
| </span>`); | |
| content = content.replace( | |
| ` A living digital twin of Bengaluru's streets - surfacing illegal-parking | |
| hotspots before they choke the road. Built for the officers who decide | |
| where to look first.`, | |
| ` GridLoc separates where parking is recorded from where it actually happens, then hands BTP the gap.`); | |
| content = content.replace( | |
| ` <Stat value={2534} label="cells scored" /> | |
| <Stat value={338} label="flagged zones" /> | |
| <Stat value={11761} label="roads mapped" />`, | |
| ` <Stat value={292768} label="violations cleaned" /> | |
| <Stat value={2534} label="cells scored" /> | |
| <Stat value={5224} label="cell by time-block rows" />`); | |
| // 5. DescentSection modification | |
| content = content.replace( | |
| ` <h2 | |
| className="reveal" | |
| style={{ | |
| transitionDelay: '0.18s', | |
| fontFamily: 'var(--font-display)', | |
| fontWeight: 500, | |
| fontSize: 'clamp(1.8rem, 3.6vw, 3rem)', | |
| lineHeight: 1.08, | |
| letterSpacing: '-0.015em', | |
| color: 'var(--lp-text)', | |
| margin: 0, | |
| }} | |
| > | |
| Forty million journeys a day | |
| <br /> | |
| converge on one city. | |
| </h2> | |
| <p className="reveal mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', marginLeft: 'auto', maxWidth: 380, transitionDelay: '0.32s' }}> | |
| We cut past the noise to the hexes that matter - the corners where | |
| parked vehicles quietly turn a moving road into a standstill. | |
| </p>`, | |
| ` <h2 | |
| className="reveal" | |
| style={{ | |
| transitionDelay: '0.18s', | |
| fontFamily: 'var(--font-display)', | |
| fontWeight: 500, | |
| fontSize: 'clamp(1.8rem, 3.6vw, 3rem)', | |
| lineHeight: 1.08, | |
| letterSpacing: '-0.015em', | |
| color: 'var(--lp-text)', | |
| margin: 0, | |
| }} | |
| > | |
| Count the busy cells,<br /> | |
| call them hotspots,<br /> | |
| and you have just redrawn the camera map. | |
| </h2> | |
| <div className="reveal mt-8" style={{ transitionDelay: '0.32s', marginLeft: 'auto', maxWidth: 380 }}> | |
| <Stat value={0.66} label="correlation between violation count and number of enforcement devices" decimals={2} /> | |
| <p className="mt-4 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)' }}> | |
| Recorded volume tracks enforcement presence, not reality. | |
| </p> | |
| </div>`); | |
| // 6. HOW_CARDS | |
| content = content.replace( | |
| `const HOW_CARDS: CardData[] = [ | |
| { | |
| id: 'ingest', | |
| title: 'Every street, as data', | |
| description: | |
| 'Traffic and parking signals across Bengaluru are snapped onto an H3 hexagonal grid - one comparable cell for every neighbourhood block.', | |
| icon: <Database className="h-5 w-5" />, | |
| }, | |
| { | |
| id: 'model', | |
| title: 'Observed vs. expected', | |
| description: | |
| 'Each cell is scored on how its real congestion compares to what the model expects for that day and time-window - isolating genuine anomalies.', | |
| icon: <Activity className="h-5 w-5" />, | |
| }, | |
| { | |
| id: 'hotspots', | |
| title: 'Ranked, not just mapped', | |
| description: | |
| 'Cells are classified - Confirmed Hotspot, Blind Spot, Calm - and sorted so the worst zones rise to the top of the queue automatically.', | |
| icon: <ListOrdered className="h-5 w-5" />, | |
| }, | |
| ]`, | |
| `const HOW_CARDS: CardData[] = [ | |
| { | |
| id: 'observed', | |
| title: 'Observed score', | |
| description: 'Impact weighted, percentile ranked, not raw counts.', | |
| icon: <Activity className="h-5 w-5" />, | |
| }, | |
| { | |
| id: 'expected', | |
| title: 'Expected score', | |
| description: 'City conditions only, POI proximity 0.7 plus road hierarchy 0.3, never touches violation data.', | |
| icon: <Database className="h-5 w-5" />, | |
| }, | |
| { | |
| id: 'certify', | |
| title: 'Certify', | |
| description: 'Getis-Ord Gi star gate, only 47 of 2534 cells survive, z up to 8.59.', | |
| icon: <ListOrdered className="h-5 w-5" />, | |
| }, | |
| ]`); | |
| // 7. Remove DataSection and HexBirth | |
| content = content.replace(/\/\/ ββ 03 Β· THE DATA βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[\s\S]*?(?=\/\/ ββ 04 Β· FINDINGS \+ CTA)/, ""); | |
| // 8. Add new components | |
| const newComponents = ` | |
| // ββ CORE IDEA ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function CoreIdeaSection() { | |
| const ref = useReveal<HTMLDivElement>(0.15) | |
| return ( | |
| <section id="core-idea" ref={ref} className="relative flex min-h-screen flex-col justify-center px-8 py-32 md:px-16"> | |
| <div className="relative z-10 grid items-center gap-16 md:grid-cols-2" style={{ maxWidth: 1100, margin: '0 auto' }}> | |
| <div className="reveal"> | |
| <div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}> | |
| The Core Idea | |
| </div> | |
| <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4.4vw, 3.4rem)', lineHeight: 1.02, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0, maxWidth: 720 }}> | |
| Observed vs. Expected | |
| </h2> | |
| <p className="mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', maxWidth: 420 }}> | |
| A low recorded count can mean a genuinely calm street, or heavy uncaught parking with no camera pointed at it. We estimate where illegal parking should occur from city conditions, and compare it against where it was recorded. | |
| </p> | |
| </div> | |
| <div className="reveal" style={{ transitionDelay: '120ms', perspective: 1100 }}> | |
| <CoreIdeaMatrix /> | |
| </div> | |
| </div> | |
| </section> | |
| ) | |
| } | |
| function CoreIdeaMatrix() { | |
| const tiltRef = useScrollTilt<HTMLDivElement>(10) | |
| const [hovered, setHovered] = useState<string | null>(null) | |
| const quadrants = [ | |
| { id: 'blindspot', col: 'Low Observed', row: 'High Expected', label: 'Blind Spot', count: 661, color: '#9a9a9e', bg: 'rgba(154,154,158,0.2)', desc: 'Conditions predict heavy parking, little recorded. Likely coverage gap. Tell BTP to look here.' }, | |
| { id: 'confirmed', col: 'High Observed', row: 'High Expected', label: 'Confirmed', count: 95, color: '#efefef', bg: 'rgba(239,239,239,0.15)', desc: 'Busy in data and conditions agree. Enforce with high confidence.' }, | |
| { id: 'calm', col: 'Low Observed', row: 'Low Expected', label: 'Calm', count: 4411, color: '#5c5c60', bg: 'rgba(92,92,96,0.1)', desc: 'Quiet and expected to be quiet. Do not waste patrols.' }, | |
| { id: 'quirk', col: 'High Observed', row: 'Low Expected', label: 'Quirk', count: 57, color: '#b0b0b4', bg: 'rgba(176,176,180,0.15)', desc: 'High tickets, low demand. Possible camera trap or one-off event.' }, | |
| ] | |
| return ( | |
| <div ref={tiltRef} style={{ background: 'var(--lp-section)', border: '1px solid var(--lp-line)', borderRadius: 16, padding: 24, willChange: 'transform' }}> | |
| <div className="grid grid-cols-2 gap-4"> | |
| {quadrants.map(q => ( | |
| <div | |
| key={q.id} | |
| onMouseEnter={() => setHovered(q.id)} | |
| onMouseLeave={() => setHovered(null)} | |
| className="p-6 rounded-xl transition-all duration-300 cursor-default" | |
| style={{ | |
| background: q.id === 'blindspot' ? 'rgba(154,154,158,0.25)' : q.bg, | |
| border: \`1px solid \${q.id === 'blindspot' ? q.color : 'transparent'}\`, | |
| boxShadow: q.id === 'blindspot' ? \`0 0 20px \${q.color}40\` : 'none', | |
| transform: hovered === q.id ? 'scale(1.02)' : 'scale(1)' | |
| }} | |
| > | |
| <div className="flex justify-between items-start mb-4"> | |
| <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, color: q.color }}>{q.label}</div> | |
| <div className="tabular-nums font-mono text-xs" style={{ color: q.color }}>{q.count}</div> | |
| </div> | |
| <div className="text-[10px] uppercase mb-2" style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', color: 'var(--lp-muted)' }}> | |
| {q.col} / {q.row} | |
| </div> | |
| <div className="text-xs" style={{ color: 'var(--lp-muted)', opacity: hovered === q.id ? 1 : 0.7, transition: 'opacity 0.2s' }}> | |
| {q.desc} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ BLIND SPOT RESULTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function BlindSpotResultsSection() { | |
| const ref = useReveal<HTMLDivElement>(0.15) | |
| return ( | |
| <section id="results" ref={ref} className="relative px-8 py-32 md:px-16"> | |
| <div style={{ maxWidth: 800, margin: '0 auto' }}> | |
| <div className="reveal text-center"> | |
| <div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}> | |
| The Output | |
| </div> | |
| <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}> | |
| Blind Spot Results | |
| </h2> | |
| <p className="mt-5 text-sm leading-relaxed mx-auto" style={{ color: 'var(--lp-muted)', maxWidth: 600 }}> | |
| High-demand corridors where recorded enforcement is lower than conditions warrant. The output no count based method can produce. | |
| </p> | |
| </div> | |
| <div className="mt-16 flex flex-col gap-4"> | |
| {[ | |
| { name: 'Outer Ring Road', O: '0.89', E: '1.00' }, | |
| { name: 'Whitefield Main Road', O: '0.54', E: '1.00' }, | |
| { name: '17th Main Road', O: '0.91', E: '1.00' }, | |
| ].map((row, i) => ( | |
| <div key={row.name} className="reveal flex items-center justify-between rounded-xl p-6" style={{ background: 'var(--lp-section)', border: '1px solid var(--lp-line)', transitionDelay: \`\${0.1 * i}s\` }}> | |
| <div style={{ fontFamily: 'var(--font-display)', fontWeight: 500, color: 'var(--lp-text)', fontSize: '1.2rem' }}>{row.name}</div> | |
| <div className="flex gap-6"> | |
| <div> | |
| <span className="text-[10px] uppercase mr-2" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-muted)' }}>O</span> | |
| <span className="tabular-nums" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-text)' }}>{row.O}</span> | |
| </div> | |
| <div> | |
| <span className="text-[10px] uppercase mr-2" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-muted)' }}>E</span> | |
| <span className="tabular-nums" style={{ fontFamily: 'var(--font-mono)', color: 'var(--lp-text)' }}>{row.E}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </section> | |
| ) | |
| } | |
| // ββ TIME AWARE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function TimeAwareSection() { | |
| const ref = useReveal<HTMLDivElement>(0.15) | |
| return ( | |
| <section id="time" ref={ref} className="relative px-8 py-24 md:px-16"> | |
| <div style={{ maxWidth: 900, margin: '0 auto', textAlign: 'center' }}> | |
| <div className="reveal"> | |
| <div className={EYEBROW} style={{ ...eyebrowStyle, marginBottom: 14 }}> | |
| Time-Aware | |
| </div> | |
| <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}> | |
| Four data-derived time blocks | |
| </h2> | |
| <p className="mx-auto mt-5 text-sm leading-relaxed" style={{ color: 'var(--lp-muted)', maxWidth: 600 }}> | |
| The blocks are data-derived ranges with no real-world clock claims. Across these shifts, 36 cells dynamically change their enforcement category. | |
| </p> | |
| </div> | |
| </div> | |
| </section> | |
| ) | |
| } | |
| // ββ TRUST ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function TrustSection() { | |
| const ref = useReveal<HTMLDivElement>(0.15) | |
| return ( | |
| <section id="trust" ref={ref} className="relative px-8 py-24 md:px-16"> | |
| <div style={{ maxWidth: 1100, margin: '0 auto' }}> | |
| <div className="reveal text-center"> | |
| <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2rem, 4vw, 3.2rem)', lineHeight: 1.03, letterSpacing: '-0.02em', color: 'var(--lp-text)', margin: 0 }}> | |
| Driven by the data, not by our weight choices. | |
| </h2> | |
| </div> | |
| <div className="mt-16 grid gap-8 md:grid-cols-3"> | |
| <div className="reveal flex flex-col items-center text-center"> | |
| <div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>β₯0.998</div> | |
| <div className="text-sm" style={{ color: 'var(--lp-muted)' }}>Spearman rho correlation under weight perturbation</div> | |
| </div> | |
| <div className="reveal flex flex-col items-center text-center" style={{ transitionDelay: '0.1s' }}> | |
| <div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>97-100%</div> | |
| <div className="text-sm" style={{ color: 'var(--lp-muted)' }}>of cells keep their category under weight perturbation</div> | |
| </div> | |
| <div className="reveal flex flex-col items-center text-center" style={{ transitionDelay: '0.2s' }}> | |
| <div className="tabular-nums mb-4" style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: '2.5rem', color: 'var(--lp-text)' }}>100%</div> | |
| <div className="text-sm" style={{ color: 'var(--lp-muted)' }}>of cells resolved to real Bengaluru street names</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| ) | |
| } | |
| `; | |
| content = content.replace(/\/\/ ββ FOOTER/, newComponents + "// ββ FOOTER"); | |
| fs.writeFileSync('src/components/landing/Sections.tsx', content); | |
| // LandingPage.tsx | |
| let lpContent = fs.readFileSync('src/components/landing/LandingPage.tsx', 'utf8'); | |
| lpContent = lpContent.replace(/β/g, '-'); | |
| lpContent = lpContent.replace( | |
| ` ScrollExpansionHero, | |
| DescentSection, | |
| HowItWorksSection, | |
| DataSection, | |
| FindingsSection,`, | |
| ` ScrollExpansionHero, | |
| DescentSection, | |
| CoreIdeaSection, | |
| HowItWorksSection, | |
| BlindSpotResultsSection, | |
| TimeAwareSection, | |
| TrustSection, | |
| FindingsSection,`); | |
| lpContent = lpContent.replace( | |
| ` <WebGLShader /> | |
| <ScrollExpansionHero /> | |
| <DescentSection /> | |
| <HowItWorksSection /> | |
| <DataSection /> | |
| <FindingsSection /> | |
| <TeamFooter />`, | |
| ` <WebGLShader /> | |
| <ScrollExpansionHero /> | |
| <DescentSection /> | |
| <CoreIdeaSection /> | |
| <HowItWorksSection /> | |
| <BlindSpotResultsSection /> | |
| <TimeAwareSection /> | |
| <TrustSection /> | |
| <FindingsSection /> | |
| <TeamFooter />`); | |
| fs.writeFileSync('src/components/landing/LandingPage.tsx', lpContent); | |
| console.log("Done updating files!"); | |