feat: Introduce InstallSection component and refine existing UI for the marketing page, layout, and various components.
This commit is contained in:
@@ -4,6 +4,7 @@ import Hero from '@/components/Hero';
|
||||
import Features from '@/components/Features';
|
||||
import HowItWorks from '@/components/HowItWorks';
|
||||
import CallToAction from '@/components/CallToAction';
|
||||
import InstallSection from '@/components/InstallSection';
|
||||
import Toast from '@/components/Toast';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
@@ -29,7 +30,8 @@ function HomeContent() {
|
||||
<Hero />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<CallToAction />
|
||||
{/* <CallToAction /> */}
|
||||
<InstallSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
@@ -21,6 +22,14 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark scroll-smooth">
|
||||
<head>
|
||||
<Script
|
||||
defer
|
||||
src="https://umami.mati.ss/script.js"
|
||||
data-website-id="40b6b47d-9d1f-47ff-91cf-151c98637945"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${inter.variable} ${poppins.variable} bg-background-dark text-text-light font-sans min-h-screen selection:bg-primary selection:text-background-dark`}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Smartphone, Sparkles, Clock } from 'lucide-react';
|
||||
import { animate, stagger } from 'animejs';
|
||||
|
||||
const Features: React.FC = () => {
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !hasAnimated && gridRef.current) {
|
||||
animate(gridRef.current.children, {
|
||||
translateY: [50, 0],
|
||||
opacity: [0, 1],
|
||||
delay: stagger(200),
|
||||
easing: 'outElastic(1, .6)',
|
||||
duration: 800
|
||||
});
|
||||
setHasAnimated(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (gridRef.current) {
|
||||
observer.observe(gridRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [hasAnimated]);
|
||||
|
||||
return (
|
||||
<section id="features" className="py-24 bg-background-dark">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -14,9 +45,9 @@ const Features: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
<div ref={gridRef} className="grid md:grid-cols-3 gap-8">
|
||||
{/* Feature 1 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="opacity-0 bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-indigo-900/30 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="text-indigo-400" size={30} />
|
||||
</div>
|
||||
@@ -27,7 +58,7 @@ const Features: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="opacity-0 bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-primary/20 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Sparkles className="text-emerald-400" size={30} />
|
||||
</div>
|
||||
@@ -38,7 +69,7 @@ const Features: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="opacity-0 bg-surface-dark p-8 rounded-2xl border border-gray-800 hover:border-primary/50 transition-colors group">
|
||||
<div className="w-14 h-14 bg-pink-900/30 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<Clock className="text-pink-400" size={30} />
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,130 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Mail, Lock, Check } from 'lucide-react';
|
||||
import { animate, stagger, random } from 'animejs';
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
const leftContentRef = useRef<HTMLDivElement>(null);
|
||||
const glow1Ref = useRef<HTMLDivElement>(null);
|
||||
const glow2Ref = useRef<HTMLDivElement>(null);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const streakRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (leftContentRef.current) {
|
||||
// Staggered entry for left content
|
||||
animate(leftContentRef.current.children, {
|
||||
translateY: [20, 0],
|
||||
opacity: [0, 1],
|
||||
delay: stagger(150, { start: 300 }),
|
||||
easing: 'easeOutQuad',
|
||||
duration: 800
|
||||
});
|
||||
}
|
||||
|
||||
if (cardRef.current) {
|
||||
// Elastic entry for the card
|
||||
animate(cardRef.current, {
|
||||
translateX: [100, 0],
|
||||
opacity: [0, 1],
|
||||
delay: 600,
|
||||
easing: 'outElastic(1, .8)'
|
||||
});
|
||||
}
|
||||
|
||||
if (streakRef.current) {
|
||||
// Bouncy entry for streak badge
|
||||
animate(streakRef.current, {
|
||||
scale: [0, 1],
|
||||
opacity: [0, 1],
|
||||
delay: 1000,
|
||||
easing: 'outElastic(1, .8)',
|
||||
}).then(() => {
|
||||
// Floating animation after entry
|
||||
animate(streakRef.current!, {
|
||||
translateY: [-10, 0],
|
||||
loop: true,
|
||||
direction: 'alternate',
|
||||
easing: 'inOutSine',
|
||||
duration: 2000
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (glow1Ref.current) {
|
||||
// Ambient glow animation
|
||||
animate(glow1Ref.current, {
|
||||
translateX: () => random(-30, 30),
|
||||
translateY: () => random(-30, 30),
|
||||
scale: [1, 1.2],
|
||||
duration: 4000,
|
||||
easing: 'inOutQuad',
|
||||
loop: true,
|
||||
direction: 'alternate'
|
||||
});
|
||||
}
|
||||
|
||||
if (glow2Ref.current) {
|
||||
animate(glow2Ref.current, {
|
||||
translateX: () => random(-30, 30),
|
||||
translateY: () => random(-30, 30),
|
||||
scale: [1, 1.3],
|
||||
duration: 5000,
|
||||
easing: 'inOutQuad',
|
||||
loop: true,
|
||||
direction: 'alternate'
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden mesh-gradient">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
|
||||
|
||||
{/* Left Content */}
|
||||
<div className="space-y-8 text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-semibold uppercase tracking-wider">
|
||||
<div ref={leftContentRef} className="space-y-8 text-center lg:text-left">
|
||||
<div className="opacity-0 inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-semibold uppercase tracking-wider">
|
||||
<span className="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
||||
New: AI Deck Generation
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl lg:text-7xl font-display font-bold leading-tight tracking-tight text-white">
|
||||
|
||||
<h1 className="opacity-0 text-5xl lg:text-7xl font-display font-bold leading-tight tracking-tight text-white">
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-emerald-400">
|
||||
Stop Doomscrolling
|
||||
</span>
|
||||
<br/>
|
||||
<br />
|
||||
Until You Study<br />
|
||||
|
||||
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-gray-400 max-w-2xl mx-auto lg:mx-0 leading-relaxed">
|
||||
|
||||
<p className="opacity-0 text-lg text-gray-400 max-w-2xl mx-auto lg:mx-0 leading-relaxed">
|
||||
Stop doom scrolling and start learning. Nemia blocks distracting apps until you complete your daily study goals using scientifically-backed spaced repetition.
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="https://getlaunchlist.com/s/pAqdup"
|
||||
method="POST"
|
||||
className="flex flex-col sm:flex-row gap-3 justify-center lg:justify-start max-w-md mx-auto lg:mx-0"
|
||||
>
|
||||
<div className="relative flex-grow">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" size={20} />
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="w-full bg-surface-dark border border-gray-700 rounded-xl py-4 pl-12 pr-4 text-white focus:outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-primary text-gray-900 px-8 py-4 rounded-xl font-bold text-lg hover:brightness-110 transition-all shadow-glow flex items-center justify-center gap-2 whitespace-nowrap"
|
||||
|
||||
<div className="opacity-0 flex flex-col sm:flex-row gap-3 justify-center lg:justify-start pt-4">
|
||||
<a
|
||||
href="#install"
|
||||
className="bg-primary text-gray-900 px-8 py-4 rounded-xl font-bold text-lg hover:brightness-110 transition-all shadow-glow flex items-center justify-center gap-2"
|
||||
onClick={(e) => {
|
||||
(window as { umami?: { track?: (eventName: string, data?: Record<string, unknown>) => void } })
|
||||
.umami?.track?.('hero_cta_clicked', { location: 'hero' });
|
||||
e.preventDefault();
|
||||
document.querySelector('#install')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
<span>Join Waitlist</span>
|
||||
</button>
|
||||
</form>
|
||||
<span>Get it now</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Visual (Mockups) */}
|
||||
<div className="relative mx-auto w-full max-w-md lg:max-w-full">
|
||||
{/* Background Glows */}
|
||||
<div className="absolute -top-10 -right-10 w-72 h-72 bg-purple-500/20 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-10 -left-10 w-72 h-72 bg-primary/20 rounded-full blur-3xl"></div>
|
||||
|
||||
<div ref={glow1Ref} className="absolute -top-10 -right-10 w-72 h-72 bg-purple-500/20 rounded-full blur-3xl"></div>
|
||||
<div ref={glow2Ref} className="absolute -bottom-10 -left-10 w-72 h-72 bg-primary/20 rounded-full blur-3xl"></div>
|
||||
|
||||
{/* Main Focus Mode Card */}
|
||||
<div className="relative bg-surface-dark border border-gray-700 rounded-3xl p-6 shadow-2xl transform rotate-3 hover:rotate-0 transition-transform duration-500">
|
||||
<div ref={cardRef} className="opacity-0 relative bg-surface-dark border border-gray-700 rounded-3xl p-6 shadow-2xl transform rotate-3 hover:rotate-0 transition-transform duration-500">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h3 className="text-xl font-bold text-white">Focus Mode</h3>
|
||||
<div className="bg-red-500/10 text-red-500 px-3 py-1 rounded-full text-xs font-bold flex items-center gap-1">
|
||||
@@ -97,7 +163,7 @@ const Hero: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Streak Badge (Floating) */}
|
||||
<div className="absolute -bottom-6 -left-6 bg-surface-accent text-white p-4 rounded-xl shadow-xl flex items-center gap-3 animate-bounce border border-gray-700 z-20">
|
||||
<div ref={streakRef} className="opacity-0 absolute -bottom-6 -left-6 bg-surface-accent text-white p-4 rounded-xl shadow-xl flex items-center gap-3 border border-gray-700 z-20">
|
||||
<div className="bg-green-500 rounded-full p-1">
|
||||
<Check size={14} className="text-white" />
|
||||
</div>
|
||||
|
||||
62
components/InstallSection.tsx
Normal file
62
components/InstallSection.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Smartphone, Apple } from 'lucide-react';
|
||||
import WaitlistModal from './WaitlistModal';
|
||||
|
||||
const InstallSection: React.FC = () => {
|
||||
const [isWaitlistOpen, setIsWaitlistOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<section id="install" className="py-20 bg-background-dark">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative rounded-3xl overflow-hidden bg-surface-dark border border-gray-800 p-12 text-center shadow-2xl">
|
||||
{/* Background Glow */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full max-w-2xl bg-primary/5 blur-3xl rounded-full pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl md:text-5xl font-display font-bold text-white mb-6">
|
||||
Start Your Focus Journey
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-gray-400 mb-10 max-w-2xl mx-auto">
|
||||
Download Nemia on Android today or join the waitlist for iOS.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=app.nemia.android&pcampaignid=web_share"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-primary text-gray-900 px-8 py-4 rounded-xl font-bold text-lg hover:brightness-110 transition-all shadow-glow flex items-center justify-center gap-3"
|
||||
onClick={() => {
|
||||
(window as { umami?: { track?: (eventName: string, data?: Record<string, unknown>) => void } })
|
||||
.umami?.track?.('play_store_clicked', { location: 'install_section' });
|
||||
}}
|
||||
>
|
||||
<Smartphone size={24} />
|
||||
<span>Get on Google Play</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
(window as { umami?: { track?: (eventName: string, data?: Record<string, unknown>) => void } })
|
||||
.umami?.track?.('waitlist_open_clicked', { location: 'install_section' });
|
||||
setIsWaitlistOpen(true);
|
||||
}}
|
||||
className="bg-surface-accent border border-gray-700 text-white px-8 py-4 rounded-xl font-bold text-lg hover:bg-gray-800 transition-all flex items-center justify-center gap-3"
|
||||
>
|
||||
<Apple size={24} />
|
||||
<span>Join iOS Waitlist</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaitlistModal
|
||||
isOpen={isWaitlistOpen}
|
||||
onClose={() => setIsWaitlistOpen(false)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallSection;
|
||||
@@ -38,12 +38,6 @@ const Navbar: React.FC = () => {
|
||||
<Link href="/#features" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">Features</Link>
|
||||
<Link href="/#how-it-works" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">How it Works</Link>
|
||||
<Link href="/#pricing" className="text-gray-300 hover:text-primary transition-colors text-sm font-medium">Pricing</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="bg-primary text-gray-900 px-5 py-2 rounded-full font-semibold text-sm hover:brightness-110 transition-all shadow-glow"
|
||||
>
|
||||
Log In
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -65,12 +59,6 @@ const Navbar: React.FC = () => {
|
||||
<Link href="/#features" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">Features</Link>
|
||||
<Link href="/#how-it-works" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">How it Works</Link>
|
||||
<Link href="/#pricing" className="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-800">Pricing</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="block w-full text-center mt-4 bg-primary text-gray-900 px-5 py-3 rounded-xl font-bold hover:brightness-110 transition-all"
|
||||
>
|
||||
Log In
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -84,4 +72,4 @@ const Navbar: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
export default Navbar;
|
||||
|
||||
@@ -42,6 +42,10 @@ const WaitlistModal: React.FC<WaitlistModalProps> = ({ isOpen, onClose }) => {
|
||||
action="https://getlaunchlist.com/s/pAqdup"
|
||||
method="POST"
|
||||
className="space-y-4"
|
||||
onSubmit={() => {
|
||||
(window as { umami?: { track?: (eventName: string, data?: Record<string, unknown>) => void } })
|
||||
.umami?.track?.('waitlist_submitted', { location: 'waitlist_modal' });
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" size={20} />
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"animejs": "^4.3.5",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.1",
|
||||
@@ -1094,6 +1095,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/animejs": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/animejs/-/animejs-4.3.5.tgz",
|
||||
"integrity": "sha512-yuQo/r97TCE+DDu3dTRKjyhBKSEGBcZorWeRW7WCE7EkAQpBoNd2E82dAAD/MDdrbREv7qsw/u7MAqiUX544WQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/juliangarnier"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"animejs": "^4.3.5",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.1",
|
||||
|
||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user