2
0

feat: website

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-01-27 16:20:44 +08:00
commit 20885990ca
32 changed files with 6928 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
import { motion } from 'framer-motion';
import { ArrowLeft, Heart, Network } from 'lucide-react';
export default function About() {
return (
<div className="from-primary/5 via-background to-secondary/5 min-h-screen bg-gradient-to-br p-6">
<div className="mx-auto max-w-3xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<a href="/">
<button
data-testid="button-back-about"
className="hover:bg-secondary/20 mb-4 rounded-full p-2 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-8"
>
<div className="space-y-4 text-center">
<Network className="text-primary mx-auto h-12 w-12" />
<h1 className="font-heading text-foreground text-4xl font-bold md:text-5xl">
About Ignis Network
</h1>
</div>
<div className="bg-card rounded-[2rem] border-none shadow-lg">
<div className="space-y-6 p-8 md:p-12">
<div className="space-y-4">
<h2 className="font-heading text-foreground text-2xl font-bold">
Our Mission
</h2>
<p className="text-muted-foreground text-lg leading-relaxed">
At Ignis Network, we believe that innovation is driven by
clarity and focus. We empower individuals and organizations to
achieve their full potential through cutting-edge solutions
and strategic insights.
</p>
</div>
<div className="space-y-4">
<h2 className="font-heading text-foreground text-2xl font-bold">
Our Philosophy
</h2>
<p className="text-muted-foreground text-lg leading-relaxed">
Our philosophy centers on the principles of efficiency,
precision, and continuous improvement. We strive to simplify
complex challenges, optimize workflows, and deliver robust,
scalable solutions that drive progress.
</p>
</div>
<div className="space-y-4">
<h2 className="font-heading text-foreground text-2xl font-bold">
About Our Name
</h2>
<p className="text-muted-foreground text-lg leading-relaxed">
"Ignis" is Latin for "fire," symbolizing innovation, passion,
and the spark of creation. We aim to ignite potential, fuel
progress, and illuminate the path forward for our clients and
partners. Our name reflects our commitment to bringing
transformative energy and clarity to the digital landscape.
</p>
</div>
<div className="space-y-4">
<h2 className="font-heading text-foreground text-2xl font-bold">
What We Offer
</h2>
<ul className="space-y-3">
{[
'Interactive meditation timer with customizable sessions',
'Breathing techniques & mindfulness exercises',
'Daily affirmations & zen wisdom',
'A collaborative network of innovators',
].map((item, idx) => (
<li key={idx} className="flex items-start gap-3">
<Heart className="text-primary mt-1 h-5 w-5 flex-shrink-0 fill-current" />
<span className="text-muted-foreground">{item}</span>
</li>
))}
</ul>
</div>
<div className="border-border/40 border-t pt-4">
<p className="text-muted-foreground text-center italic">
Ignite your potential. One insight, one connection at a time.
</p>
</div>
</div>
</div>
<div className="text-center">
<a href="/">
<button
data-testid="button-back-home-about"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-8 py-2 font-bold"
>
Back to Home
</button>
</a>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { ArrowLeft, Mail, Heart } from 'lucide-react';
export default function Contact() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name && email && message) {
const messages = JSON.parse(
localStorage.getItem('contactMessages') || '[]',
);
messages.push({ name, email, message, date: new Date().toISOString() });
localStorage.setItem('contactMessages', JSON.stringify(messages));
setSubmitted(true);
setName('');
setEmail('');
setMessage('');
}
};
return (
<div className="from-primary/5 via-background to-secondary/5 min-h-screen bg-gradient-to-br p-6">
<div className="mx-auto max-w-2xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<a href="/">
<button
data-testid="button-back-contact"
className="hover:bg-secondary/20 mb-4 rounded-full p-2 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-8"
>
<div className="space-y-4 text-center">
<Mail className="text-primary mx-auto h-12 w-12" />
<h1 className="font-heading text-foreground text-4xl font-bold md:text-5xl">
Get in Touch
</h1>
<p className="text-muted-foreground text-lg">
Have questions or feedback? We'd love to hear from you!
</p>
</div>
<div className="bg-card rounded-[2rem] border-none shadow-lg">
<div className="p-8 md:p-12">
{!submitted ? (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<label className="text-foreground text-sm font-bold">
Name
</label>
<input
type="text"
placeholder="Your name"
value={name}
onChange={(e) => setName(e.target.value)}
data-testid="input-contact-name"
className="border-border bg-background focus:border-primary w-full rounded-lg border-2 px-4 py-3 transition-colors focus:outline-none"
/>
</div>
<div className="space-y-2">
<label className="text-foreground text-sm font-bold">
Email
</label>
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
data-testid="input-contact-email"
className="border-border bg-background focus:border-primary w-full rounded-lg border-2 px-4 py-3 transition-colors focus:outline-none"
/>
</div>
<div className="space-y-2">
<label className="text-foreground text-sm font-bold">
Message
</label>
<textarea
placeholder="Your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
data-testid="textarea-contact-message"
rows={5}
className="border-border bg-background focus:border-primary w-full resize-none rounded-lg border-2 px-4 py-3 transition-colors focus:outline-none"
/>
</div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<button
data-testid="button-contact-submit"
type="submit"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg py-3 font-bold"
>
Send Message
</button>
</motion.div>
</form>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="space-y-4 text-center"
>
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
<Heart className="h-8 w-8 fill-current text-green-600" />
</div>
<h2 className="font-heading text-foreground text-2xl font-bold">
Thank you!
</h2>
<p className="text-muted-foreground">
We received your message and will get back to you soon. Stay
zen! 🐱
</p>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<button
onClick={() => setSubmitted(false)}
className="border-primary text-primary hover:bg-primary/10 mt-4 rounded-lg border-2 px-6 py-2 font-bold transition-colors"
>
Send Another Message
</button>
</motion.div>
</motion.div>
)}
</div>
</div>
<div className="text-center">
<a href="/">
<button
data-testid="button-back-home-contact"
className="border-primary text-primary hover:bg-primary/10 rounded-full border-2 px-8 py-2 font-bold transition-colors"
>
Back to Home
</button>
</a>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,382 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft, Lightbulb, Wind, Heart, Sparkles } from 'lucide-react';
type Category = 'tips' | 'breathing' | 'affirmations';
interface Guide {
id: string;
title: string;
description: string;
icon: React.ReactNode;
color: string;
}
const Explore = () => {
const [selectedCategory, setSelectedCategory] = useState<Category | null>(
null,
);
const categories: Record<Category, Guide[]> = {
tips: [
{
id: 'tip-1',
title: 'The Power of Silence',
description:
'Like a focused engineer, find clarity in quiet moments. Deep work is where innovation begins.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-amber-100',
},
{
id: 'tip-2',
title: 'Slow Blinking',
description:
"Practice mindful pauses. It's a way of signaling calm to your nervous system and fostering trust in your decisions.",
icon: <Heart className="h-6 w-6" />,
color: 'bg-pink-100',
},
{
id: 'tip-3',
title: 'Mindful Stretching',
description:
'Integrate micro-breaks throughout your day. Short, mindful pauses enhance productivity and prevent burnout.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-green-100',
},
{
id: 'tip-4',
title: 'Comfort is Key',
description:
'Find your cozy corner. A comfortable space makes meditation easier and more enjoyable.',
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-blue-100',
},
{
id: 'tip-5',
title: 'Focus on Your Breath',
description:
'Let your breath be your anchor. When your mind wanders, gently bring it back to your natural rhythm.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-teal-100',
},
{
id: 'tip-6',
title: 'Practice Gratitude',
description:
"Start each session by noting 3 things you're grateful for. This shifts your mind toward positivity.",
icon: <Heart className="h-6 w-6" />,
color: 'bg-fuchsia-100',
},
{
id: 'tip-7',
title: 'Meditate in Nature',
description:
'Whenever possible, take your meditation practice outdoors. Nature amplifies the calming effect.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-emerald-100',
},
{
id: 'tip-8',
title: 'Same Time, Every Day',
description:
'Establish a routine. Consistent practice helps train your mind for optimal performance and focus.',
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-sky-100',
},
],
breathing: [
{
id: 'breath-1',
title: '4-4-4-4 Breathing',
description:
'Inhale for 4, hold for 4, exhale for 4, hold for 4. A balanced rhythm for mental clarity.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-cyan-100',
},
{
id: 'breath-2',
title: 'Deep Focus Breath',
description:
'Exhale with a controlled, steady release. Feel the rhythm stabilize your focus and calm your mind.',
icon: <Heart className="h-6 w-6" />,
color: 'bg-purple-100',
},
{
id: 'breath-3',
title: 'Box Breathing',
description:
'Equal parts: inhale, hold, exhale, pause. Simple yet powerful for instant calm.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-rose-100',
},
{
id: 'breath-4',
title: 'Extended Exhale',
description:
"Make your exhale longer than your inhale. Signals to your body it's safe to relax.",
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-lime-100',
},
{
id: 'breath-5',
title: 'Alternate Nostril Breathing',
description:
'Nadi Shodhana: Balance your nervous system by breathing through alternating nostrils.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-violet-100',
},
{
id: 'breath-6',
title: 'Belly Breathing',
description:
'Place your hand on your belly. Feel it rise and fall with each breath. This activates calm.',
icon: <Heart className="h-6 w-6" />,
color: 'bg-pink-100',
},
{
id: 'breath-7',
title: 'Coherent Breathing',
description:
'5 seconds in, 5 seconds out. Creates heart-brain coherence for deep relaxation.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-amber-100',
},
{
id: 'breath-8',
title: "Lion's Breath",
description:
'Take a deep breath, then exhale forcefully while opening your mouth. Releases tension instantly.',
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-orange-100',
},
],
affirmations: [
{
id: 'aff-1',
title: 'I breathe in calm, I breathe out noise',
description:
'Like a resilient system, I adapt to change and optimize for the future.',
icon: <Heart className="h-6 w-6" />,
color: 'bg-red-100',
},
{
id: 'aff-2',
title: 'Slow is better than fast',
description:
'I embrace the philosophy of precision and deliberate action.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-yellow-100',
},
{
id: 'aff-3',
title: 'I deserve rest and comfort',
description:
"Strategic rest is not idleness, it's essential. I optimize my energy for peak performance.",
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-indigo-100',
},
{
id: 'aff-4',
title: 'Every moment is a new beginning',
description:
'Like a robust algorithm, I adapt and find optimal solutions.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-orange-100',
},
{
id: 'aff-5',
title: 'My mind is clear and peaceful',
description: 'I let go of stress and embrace serenity in every breath.',
icon: <Heart className="h-6 w-6" />,
color: 'bg-rose-100',
},
{
id: 'aff-6',
title: 'Like a stable network, I radiate calm',
description:
'I treat myself with the same precision and care I apply to my work.',
icon: <Sparkles className="h-6 w-6" />,
color: 'bg-pink-100',
},
{
id: 'aff-7',
title: 'Challenges make me stronger',
description:
'Like a well-engineered solution, I navigate challenges with resilience.',
icon: <Lightbulb className="h-6 w-6" />,
color: 'bg-cyan-100',
},
{
id: 'aff-8',
title: 'My energy is positive and vibrant',
description:
'I radiate calm confidence and attract good things into my life.',
icon: <Wind className="h-6 w-6" />,
color: 'bg-lime-100',
},
],
};
const categoryLabels: Record<
Category,
{ name: string; icon: React.ReactNode }
> = {
tips: { name: 'Zen Tips', icon: <Lightbulb className="h-5 w-5" /> },
breathing: {
name: 'Breathing Techniques',
icon: <Wind className="h-5 w-5" />,
},
affirmations: {
name: 'Daily Affirmations',
icon: <Heart className="h-5 w-5" />,
},
};
return (
<div className="from-secondary/10 via-background to-primary/5 min-h-screen bg-gradient-to-br p-6">
{/* Header */}
<div className="mx-auto max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 flex items-center gap-4"
>
<a href="/">
<button
data-testid="button-back-explore"
className="hover:bg-secondary/20 rounded-full p-2 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
</a>
<div>
<h1 className="font-heading text-foreground text-4xl font-bold">
Explore Zen
</h1>
<p className="text-muted-foreground">
Deepen your practice with timeless wisdom
</p>
</div>
</motion.div>
{/* Category Selection */}
<AnimatePresence mode="wait">
{!selectedCategory ? (
<motion.div
key="categories"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="grid grid-cols-1 gap-6 md:grid-cols-3"
>
{(Object.keys(categoryLabels) as Category[]).map(
(category, idx) => (
<motion.button
key={category}
onClick={() => setSelectedCategory(category)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }}
data-testid={`button-category-${category}`}
className="group"
>
<div className="bg-card h-full overflow-hidden rounded-[2rem] border-none shadow-lg transition-all hover:shadow-xl">
<div className="flex flex-col items-center gap-4 p-8 text-center">
<div className="bg-primary/10 group-hover:bg-primary/20 text-primary flex h-16 w-16 items-center justify-center rounded-full transition-colors">
{categoryLabels[category].icon}
</div>
<h3 className="font-heading text-foreground text-2xl font-bold">
{categoryLabels[category].name}
</h3>
<p className="text-muted-foreground text-sm">
{category === 'tips' && 'Learn meditation wisdom'}
{category === 'breathing' &&
'Master breathing techniques'}
{category === 'affirmations' && 'Empower your mind'}
</p>
</div>
</div>
</motion.button>
),
)}
</motion.div>
) : (
/* Content View */
<motion.div
key="content"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-6"
>
{/* Back Button */}
<motion.button
onClick={() => setSelectedCategory(null)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="text-primary hover:text-primary/80 mb-4 flex items-center gap-2 transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Back to categories
</motion.button>
{/* Header */}
<div>
<h2 className="font-heading text-foreground mb-2 text-3xl font-bold">
{categoryLabels[selectedCategory].name}
</h2>
<p className="text-muted-foreground">
{selectedCategory === 'tips' &&
'Insights for peak performance and well-being'}
{selectedCategory === 'breathing' &&
'Techniques to calm your mind and body'}
{selectedCategory === 'affirmations' &&
'Affirmations to start your day with zen'}
</p>
</div>
{/* Items Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<AnimatePresence>
{categories[selectedCategory].map((item, idx) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ delay: idx * 0.05 }}
data-testid={`card-${selectedCategory}-${item.id}`}
whileHover={{ y: -5 }}
>
<div className="bg-card h-full overflow-hidden rounded-[2rem] border-none shadow-lg transition-all hover:shadow-xl">
<div className="flex h-full flex-col gap-4 p-8">
<div
className={`text-primary flex h-12 w-12 items-center justify-center rounded-full ${item.color}`}
>
{item.icon}
</div>
<div>
<h3 className="font-heading text-foreground mb-2 text-xl font-bold">
{item.title}
</h3>
<p className="text-muted-foreground leading-relaxed">
{item.description}
</p>
</div>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
export default Explore;

156
src/components/FaqPage.tsx Normal file
View File

@@ -0,0 +1,156 @@
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft, ChevronDown } from 'lucide-react';
import { useState } from 'react';
const FAQ = () => {
const [expandedId, setExpandedId] = useState<string | null>(null);
const faqs = [
{
id: 'faq-1',
question: 'How long should I meditate each day?',
answer:
'Start with just 5 minutes daily. Even a short practice is powerful when consistent. As you progress, you can extend to 10, 20, or 30 minutes.',
},
{
id: 'faq-2',
question: "I can't stop my mind from wandering. Am I doing it wrong?",
answer:
"Not at all! Mind-wandering is completely normal. Meditation is noticing when your mind wanders and gently bringing it back—that's the practice itself.",
},
{
id: 'faq-3',
question: 'Best time of day to meditate?',
answer:
"Early morning (after waking) is ideal for most people. However, the best time is whenever you'll actually do it. Consistency matters more than timing.",
},
{
id: 'faq-4',
question: 'Do I need a special place to meditate?',
answer:
'No. While a quiet, comfortable space helps, meditation can happen anywhere—your bedroom, a park, even on the bus. Find what works for you.',
},
{
id: 'faq-5',
question: 'Is meditation religious? Can anyone do it?',
answer:
'Meditation has roots in many traditions, but modern mindfulness meditation is secular and evidence-based. Anyone can benefit, regardless of beliefs.',
},
{
id: 'faq-6',
question: 'How long before I see results?',
answer:
'Some benefits (like reduced stress) appear within days. Deeper changes take weeks or months. Trust the process and keep practicing.',
},
{
id: 'faq-7',
question: 'What if I fall asleep during meditation?',
answer:
"It's fine! Your body might need rest. If it happens frequently, try meditating earlier in the day or sitting upright instead of lying down.",
},
{
id: 'faq-8',
question: 'Can I meditate while walking?',
answer:
'Yes! Walking meditation is a powerful practice. Focus on the sensation of each step, your breath, and your surroundings as you walk slowly.',
},
];
return (
<div className="from-secondary/10 via-background to-primary/5 min-h-screen bg-gradient-to-br p-6">
<div className="mx-auto max-w-3xl">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-12 flex items-center gap-4"
>
<a href="/">
<button className="hover:bg-secondary/20 rounded-full p-2 transition-colors">
<ArrowLeft className="h-5 w-5" />
</button>
</a>
<div>
<h1 className="font-heading text-foreground text-4xl font-bold">
FAQ
</h1>
<p className="text-muted-foreground">
Answers to your meditation questions
</p>
</div>
</motion.div>
{/* FAQs */}
<div className="space-y-3">
{faqs.map((faq, idx) => (
<motion.div
key={faq.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.05 }}
data-testid={`faq-item-${faq.id}`}
>
<div className="border-border/40 bg-card overflow-hidden rounded-xl border shadow-lg transition-shadow hover:shadow-xl">
<button
onClick={() =>
setExpandedId(expandedId === faq.id ? null : faq.id)
}
className="bg-card hover:bg-card/80 w-full p-6 text-left transition-colors"
>
<div className="flex items-start justify-between gap-4">
<h3 className="font-heading text-foreground text-lg font-bold">
{faq.question}
</h3>
<motion.div
animate={{ rotate: expandedId === faq.id ? 180 : 0 }}
transition={{ duration: 0.3 }}
className="text-primary flex-shrink-0"
>
<ChevronDown className="h-5 w-5" />
</motion.div>
</div>
</button>
<AnimatePresence>
{expandedId === faq.id && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="border-border/40 overflow-hidden border-t"
>
<p className="text-muted-foreground p-6 leading-relaxed">
{faq.answer}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
))}
</div>
{/* Footer CTA */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="bg-primary/10 mt-12 rounded-2xl p-8 text-center"
>
<p className="text-muted-foreground mb-4">
Still have questions? We'd love to hear from you.
</p>
<a href="/contact">
<button className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-6 py-2 font-bold shadow-lg transition-all hover:shadow-xl">
Contact Us
</button>
</a>
</motion.div>
</div>
</div>
);
};
export default FAQ;

View File

@@ -0,0 +1,151 @@
import { motion } from 'framer-motion';
import { ArrowLeft, CheckCircle, Lightbulb, Heart } from 'lucide-react';
const Guide = () => {
const steps = [
{
title: 'Why Meditate?',
description:
'Meditation reduces stress and anxiety, improves focus, and enhances emotional well-being. It literally rewires your brain for peace.',
icon: <Heart className="h-6 w-6" />,
benefits: [
'Better sleep quality',
'Reduced anxiety & stress',
'Improved focus & clarity',
'Emotional resilience',
],
},
{
title: 'How to Start',
description:
"You don't need experience. Just find a comfortable spot, close your eyes, and follow your breath. That's it!",
icon: <Lightbulb className="h-6 w-6" />,
steps: [
'Find a quiet, comfortable place',
'Sit or lie down (whatever feels right)',
'Close your eyes',
'Focus on your natural breath',
'When your mind wanders, gently bring it back',
],
},
{
title: 'Building Consistency',
description:
'The magic happens through regular practice. Start small and build from there.',
icon: <CheckCircle className="h-6 w-6" />,
tips: [
'Start with just 5 minutes',
'Same time every day (ideally morning)',
'Track your sessions',
'Be patient with yourself',
'Celebrate small wins',
],
},
];
return (
<div className="from-secondary/10 via-background to-primary/5 min-h-screen bg-gradient-to-br p-6">
<div className="mx-auto max-w-3xl">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-12 flex items-center gap-4"
>
<a href="/">
<button className="hover:bg-secondary/20 rounded-full p-2 transition-colors">
<ArrowLeft className="h-5 w-5" />
</button>
</a>
<div>
<h1 className="font-heading text-foreground text-4xl font-bold">
Beginner's Guide
</h1>
<p className="text-muted-foreground">
Start your meditation journey with us
</p>
</div>
</motion.div>
{/* Steps */}
<div className="space-y-8">
{steps.map((step, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
>
<div className="bg-card overflow-hidden rounded-2xl border-none shadow-lg transition-shadow hover:shadow-xl">
<div className="p-8">
{/* Title */}
<div className="mb-4 flex items-center gap-4">
<div className="bg-primary/20 text-primary flex h-12 w-12 items-center justify-center rounded-full">
{step.icon}
</div>
<div>
<h2 className="font-heading text-foreground text-2xl font-bold">
{step.title}
</h2>
<p className="text-muted-foreground text-sm">
Step {idx + 1} of {steps.length}
</p>
</div>
</div>
{/* Description */}
<p className="text-muted-foreground mb-6 leading-relaxed">
{step.description}
</p>
{/* Benefits/Steps/Tips */}
<div className="space-y-2">
{(step.benefits || step.steps || step.tips)?.map(
(item, i) => (
<div key={i} className="flex items-start gap-3">
<CheckCircle className="text-primary mt-1 h-5 w-5 flex-shrink-0" />
<span className="text-foreground/80">{item}</span>
</div>
),
)}
</div>
</div>
</div>
</motion.div>
))}
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="bg-primary/10 mt-12 rounded-2xl p-8 text-center"
>
<h2 className="font-heading text-foreground mb-3 text-2xl font-bold">
Ready to begin? 🐱
</h2>
<p className="text-muted-foreground mb-6">
Start with our meditation timer and pick a breathing technique that
resonates with you.
</p>
<div className="flex flex-wrap justify-center gap-4">
<a href="/app">
<button className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-6 py-2 font-bold shadow-lg transition-all hover:shadow-xl">
Start Meditating
</button>
</a>
<a href="/explore">
<button className="border-primary text-primary hover:bg-primary/10 rounded-full border-2 px-6 py-2 font-bold transition-colors">
Explore Techniques
</button>
</a>
</div>
</motion.div>
</div>
</div>
);
};
export default Guide;

482
src/components/HomePage.tsx Normal file
View File

@@ -0,0 +1,482 @@
import { SiGo, SiKubernetes, SiNixos, SiOpentelemetry, SiReact, SiTalos, SiTerraform, SiTypescript } from '@icons-pack/react-simple-icons';
import { motion, AnimatePresence } from 'framer-motion';
import {
Heart,
Sparkles,
Menu,
Snowflake,
X,
Network,
Users,
BookOpen,
Compass,
Star,
Lock,
CheckCircle,
Cpu,
Bot,
CalendarHeart,
Flame,
} from 'lucide-react';
import { useRef,
useState, useEffect } from 'react';
import CanvasNest from 'canvas-nest.js';
import { skipPartiallyEmittedExpressions } from 'typescript';
const Navigation = () => {
const [isOpen, setIsOpen] = useState(false);
const [activeSection, setActiveSection] = useState('home');
useEffect(() => {
const handleScroll = () => {
const sections = ['home', 'features', 'cta'];
let current = 'home';
for (const section of sections) {
const element = document.getElementById(section);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= 100) {
current = section;
}
}
}
setActiveSection(current);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const navLinks = [] as const;
return (
<nav className="bg-background/80 border-border/40 relative sticky top-0 z-50 flex w-full items-center justify-between border-b px-6 py-6 shadow-sm backdrop-blur-lg md:px-12">
<a
href="/"
className="text-primary flex items-center gap-3 transition-opacity hover:opacity-80"
>
<img src="images/logo.png" alt="Ignis Logo" className="h-10" />
</a>
<a href="mailto:contact@ignisnet.co">
<button
data-testid="button-join-nav"
className="cursor-pointer bg-primary text-primary-background hover:bg-primary/90 rounded-full px-6 py-2 font-bold shadow-lg transition-all hover:scale-105 hover:shadow-xl"
>
Contact Us
</button>
</a>
</nav>
);
};
const Hero = () => {
const bodyRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!bodyRef.current) return;
const config = {
color: '241,151,142',
count: 88,
opacity: 0.8,
zIndex: 0,
};
const cn = new CanvasNest(bodyRef.current, config);
return () => {
cn.destroy();
};
}, [])
return (
<section
id="home"
ref={bodyRef}
className="relative flex min-h-[90vh] flex-col items-center overflow-hidden px-6 py-12 md:flex-row md:px-12 lg:px-24"
>
{/* Decorative Blobs */}
<div className="bg-primary/10 absolute top-[-10%] left-[-10%] -z-10 h-[500px] w-[500px] rounded-full blur-3xl" />
<div className="bg-accent/30 absolute right-[-5%] bottom-[10%] -z-10 h-[400px] w-[400px] rounded-full blur-3xl" />
<div className="relative z-10 w-full space-y-8 md:w-1/2">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="font-heading font-normal text-foreground mb-6 text-5xl leading-[1.1] md:text-7xl">
Ignis Network
</div>
<p className="text-muted-foreground max-w-3xl text-lg leading-relaxed md:text-xl">
We're a startup company that builds resilient digital infrastructure designed to endure, with AI applied where it meaningfully adds strength and insight.
</p>
<p className="text-muted-foreground max-w-2xl text-lg leading-relaxed md:text-xl mt-2">
We focus on stability, clarity, and long-term reliability — creating systems that smartly carry what matters, support growth over time, and let everything else move forward with confidence.
</p>
<motion.div
className="flex flex-wrap gap-4 mt-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{[
{ icon: SiNixos, label: 'Nix', color: 'text-[#5277C3]' },
{ icon: SiKubernetes, label: 'Kubernetes', color: 'text-[#326CE5]' },
{
icon: SiTerraform,
label: 'Terraform',
color: 'text-[#844FBA]',
},
{ icon: SiOpentelemetry, label: 'OpenTelemetry', color: 'text-[#000000]' },
{ icon: Bot, label: 'Ignis AI', color: 'text-[#F1978E]'}
].map((badge, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 + idx * 0.1 }}
className="bg-secondary/30 border-border/40 flex items-center justify-center gap-2 rounded-full border px-3 py-2"
>
<badge.icon
className={`h-4 w-4 flex-shrink-0 ${badge.color}`}
/>
<span className="text-foreground text-xs font-semibold">
{badge.label}
</span>
</motion.div>
))}
</motion.div>
</motion.div>
</div>
<div className='flex-1 flex justify-center items-center'>
<Flame className='stroke-[#F1978E] size-96 stroke-[0.3px] opacity-70 mb-4 mr-12 hidden md:block' />
</div>
</section>
);
};
const FeatureCard = ({
title,
desc,
img,
delay,
testId,
}: {
title: string;
desc: string;
img: string;
delay: number;
testId: string;
}) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay }}
whileHover={{ y: -10 }}
data-testid={testId}
>
<div className="bg-card h-full overflow-hidden rounded-[2rem] border-none shadow-lg transition-shadow duration-300 hover:shadow-xl">
<div className="flex h-full flex-col p-0">
<div className="bg-secondary/30 flex h-48 items-center justify-center p-8">
<motion.img
src={img}
alt={title}
className="h-32 w-auto object-contain drop-shadow-md"
whileHover={{ scale: 1.1 }}
/>
</div>
<div className="flex flex-1 flex-col items-center p-8 text-center">
<h3 className="font-heading text-foreground mb-3 text-2xl font-bold">
{title}
</h3>
<p className="text-muted-foreground leading-relaxed">{desc}</p>
</div>
</div>
</div>
</motion.div>
);
const Features = () => {
return (
<section
id="features"
className="relative bg-white/50 px-6 py-24 md:px-12 lg:px-24"
>
<div className="mx-auto max-w-7xl">
<div className="mb-16 space-y-4 text-center">
<span className="font-hand text-primary text-xl">Our Vision</span>
<h2 className="font-heading text-foreground text-4xl font-bold md:text-5xl">
Pioneering Digital Transformation
</h2>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<FeatureCard
title="AI-Driven Insights"
desc="Leverage advanced AI to unlock strategic opportunities and optimize decision-making."
img="/images/focused_mind_illustration.png"
delay={0.1}
testId="card-feature-chill"
/>
<FeatureCard
title="Seamless Transformation"
desc="Accelerate your digital journey with agile methodologies and intelligent solutions."
img="/images/dynamic_innovation_illustration.png"
delay={0.2}
testId="card-feature-playful"
/>
<FeatureCard
title="Collaborative Ecosystem"
desc="Foster continuous learning and a collaborative spirit for groundbreaking innovation."
img="/images/holistic_growth_illustration.png"
delay={0.3}
testId="card-feature-nourishment"
/>
</div>
</div>
</section>
);
};
export const Footer = () => {
return (
<footer className="from-secondary/5 via-background to-primary/5 border-border/40 relative border-t bg-gradient-to-br px-6 py-16">
<div className="mx-auto max-w-7xl">
<div className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-8 lg:gap-12">
{/* Left Column: Brand */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col justify-center space-y-3"
>
<div className="flex items-center gap-3">
<img src="/images/logo.png" alt="Ignis Logo" className="h-8" />
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Smart infrastructure built for stability, longevity, and trust.
</p>
By
<a
href="https://bsky.app/profile/sne.moe"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary w-fit text-sm transition-colors"
>
@AsaiNeko
</a>
</motion.div>
{/* Center Column: Links by Category */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="space-y-6"
>
{/* Events */}
<div className="space-y-3">
<div className="text-primary flex items-center gap-2">
<CalendarHeart className="h-5 w-5" />
<span className="font-heading text-sm font-bold">Events</span>
</div>
<div className="flex flex-col gap-2 pl-7">
<span
data-testid="link-footer-guide"
className="text-muted-foreground hover:text-primary w-fit text-sm transition-colors"
>
NixCN Conference
</span>
</div>
</div>
{/* Contact */}
<div className="space-y-3">
<div className="text-primary flex items-center gap-2">
<Users className="h-5 w-5" />
<span className="font-heading text-sm font-bold">
Contact
</span>
</div>
<div className="flex flex-col gap-2 pl-7">
<a
href="/contact"
data-testid="link-footer-contact"
className="text-muted-foreground hover:text-primary w-fit text-sm transition-colors"
>
Contact Us
</a>
{/*<a
href="/about"
data-testid="link-footer-about-community"
className="text-muted-foreground hover:text-primary w-fit text-sm transition-colors"
>
About Us
</a>*/}
</div>
</div>
{/* Legal */}
{/*<div className="space-y-3">
<div className="text-primary flex items-center gap-2">
<Lock className="h-5 w-5" />
<span className="font-heading text-sm font-bold">Legal</span>
</div>
<div className="flex flex-col gap-2 pl-7">
<a
href="/privacy"
data-testid="link-footer-privacy"
className="text-muted-foreground hover:text-primary w-fit text-sm transition-colors"
>
Privacy Policy
</a>
</div>
</div>*/}
</motion.div>
{/* Right Column: CTA */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex flex-col justify-center space-y-4"
>
<div className="space-y-2">
<h4 className="font-heading text-foreground font-bold">
Get in touch
</h4>
<p className="text-muted-foreground text-sm">
Reach out whenever theres something worth building together.
</p>
</div>
<a href="mailto:contact@ignisnet.co" className="w-full">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
data-testid="button-footer-cta"
className="cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-full px-6 py-3 font-bold shadow-lg transition-all hover:shadow-xl"
>
Contact Us
</motion.button>
</a>
</motion.div>
</div>
{/* Divider */}
<div className="border-border/40 my-8 border-t" />
{/* Bottom: Copyright */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="text-muted-foreground/60 flex flex-col items-center justify-between gap-4 text-center text-xs md:flex-row"
>
<p>&copy; 2026 Ignis Network, Co. All rights reserved.</p>
<p className="flex items-center justify-center gap-1">
Built with <Heart className="h-3 w-3 text-red-400" /> for the
digital age
</p>
</motion.div>
</div>
</footer>
);
};
function CTA() {
<section id="cta" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="mb-12 space-y-4 text-center"
>
<h2 className="font-heading text-foreground text-4xl font-bold md:text-5xl">
Ready to Ignite Innovation?
</h2>
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
Start your transformation journey today.
</p>
</motion.div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{[
{
id: 'cta-start-app',
title: 'Deploy AI Solutions',
description:
'Implement scalable AI architectures tailored to your enterprise needs.',
href: '/app',
testId: 'button-cta-app',
icon: <Cpu className="text-primary h-8 w-8" />,
},
{
id: 'cta-guide',
title: 'Strategic Consulting',
description:
'Expert guidance on digital transformation and technology roadmap planning.',
href: '/guide',
testId: 'button-cta-guide',
icon: <BookOpen className="text-primary h-8 w-8" />,
},
{
id: 'cta-explore',
title: 'Explore Platform',
description:
'Discover our suite of intelligent tools designed to accelerate your growth.',
href: '/explore',
testId: 'button-cta-explore',
icon: <Compass className="text-primary h-8 w-8" />,
},
].map((cta, idx) => (
<motion.div
key={cta.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1, duration: 0.5 }}
whileHover={{ y: -8 }}
className="bg-card flex h-full flex-col items-center gap-4 rounded-[2rem] p-8 text-center shadow-lg transition-shadow hover:shadow-xl"
>
<div className="bg-primary/15 flex h-16 w-16 items-center justify-center rounded-full">
{cta.icon}
</div>
<h3 className="font-heading text-foreground text-2xl font-bold">
{cta.title}
</h3>
<p className="text-muted-foreground flex-1">
{cta.description}
</p>
<a href={cta.href}>
<button
data-testid={cta.testId}
className="bg-primary text-primary-foreground hover:bg-primary/90 cursor-pointer rounded-full px-6 py-2 text-sm font-bold shadow-md transition-colors"
>
Get Started
</button>
</a>
</motion.div>
))}
</div>
</div>
</section>
}
export default function Home() {
return (
<div className="bg-background selection:bg-primary/20 selection:text-primary-foreground min-h-screen">
<Navigation />
<Hero />
<Footer />
</div>
);
}

306
src/components/JoinPage.tsx Normal file
View File

@@ -0,0 +1,306 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ArrowLeft, Mail, Heart, Users, Network } from 'lucide-react';
const Join = () => {
const [email, setEmail] = useState('');
const [joined, setJoined] = useState(false);
const [error, setError] = useState('');
const [members, setMembers] = useState(0);
useEffect(() => {
const communityMembers = JSON.parse(
localStorage.getItem('communityMembers') || '[]',
);
setMembers(communityMembers.length);
}, []);
const handleJoin = (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setError('Please enter your email');
return;
}
if (!email.includes('@')) {
setError('Please enter a valid email');
return;
}
// Save to localStorage
const communityMembers = JSON.parse(
localStorage.getItem('communityMembers') || '[]',
);
if (!communityMembers.includes(email)) {
communityMembers.push(email);
localStorage.setItem(
'communityMembers',
JSON.stringify(communityMembers),
);
setMembers(communityMembers.length);
}
setJoined(true);
setEmail('');
setError('');
};
return (
<div className="from-primary/10 via-background to-accent/10 flex min-h-screen flex-col bg-gradient-to-br p-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mx-auto w-full max-w-2xl"
>
<a href="/">
<button
data-testid="button-back-join"
className="hover:bg-secondary/20 mb-6 rounded-full p-2 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
</a>
</motion.div>
{/* Main Content */}
<div className="mx-auto flex w-full max-w-2xl flex-1 items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6 }}
className="w-full"
>
<div className="bg-card/95 overflow-hidden rounded-[2rem] border-none shadow-2xl backdrop-blur-xl">
<div className="flex flex-col items-center gap-8 p-12 text-center md:p-16">
{!joined ? (
<>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-4"
>
<div className="flex justify-center">
<div className="bg-primary/20 flex h-20 w-20 items-center justify-center rounded-full">
<Network className="text-primary h-10 w-10" />
</div>
</div>
<h1 className="font-heading text-foreground text-4xl font-bold md:text-5xl">
Join the Network
</h1>
<p className="text-muted-foreground mx-auto max-w-md text-lg leading-relaxed">
A network is a group of innovators. Join our network of
visionaries and tech enthusiasts worldwide.
</p>
</motion.div>
{/* Benefits */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="grid w-full grid-cols-1 gap-4 md:grid-cols-3"
>
<div className="space-y-2">
<Mail className="text-primary mx-auto h-6 w-6" />
<p className="text-foreground text-sm font-bold">
Weekly Tips
</p>
<p className="text-muted-foreground text-xs">
Weekly zen tips and affirmations
</p>
</div>
<div className="space-y-2">
<Heart className="text-primary mx-auto h-6 w-6" />
<p className="text-foreground text-sm font-bold">
Community
</p>
<p className="text-muted-foreground text-xs">
Connect with our meditation community
</p>
</div>
<div className="space-y-2">
<Users className="text-primary mx-auto h-6 w-6" />
<p className="text-foreground text-sm font-bold">
Exclusive
</p>
<p className="text-muted-foreground text-xs">
Access member-only content
</p>
</div>
</motion.div>
{/* Form */}
<motion.form
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
onSubmit={handleJoin}
className="w-full space-y-4"
>
<div className="flex flex-col gap-3 sm:flex-row">
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
}}
data-testid="input-email-join"
className="bg-secondary/20 focus:border-primary flex-1 rounded-full border-2 border-transparent px-6 py-4 text-lg transition-all focus:outline-none"
/>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<button
data-testid="button-join-submit"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-8 py-3 font-bold shadow-lg"
>
Join Now
</button>
</motion.div>
</div>
{error && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-sm font-medium text-red-500"
>
{error}
</motion.p>
)}
</motion.form>
{/* Stats */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="border-border/40 w-full border-t pt-6"
>
<div className="flex items-center justify-center gap-6 text-sm">
<div>
<p className="text-foreground text-lg font-bold">
{members}+
</p>
<p className="text-muted-foreground">
Community Members
</p>
</div>
<div className="bg-border/40 h-8 w-px" />
<div>
<p className="text-foreground text-lg font-bold">
24/7
</p>
<p className="text-muted-foreground">
Support Available
</p>
</div>
</div>
</motion.div>
{/* Privacy note */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-muted-foreground text-xs"
>
We respect your privacy. No spam, just insights. Ever.
</motion.p>
</>
) : (
<>
{/* Success State */}
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="w-full space-y-6"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 0.6 }}
className="flex justify-center"
>
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-green-100">
<Heart className="h-12 w-12 fill-current text-green-600" />
</div>
</motion.div>
<div className="space-y-2">
<h2 className="font-heading text-foreground text-3xl font-bold">
Welcome to the Community!
</h2>
<p className="text-muted-foreground text-lg">
You're now part of our innovative network. Check your
email for exclusive content! 📧
</p>
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="space-y-3 pt-4"
>
<p className="text-foreground font-bold">What's next?</p>
<div className="mx-auto max-w-sm space-y-2 text-left">
<p className="flex items-start gap-2 text-sm">
<span className="text-primary font-bold"></span>
<span>
Weekly meditation tips delivered to your inbox
</span>
</p>
<p className="flex items-start gap-2 text-sm">
<span className="text-primary font-bold"></span>
<span>
Exclusive breathing techniques & affirmations
</span>
</p>
<p className="flex items-start gap-2 text-sm">
<span className="text-primary font-bold"></span>
<span>Join our growing community of zen-seekers</span>
</p>
</div>
</motion.div>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="pt-4"
>
<a href="/app">
<button
data-testid="button-start-meditation"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-8 py-3 font-bold"
>
Start Meditating Now
</button>
</a>
</motion.div>
<a href="/" className="inline-block">
<button
data-testid="button-back-home-join"
className="border-primary text-primary hover:bg-primary/10 rounded-full border-2 px-8 py-2 transition-colors"
>
Back to Home
</button>
</a>
</motion.div>
</>
)}
</div>
</div>
</motion.div>
</div>
</div>
);
};
export default Join;

