Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 632d5f5b0e | |||
| 02a2e47483 | |||
| 01d82f500e | |||
| 499470486b | |||
| fa87385a30 | |||
| 4fca796f17 | |||
| 86fd66cd4f | |||
| d87ad0467b | |||
| 60e5eaee78 | |||
| 0b1452ab23 | |||
| f7a8a7d76a | |||
| f40adddf1b | |||
| 2a8ba69422 | |||
| 4f2137d411 | |||
| f145cfbe21 | |||
| 1b10b9c9a3 | |||
| d2f6a92f5d | |||
| f6c54fbecd | |||
| 88bc009d32 | |||
| 699db63ab6 | |||
| b7bc3df039 | |||
| 6310a94da7 | |||
| 0095283904 |
@@ -1,5 +1,145 @@
|
||||
@import "tailwindcss";
|
||||
@import "./styles/variables.css";
|
||||
@import "./styles/theme.css";
|
||||
@import "./styles/utilities.css";
|
||||
@import "./styles/base.css";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply m-0 p-0 box-border;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground overflow-x-hidden;
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.rounded-theme {
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
.rounded-theme-capped {
|
||||
@apply rounded-2xl;
|
||||
}
|
||||
|
||||
.rounded-theme-sharp {
|
||||
@apply rounded-none;
|
||||
}
|
||||
|
||||
.rounded-theme-pill {
|
||||
@apply rounded-full;
|
||||
}
|
||||
|
||||
.p-content {
|
||||
@apply px-6 sm:px-8 lg:px-10 py-8 sm:py-12 lg:py-16;
|
||||
}
|
||||
|
||||
.w-content-width {
|
||||
width: var(--width-content-width);
|
||||
}
|
||||
|
||||
.max-w-content-width {
|
||||
max-width: var(--width-content-width);
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-2 {
|
||||
animation: slideInFromBottom2 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromBottom2 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-4 {
|
||||
animation: slideInFromBottom4 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromBottom4 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-from-left-4 {
|
||||
animation: slideInFromLeft4 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromLeft4 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-from-right-4 {
|
||||
animation: slideInFromRight4 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight4 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Poppins } from "next/font/google";
|
||||
import "./styles/variables.css";
|
||||
import "./globals.css";
|
||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
||||
import Tag from "@/tag/Tag";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter", subsets: ["latin"],
|
||||
});
|
||||
|
||||
const poppins = Poppins({
|
||||
variable: "--font-poppins", subsets: ["latin"],
|
||||
@@ -15,40 +9,18 @@ const poppins = Poppins({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Native Line - Build Native Apps Just Talk", description: "Create production-ready iOS, iPad, and Mac apps with conversational AI. No code, no compromise. Native Line turns your ideas into native Swift apps instantly.", keywords: "native app builder, iOS development, app generator, no-code, Swift, indie developers, app creation, AI, macOS", metadataBase: new URL("https://nativeline.app"),
|
||||
alternates: {
|
||||
canonical: "https://nativeline.app"
|
||||
},
|
||||
openGraph: {
|
||||
title: "Native Line - Build Native Apps Just Talk", description: "Create production-ready iOS, iPad, and Mac apps with conversational AI. Native Line turns your ideas into native Swift apps instantly.", url: "https://nativeline.app", siteName: "Native Line", type: "website", images: [
|
||||
{
|
||||
url: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AQRMtx5ZUGLrwvjDcVIYHeaKiH/a-sleek-macos-application-window-showing-1772522998510-05a82eb5.png", alt: "Native Line App Builder Interface"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image", title: "Native Line - Build Native Apps Just Talk", description: "Create production-ready iOS, iPad, and Mac apps with conversational AI. No code, no compromise.", images: ["https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AQRMtx5ZUGLrwvjDcVIYHeaKiH/a-sleek-macos-application-window-showing-1772522998510-05a82eb5.png"]
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true
|
||||
}
|
||||
};
|
||||
title: "Native Line - Build Native Apps Just Talk", description: "Turn your ideas into production-ready iOS, iPad, and Mac apps instantly. Pure native Swift, beautifully designed, completely yours."};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<ServiceWrapper>
|
||||
<body
|
||||
className={`${inter.variable} ${poppins.variable} antialiased`}
|
||||
>
|
||||
<Tag />
|
||||
{children}
|
||||
|
||||
<html lang="en">
|
||||
<body className={poppins.variable}>
|
||||
{children}
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -1416,7 +1388,6 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</ServiceWrapper>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,23 +11,24 @@ import TestimonialCardFive from '@/components/sections/testimonial/TestimonialCa
|
||||
import FaqDouble from '@/components/sections/faq/FaqDouble';
|
||||
import ContactFaq from '@/components/sections/contact/ContactFaq';
|
||||
import FooterBaseReveal from '@/components/sections/footer/FooterBaseReveal';
|
||||
import { Sparkles, Zap, Lightbulb, Rocket, Users, Star, HelpCircle, Download, Moon, Sun } from 'lucide-react';
|
||||
import { Sparkles, Zap, Lightbulb, Rocket, Users, Star, HelpCircle, Download } from 'lucide-react';
|
||||
import AnimatedChatDemo from '@/components/sections/demo/AnimatedChatDemo';
|
||||
|
||||
export default function LandingPage() {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="hover-magnetic"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="soft"
|
||||
contentWidth="small"
|
||||
defaultButtonVariant="expand-hover"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="mediumLargeSizeLargeTitles"
|
||||
background="noise"
|
||||
cardStyle="soft-shadow"
|
||||
primaryButtonStyle="flat"
|
||||
secondaryButtonStyle="layered"
|
||||
headingFontWeight="extrabold"
|
||||
background="none"
|
||||
cardStyle="glass-elevated"
|
||||
primaryButtonStyle="shadow"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="semibold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
@@ -90,6 +91,10 @@ export default function LandingPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="chatDemo" data-section="chatDemo">
|
||||
<AnimatedChatDemo />
|
||||
</div>
|
||||
|
||||
<div id="features" data-section="features">
|
||||
<FeatureCardNineteen
|
||||
title="Why Choose Native Line"
|
||||
|
||||
@@ -1,28 +1,7 @@
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 1) rgba(255, 255, 255, 0);
|
||||
}
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-inter), sans-serif;
|
||||
@layer base {
|
||||
body {
|
||||
font-family: var(--font-poppins), sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
/* Base units */
|
||||
/* --vw is set by ThemeProvider */
|
||||
|
||||
/* --background: #f5f3f0;;;
|
||||
--card: #fdfcfb;;;
|
||||
--foreground: #2a2520e6;;;
|
||||
--primary-cta: #c69c7b;;;
|
||||
--secondary-cta: #fdfcfb;;;
|
||||
--accent: #e8dfd5;;;
|
||||
--background-accent: #d4c4b0;;; */
|
||||
/* --background: #ffffff;;;
|
||||
--card: #f9f9f9;;;
|
||||
--foreground: #000612e6;;;
|
||||
--primary-cta: #106EFB;;;
|
||||
--secondary-cta: #f9f9f9;;;
|
||||
--accent: #e2e2e2;;;
|
||||
--background-accent: #106EFB;;; */
|
||||
|
||||
--background: #f5f3f0;;;
|
||||
--card: #fdfcfb;;;
|
||||
--foreground: #2a2520e6;;;
|
||||
--primary-cta: #c69c7b;;;
|
||||
--background: #ffffff;;;
|
||||
--card: #f9f9f9;;;
|
||||
--foreground: #000612e6;;;
|
||||
--primary-cta: #106EFB;;;
|
||||
--primary-cta-text: #ffffff;;;
|
||||
--secondary-cta: #fdfcfb;;;
|
||||
--secondary-cta-text: #120a00e6;;;
|
||||
--accent: #e8dfd5;;;
|
||||
--background-accent: #d4c4b0;;;
|
||||
--secondary-cta: #f9f9f9;;;
|
||||
--secondary-cta-text: #000612e6;;;
|
||||
--accent: #e2e2e2;;;
|
||||
--background-accent: #106EFB;;;
|
||||
|
||||
/* text sizing - set by ThemeProvider */
|
||||
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
|
||||
282
src/components/sections/animated-chat-demo/AnimatedChatDemo.tsx
Normal file
282
src/components/sections/animated-chat-demo/AnimatedChatDemo.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Send } from "lucide-react";
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
isComplete?: boolean;
|
||||
}
|
||||
|
||||
interface AppIdea {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const appIdeas: AppIdea[] = [
|
||||
{
|
||||
id: "1", title: "Weather Dashboard", description: "A beautiful real-time weather app with hourly forecasts and weather alerts"
|
||||
},
|
||||
{
|
||||
id: "2", title: "Todo Manager", description: "A productivity app with task organization, categories, and smart reminders"
|
||||
},
|
||||
{
|
||||
id: "3", title: "Budget Tracker", description: "Personal finance app with spending analytics and budget goals tracking"
|
||||
}
|
||||
];
|
||||
|
||||
export default function AnimatedChatDemo() {
|
||||
const [stage, setStage] = useState<"chat" | "process" | "finished">("chat");
|
||||
const [currentIdeaIndex, setCurrentIdeaIndex] = useState(0);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [processSteps, setProcessSteps] = useState<Array<{ name: string; active: boolean }>>([]);
|
||||
const [finishedApp, setFinishedApp] = useState<AppIdea | null>(null);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
|
||||
// Stage 1: Chat simulation with auto-filling and sending
|
||||
useEffect(() => {
|
||||
if (stage !== "chat" || !isAutoPlaying) return;
|
||||
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const currentIdea = appIdeas[currentIdeaIndex];
|
||||
|
||||
// Clear messages and start fresh
|
||||
if (messages.length === 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
// Add user message character by character
|
||||
const userMessage = currentIdea.description;
|
||||
let charIndex = 0;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (charIndex <= userMessage.length) {
|
||||
setMessages((prev) => {
|
||||
if (prev.length === 0 || !prev[prev.length - 1].isUser) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: `user-${Date.now()}`,
|
||||
text: userMessage.substring(0, charIndex),
|
||||
isUser: true,
|
||||
isComplete: false
|
||||
}
|
||||
];
|
||||
} else {
|
||||
const updated = [...prev];
|
||||
updated[updated.length - 1].text = userMessage.substring(0, charIndex);
|
||||
return updated;
|
||||
}
|
||||
});
|
||||
charIndex++;
|
||||
} else {
|
||||
clearInterval(typeInterval);
|
||||
// Mark message as complete and move to next stage
|
||||
setTimeout(() => {
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
if (updated.length > 0) {
|
||||
updated[updated.length - 1].isComplete = true;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
// Auto-send and transition to process
|
||||
setTimeout(() => {
|
||||
setStage("process");
|
||||
}, 800);
|
||||
}, 500);
|
||||
}
|
||||
}, 30);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [stage, currentIdeaIndex, messages.length, isAutoPlaying]);
|
||||
|
||||
// Stage 2: Process flow animation
|
||||
useEffect(() => {
|
||||
if (stage !== "process" || !isAutoPlaying) return;
|
||||
|
||||
const steps = [
|
||||
{ name: "Design", active: false },
|
||||
{ name: "Planning", active: false },
|
||||
{ name: "Coding", active: false }
|
||||
];
|
||||
setProcessSteps(steps);
|
||||
|
||||
let stepIndex = 0;
|
||||
const stepInterval = setInterval(() => {
|
||||
if (stepIndex < steps.length) {
|
||||
setProcessSteps((prev) =>
|
||||
prev.map((step, idx) => ({
|
||||
...step,
|
||||
active: idx === stepIndex
|
||||
}))
|
||||
);
|
||||
stepIndex++;
|
||||
} else {
|
||||
clearInterval(stepInterval);
|
||||
// Transition to finished
|
||||
setTimeout(() => {
|
||||
setStage("finished");
|
||||
setFinishedApp(appIdeas[currentIdeaIndex]);
|
||||
}, 1000);
|
||||
}
|
||||
}, 1200);
|
||||
|
||||
return () => clearInterval(stepInterval);
|
||||
}, [stage, currentIdeaIndex, isAutoPlaying]);
|
||||
|
||||
// Stage 3: Finished app display and loop
|
||||
useEffect(() => {
|
||||
if (stage !== "finished" || !isAutoPlaying) return;
|
||||
|
||||
const loopTimeout = setTimeout(() => {
|
||||
const nextIndex = (currentIdeaIndex + 1) % appIdeas.length;
|
||||
setCurrentIdeaIndex(nextIndex);
|
||||
setStage("chat");
|
||||
setMessages([]);
|
||||
setProcessSteps([]);
|
||||
setFinishedApp(null);
|
||||
}, 4000);
|
||||
|
||||
return () => clearTimeout(loopTimeout);
|
||||
}, [stage, currentIdeaIndex, isAutoPlaying]);
|
||||
|
||||
return (
|
||||
<div className="w-full py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-background via-background to-background-accent/10">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16 opacity-0 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<p className="text-sm font-semibold text-primary-cta mb-2">INTERACTIVE DEMO</p>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">See It In Action</h2>
|
||||
<p className="text-base sm:text-lg text-foreground/70 max-w-2xl mx-auto">
|
||||
Watch how Native Line transforms your ideas into complete apps in real-time
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Demo Container */}
|
||||
<div className="relative">
|
||||
{/* Chat Stage */}
|
||||
{stage === "chat" && (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="bg-card border border-accent/20 rounded-lg p-6 sm:p-8 shadow-lg">
|
||||
<div className="space-y-4 mb-6 h-40 flex flex-col justify-center">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.isUser ? "justify-end" : "justify-start"} animate-in fade-in slide-in-from-bottom-2 duration-300`}
|
||||
>
|
||||
<div
|
||||
className={`px-4 py-2 rounded-lg max-w-xs ${
|
||||
msg.isUser
|
||||
? "bg-primary-cta text-white"
|
||||
: "bg-background text-foreground border border-accent/30"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{msg.text}</p>
|
||||
{msg.isComplete && msg.isUser && (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Send className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Process Flow Stage */}
|
||||
{stage === "process" && (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="flex justify-center items-center gap-2 sm:gap-4 h-40 flex-wrap">
|
||||
{processSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex items-center">
|
||||
<div
|
||||
className={`w-16 sm:w-20 h-16 sm:h-20 rounded-full flex items-center justify-center text-sm font-semibold transition-all duration-500 ${
|
||||
step.active
|
||||
? "bg-primary-cta text-white scale-110 shadow-lg animate-pulse"
|
||||
: "bg-background border-2 border-accent/30 text-foreground"
|
||||
}`}
|
||||
>
|
||||
{step.name}
|
||||
</div>
|
||||
{idx < processSteps.length - 1 && (
|
||||
<div
|
||||
className={`w-8 sm:w-12 h-1 mx-1 sm:mx-2 transition-all duration-500 ${
|
||||
processSteps[idx + 1].active ? "bg-primary-cta" : "bg-accent/20"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Finished App Stage */}
|
||||
{stage === "finished" && finishedApp && (
|
||||
<div className="animate-in fade-in duration-700">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||
{/* App Image - Right */}
|
||||
<div className="order-2 md:order-2 h-64 sm:h-80 bg-gradient-to-br from-primary-cta/10 to-accent/10 rounded-lg border border-accent/30 flex items-center justify-center animate-in fade-in slide-in-from-right-4 duration-700">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 sm:w-24 sm:h-24 bg-primary-cta/20 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<div className="w-16 h-16 sm:w-20 sm:h-20 bg-primary-cta rounded-full" />
|
||||
</div>
|
||||
<p className="text-sm text-foreground/50">App Preview</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Details - Left */}
|
||||
<div className="order-1 md:order-1 animate-in fade-in slide-in-from-left-4 duration-700 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-2xl sm:text-3xl font-bold mb-2">{finishedApp.title}</h3>
|
||||
<p className="text-foreground/70 text-sm sm:text-base">{finishedApp.description}</p>
|
||||
</div>
|
||||
<div className="space-y-2 pt-4">
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Production-Ready Code</p>
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Ready to Deploy</p>
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Fully Yours to Modify</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Loop Indicator */}
|
||||
<div className="mt-12 flex justify-center gap-2">
|
||||
{appIdeas.map((idea, idx) => (
|
||||
<button
|
||||
key={idea.id}
|
||||
onClick={() => {
|
||||
setCurrentIdeaIndex(idx);
|
||||
setStage("chat");
|
||||
setMessages([]);
|
||||
setProcessSteps([]);
|
||||
setFinishedApp(null);
|
||||
}}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||
idx === currentIdeaIndex ? "bg-primary-cta w-8" : "bg-accent/30"
|
||||
}`}
|
||||
aria-label={`Show ${idea.title}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Auto-play Toggle */}
|
||||
<div className="mt-8 flex justify-center">
|
||||
<button
|
||||
onClick={() => setIsAutoPlaying(!isAutoPlaying)}
|
||||
className="px-4 py-2 text-sm rounded-full border border-accent/30 hover:bg-card transition-colors"
|
||||
>
|
||||
{isAutoPlaying ? "Pause" : "Play"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
284
src/components/sections/demo/AnimatedChatDemo.tsx
Normal file
284
src/components/sections/demo/AnimatedChatDemo.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Send } from "lucide-react";
|
||||
import gsap from "gsap";
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
isComplete?: boolean;
|
||||
}
|
||||
|
||||
interface AppIdea {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const appIdeas: AppIdea[] = [
|
||||
{
|
||||
id: "1", title: "Weather Dashboard", description: "A beautiful real-time weather app with hourly forecasts and weather alerts"
|
||||
},
|
||||
{
|
||||
id: "2", title: "Todo Manager", description: "A productivity app with task organization, categories, and smart reminders"
|
||||
},
|
||||
{
|
||||
id: "3", title: "Budget Tracker", description: "Personal finance app with spending analytics and budget goals tracking"
|
||||
}
|
||||
];
|
||||
|
||||
export default function AnimatedChatDemo() {
|
||||
const [stage, setStage] = useState<"chat" | "process" | "finished">("chat");
|
||||
const [currentIdeaIndex, setCurrentIdeaIndex] = useState(0);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [processSteps, setProcessSteps] = useState<Array<{ name: string; active: boolean }>>([]);
|
||||
const [finishedApp, setFinishedApp] = useState<AppIdea | null>(null);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||
|
||||
// Stage 1: Chat simulation with auto-filling and sending
|
||||
useEffect(() => {
|
||||
if (stage !== "chat" || !isAutoPlaying) return;
|
||||
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const currentIdea = appIdeas[currentIdeaIndex];
|
||||
|
||||
// Clear messages and start fresh
|
||||
if (messages.length === 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
// Add user message character by character
|
||||
const userMessage = currentIdea.description;
|
||||
let charIndex = 0;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (charIndex <= userMessage.length) {
|
||||
setMessages((prev) => {
|
||||
if (prev.length === 0 || !prev[prev.length - 1].isUser) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: `user-${Date.now()}`,
|
||||
text: userMessage.substring(0, charIndex),
|
||||
isUser: true,
|
||||
isComplete: false
|
||||
}
|
||||
];
|
||||
} else {
|
||||
const updated = [...prev];
|
||||
updated[updated.length - 1].text = userMessage.substring(0, charIndex);
|
||||
return updated;
|
||||
}
|
||||
});
|
||||
charIndex++;
|
||||
} else {
|
||||
clearInterval(typeInterval);
|
||||
// Mark message as complete and move to next stage
|
||||
setTimeout(() => {
|
||||
setMessages((prev) => {
|
||||
const updated = [...prev];
|
||||
if (updated.length > 0) {
|
||||
updated[updated.length - 1].isComplete = true;
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
// Auto-send and transition to process
|
||||
setTimeout(() => {
|
||||
setStage("process");
|
||||
}, 800);
|
||||
}, 500);
|
||||
}
|
||||
}, 30);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [stage, currentIdeaIndex, messages.length, isAutoPlaying]);
|
||||
|
||||
// Stage 2: Process flow animation
|
||||
useEffect(() => {
|
||||
if (stage !== "process" || !isAutoPlaying) return;
|
||||
|
||||
const steps = [
|
||||
{ name: "Design", active: false },
|
||||
{ name: "Planning", active: false },
|
||||
{ name: "Coding", active: false }
|
||||
];
|
||||
setProcessSteps(steps);
|
||||
|
||||
let stepIndex = 0;
|
||||
const stepInterval = setInterval(() => {
|
||||
if (stepIndex < steps.length) {
|
||||
setProcessSteps((prev) =>
|
||||
prev.map((step, idx) => ({
|
||||
...step,
|
||||
active: idx === stepIndex
|
||||
}))
|
||||
);
|
||||
stepIndex++;
|
||||
} else {
|
||||
clearInterval(stepInterval);
|
||||
// Transition to finished
|
||||
setTimeout(() => {
|
||||
setStage("finished");
|
||||
setFinishedApp(appIdeas[currentIdeaIndex]);
|
||||
}, 1000);
|
||||
}
|
||||
}, 1200);
|
||||
|
||||
return () => clearInterval(stepInterval);
|
||||
}, [stage, currentIdeaIndex, isAutoPlaying]);
|
||||
|
||||
// Stage 3: Finished app display and loop
|
||||
useEffect(() => {
|
||||
if (stage !== "finished" || !isAutoPlaying) return;
|
||||
|
||||
const loopTimeout = setTimeout(() => {
|
||||
const nextIndex = (currentIdeaIndex + 1) % appIdeas.length;
|
||||
setCurrentIdeaIndex(nextIndex);
|
||||
setStage("chat");
|
||||
setMessages([]);
|
||||
setProcessSteps([]);
|
||||
setFinishedApp(null);
|
||||
}, 4000);
|
||||
|
||||
return () => clearTimeout(loopTimeout);
|
||||
}, [stage, currentIdeaIndex, isAutoPlaying]);
|
||||
|
||||
return (
|
||||
<div className="w-full py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-background via-background to-background-accent/10">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16 opacity-0 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<p className="text-sm font-semibold text-primary-cta mb-2">INTERACTIVE DEMO</p>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">See It In Action</h2>
|
||||
<p className="text-base sm:text-lg text-foreground/70 max-w-2xl mx-auto">
|
||||
Watch how Native Line transforms your ideas into complete apps in real-time
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Demo Container */}
|
||||
<div className="relative">
|
||||
{/* Chat Stage */}
|
||||
{stage === "chat" && (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="bg-card border border-accent/20 rounded-lg p-6 sm:p-8 shadow-lg">
|
||||
<div className="space-y-4 mb-6 h-40 flex flex-col justify-center">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.isUser ? "justify-end" : "justify-start"} animate-in fade-in slide-in-from-bottom-2 duration-300`}
|
||||
>
|
||||
<div
|
||||
className={`px-4 py-2 rounded-lg max-w-xs ${
|
||||
msg.isUser
|
||||
? "bg-primary-cta text-white"
|
||||
: "bg-background text-foreground border border-accent/30"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{msg.text}</p>
|
||||
{msg.isComplete && msg.isUser && (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Send className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Process Flow Stage */}
|
||||
{stage === "process" && (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
<div className="flex justify-center items-center gap-2 sm:gap-4 h-40 flex-wrap">
|
||||
{processSteps.map((step, idx) => (
|
||||
<div key={idx} className="flex items-center">
|
||||
<div
|
||||
className={`w-16 sm:w-20 h-16 sm:h-20 rounded-full flex items-center justify-center text-sm font-semibold transition-all duration-500 ${
|
||||
step.active
|
||||
? "bg-primary-cta text-white scale-110 shadow-lg animate-pulse"
|
||||
: "bg-background border-2 border-accent/30 text-foreground"
|
||||
}`}
|
||||
>
|
||||
{step.name}
|
||||
</div>
|
||||
{idx < processSteps.length - 1 && (
|
||||
<div
|
||||
className={`w-8 sm:w-12 h-1 mx-1 sm:mx-2 transition-all duration-500 ${
|
||||
processSteps[idx + 1].active ? "bg-primary-cta" : "bg-accent/20"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Finished App Stage */}
|
||||
{stage === "finished" && finishedApp && (
|
||||
<div className="animate-in fade-in duration-700">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||
{/* App Image - Right */}
|
||||
<div className="order-2 md:order-2 h-64 sm:h-80 bg-gradient-to-br from-primary-cta/10 to-accent/10 rounded-lg border border-accent/30 flex items-center justify-center animate-in fade-in slide-in-from-right-4 duration-700">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 sm:w-24 sm:h-24 bg-primary-cta/20 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<div className="w-16 h-16 sm:w-20 sm:h-20 bg-primary-cta rounded-full" />
|
||||
</div>
|
||||
<p className="text-sm text-foreground/50">App Preview</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Details - Left */}
|
||||
<div className="order-1 md:order-1 animate-in fade-in slide-in-from-left-4 duration-700 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-2xl sm:text-3xl font-bold mb-2">{finishedApp.title}</h3>
|
||||
<p className="text-foreground/70 text-sm sm:text-base">{finishedApp.description}</p>
|
||||
</div>
|
||||
<div className="space-y-2 pt-4">
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Production-Ready Code</p>
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Ready to Deploy</p>
|
||||
<p className="text-xs sm:text-sm font-semibold text-primary-cta">✓ Fully Yours to Modify</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Loop Indicator */}
|
||||
<div className="mt-12 flex justify-center gap-2">
|
||||
{appIdeas.map((idea, idx) => (
|
||||
<button
|
||||
key={idea.id}
|
||||
onClick={() => {
|
||||
setCurrentIdeaIndex(idx);
|
||||
setStage("chat");
|
||||
setMessages([]);
|
||||
setProcessSteps([]);
|
||||
setFinishedApp(null);
|
||||
}}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||
idx === currentIdeaIndex ? "bg-primary-cta w-8" : "bg-accent/30"
|
||||
}`}
|
||||
aria-label={`Show ${idea.title}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Auto-play Toggle */}
|
||||
<div className="mt-8 flex justify-center">
|
||||
<button
|
||||
onClick={() => setIsAutoPlaying(!isAutoPlaying)}
|
||||
className="px-4 py-2 text-sm rounded-full border border-accent/30 hover:bg-card transition-colors"
|
||||
>
|
||||
{isAutoPlaying ? "Pause" : "Play"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user