View File

@@ -0,0 +1,112 @@
import { motion } from 'framer-motion';
import { ArrowLeft } from 'lucide-react';
export default function Privacy() {
return (
<div className="from-primary/5 via-background to-secondary/5 min-h-screen bg-gradient-to-br p-6">
<div className="mx-auto max-w-3xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<a href="/">
<button
data-testid="button-back-privacy"
className="hover:bg-secondary/20 mb-4 rounded-full p-2 transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</button>
</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-8"
>
<div>
<h1 className="font-heading text-foreground mb-4 text-4xl font-bold md:text-5xl">
Privacy Policy
</h1>
</div>
<div className="bg-card rounded-[2rem] border-none shadow-lg">
<div className="space-y-6 p-8 md:p-12">
<div className="space-y-3">
<h2 className="font-heading text-foreground text-xl font-bold">
Data We Collect
</h2>
<p className="text-muted-foreground">
We collect email addresses from users who choose to join our
community. No payment information or sensitive data is
required. Everything is stored locally in your browser using
localStorage.
</p>
</div>
<div className="space-y-3">
<h2 className="font-heading text-foreground text-xl font-bold">
How We Use Your Data
</h2>
<p className="text-muted-foreground">
Your email is used only for newsletter communications and
community updates. We never sell or share your data with third
parties. You can unsubscribe anytime.
</p>
</div>
<div className="space-y-3">
<h2 className="font-heading text-foreground text-xl font-bold">
Session Data
</h2>
<p className="text-muted-foreground">
Your meditation sessions and progress are stored locally on
your device. We do not track or collect information about your
usage patterns. You have full control over your data.
</p>
</div>
<div className="space-y-3">
<h2 className="font-heading text-foreground text-xl font-bold">
Security
</h2>
<p className="text-muted-foreground">
We take your privacy seriously. All data transmissions are
encrypted. We follow best practices for web security and
regularly update our systems.
</p>
</div>
<div className="space-y-3">
<h2 className="font-heading text-foreground text-xl font-bold">
Contact Us
</h2>
<p className="text-muted-foreground">
If you have concerns about your privacy, please reach out
through our contact page. We're here to help.
</p>
</div>
<div className="border-border/40 text-muted-foreground border-t pt-4 text-sm">
Last updated: {new Date().toLocaleDateString()}
</div>
</div>
</div>
<div className="text-center">
<a href="/">
<button
data-testid="button-back-home-privacy"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-full px-8 py-2 font-bold"
>
Back to Home
</button>
</a>
</div>
</motion.div>
</div>
</div>
);
}

1
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="astro/client" />

181
src/layouts/Layout.astro Normal file
View File

@@ -0,0 +1,181 @@
---
interface Props {
title?: string;
description?: string;
canonicalPath?: string;
noindex?: boolean;
}
const {
title = 'Ignis Network - Cat-Inspired Meditation & Mindfulness',
description = 'Redefining the digital frontier with next-generation internet services and advanced AI.',
canonicalPath = '',
noindex = false,
} = Astro.props;
const siteUrl = Astro.site?.href || '';
const canonicalUrl = `${siteUrl.replace(/\/$/, '')}${canonicalPath}`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta name="theme-color" content="#E8A598" />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="canonical" href={canonicalUrl} />
<title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fredoka:wght@300..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Patrick+Hand&display=swap"
rel="stylesheet"
/>
<script
type="application/ld+json"
set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'Ignis Network',
description: 'Cat-inspired meditation and mindfulness application',
applicationCategory: 'HealthApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
})}
/>
</head>
<body>
<slot />
</body>
</html>
<style is:global>
@import 'tailwindcss';
@custom-variant hover (&:hover);
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-xl: 1.5rem;
--font-sans: 'Nunito', sans-serif;
--font-heading: 'Fredoka', sans-serif;
--font-hand: 'Patrick Hand', cursive;
}
:root {
--background: 40 33% 96%;
--foreground: 25 10% 30%;
--primary: 5 78% 75%;
--primary-foreground: 25 10% 20%;
--secondary: 140 25% 85%;
--secondary-foreground: 140 30% 25%;
--card: 0 0% 100%;
--card-foreground: 25 10% 30%;
--popover: 0 0% 100%;
--popover-foreground: 25 10% 30%;
--muted: 40 20% 90%;
--muted-foreground: 25 10% 50%;
--accent: 45 80% 90%;
--accent-foreground: 25 10% 30%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
--border: 40 20% 90%;
--input: 40 20% 90%;
--ring: 6 78% 75%;
--radius: 1rem;
}
@layer base {
html {
scroll-behavior: smooth;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-sans antialiased;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-heading font-semibold tracking-tight;
}
}
button,
a,
nav,
[role='button'],
.no-select {
@apply select-none;
}
[role='presentation'],
.pointer-none,
svg[aria-hidden='true'] {
pointer-events: none;
}
.absolute.blur-3xl,
[class*='blob'],
[class*='pattern'] {
@apply pointer-events-none;
}
svg:not([role='img']) {
pointer-events: none;
}
.font-hand {
font-family: var(--font-hand);
}
</style>

35
src/lib/canvas-nest.d.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
declare module 'canvas-nest.js' {
/**
* CanvasNest 配置项接口
*/
export interface Config {
/** 线条颜色,格式为 "R,G,B" */
color?: string;
/** 线条透明度 (0~1) */
opacity?: number;
/** 粒子数量 */
count?: number;
/** z-index 层级 */
zIndex?: number;
/** 仅在特定元素内渲染(如果不传则默认为 body */
element?: HTMLElement;
}
/**
* CanvasNest 实例类
*/
class CanvasNest {
/**
* @param el 容器元素
* @param config 配置项
*/
constructor(el: HTMLElement, config?: Config);
/**
* 销毁实例,移除 canvas 元素
*/
destroy(): void;
}
export default CanvasNest;
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

12
src/pages/about.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import AboutPage from '../components/AboutPage';
---
<Layout
title="About Us - Ignis Network"
description="Learn about our mission to help you find peace through cat-inspired meditation. Discover the team behind Ignis Network."
canonicalPath="/about"
>
<AboutPage client:load />
</Layout>

12
src/pages/contact.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import ContactPage from '../components/ContactPage';
---
<Layout
title="Contact Us - Ignis Network"
description="Have questions or feedback about Ignis Network? We'd love to hear from you. Reach out to our team."
canonicalPath="/contact"
>
<ContactPage client:load />
</Layout>

12
src/pages/explore.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import ExplorePage from '../components/ExplorePage';
---
<Layout
title="Explore Zen Tips & Techniques - Ignis Network"
description="Discover zen tips, breathing techniques, and daily affirmations to deepen your meditation practice. Cat-inspired wisdom for mindfulness."
canonicalPath="/explore"
>
<ExplorePage client:load />
</Layout>

12
src/pages/faq.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import FaqPage from '../components/FaqPage';
---
<Layout
title="Frequently Asked Questions - Ignis Network"
description="Find answers to common questions about meditation, mindfulness, and using Ignis Network for your wellness journey."
canonicalPath="/faq"
>
<FaqPage client:load />
</Layout>

12
src/pages/guide.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import GuidePage from '../components/GuidePage';
---
<Layout
title="Beginner's Meditation Guide - Ignis Network"
description="Start your meditation journey with our step-by-step guide for beginners. Learn the basics of mindfulness with cat-inspired techniques."
canonicalPath="/guide"
>
<GuidePage client:load />
</Layout>

12
src/pages/index.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import HomePage from '../components/HomePage';
---
<Layout
title="Ignis Network - Cat-Inspired Meditation & Mindfulness"
description="Redefining the digital frontier with next-generation internet services and advanced AI."
canonicalPath="/"
>
<HomePage client:only="react" />
</Layout>

12
src/pages/join.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import JoinPage from '../components/JoinPage';
---
<Layout
title="Join the Community - Ignis Network"
description="Join our community of zen-seekers and cat lovers worldwide. Connect with fellow meditators on their mindfulness journey."
canonicalPath="/join"
>
<JoinPage client:load />
</Layout>

12
src/pages/privacy.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import Layout from '../layouts/Layout.astro';
import PrivacyPage from '../components/PrivacyPage';
---
<Layout
title="Privacy Policy - Ignis Network"
description="Learn how Ignis Network handles your data and protects your privacy. Our commitment to keeping your information safe."
canonicalPath="/privacy"
>
<PrivacyPage client:load />
</Layout>