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

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
dist
.DS_Store
*.tar.gz
.astro

8
.sisyphus/boulder.json Normal file
View File

@@ -0,0 +1,8 @@
{
"active_plan": "/Users/nvirellia/Projects/ignis-website/.sisyphus/plans/content-refresh.md",
"started_at": "2026-01-25T18:11:10.652Z",
"session_ids": [
"ses_409aab7c7ffeDJJAqWV5ZEQjro"
],
"plan_name": "content-refresh"
}

View File

@@ -0,0 +1,43 @@
### Clowder to Community/Network Replacement
- **Files Modified**: `src/components/HomePage.tsx`, `src/components/JoinPage.tsx`, `src/pages/join.astro`
- **Changes Made**: Replaced all instances of "clowder" with "community" or "network" based on context.
- "Join the Clowder" -> "Join the Network" (HomePage.tsx)
- "Join our clowder" -> "Join our network" (HomePage.tsx)
- "clowderMembers" -> "communityMembers" (JoinPage.tsx)
- "A clowder is a group of cats. Join our community of" -> "A community is a group of people. Join our community of" (JoinPage.tsx)
- "Clowder Members" -> "Community Members" (JoinPage.tsx)
- "Welcome to the Clowder!" -> "Welcome to the Community!" (JoinPage.tsx)
- "Join the Clowder Community" -> "Join the Community" (join.astro)
- **Verification**: `grep -ri "clowder" src` returned no results, confirming all references were removed.
### PawPrint Icon Replacement
- **Files Modified**: `src/components/ZenApp.tsx`, `src/components/JoinPage.tsx`, `src/components/HomePage.tsx`, `src/components/AboutPage.tsx`
- **Changes Made**: Replaced all instances of `PawPrint` icon with `Network` icon from `lucide-react`. This included updating both the import statements and the JSX component usages.
- **Verification**: `grep -r "PawPrint" src` returned no results.
- **Commit Strategy**: Changes were split into two atomic commits to adhere to the principle of small, focused commits:
1. `refactor: replace PawPrint icon with Network icon in ZenApp and JoinPage`
2. `refactor: replace PawPrint icon with Network icon in HomePage and AboutPage`
### Task 5.3: Neutralize HomePage.tsx (Cat text & Testimonials)
- Successfully replaced all specified cat-themed text with professional/tech equivalents.
- Successfully updated the specified image alt text.
- Encountered an issue with `edit` when `oldString` was not unique, resolved by providing more context to the `oldString` parameter.
- Verified changes using `grep -riE "meow|purr|cat|kitten" src/components/HomePage.tsx`, confirming that only acceptable occurrences of "cat" (in image paths and an untargeted quote) remain.
### Neutralized remaining components (About, ZenApp, Join, Explore, Home)
**Changes Made:**
- Modified `src/components/AboutPage.tsx`, `src/components/ZenApp.tsx`, `src/components/JoinPage.tsx`, `src/components/ExplorePage.tsx`, and `src/components/HomePage.tsx`.
- Replaced cat-themed text with professional/tech equivalents in all specified files.
- Replaced "PawPrint" references with "Network" or "Cpu" where appropriate.
- Maintained existing layout and animations.
**Verification:**
- Ran `grep -riE "PawPrint|meow|clowder|kitten" src` which returned no results, confirming the removal of specified cat-themed terms and icons.
- All files were modified as expected.
- The changes were committed with the message: "refactor: neutralize remaining cat-themed content across all components".

View File

@@ -0,0 +1,20 @@
# Learnings - remove-sections
## Conventions
- Target file: `src/components/HomePage.tsx`.
- Components to remove: `Community`, `QuoteSection`.
- Navigation items to remove: `community`, `wisdom`.
## Patterns
- `navLinks` array defines the top navigation.
- `useEffect` scroll listener updates `activeSection` based on element visibility.
- Successfully removed Community and QuoteSection components from src/components/HomePage.tsx.
- Deleted both the usage in the Home component's return block and their respective component definitions.
- Verified that the Home component's return structure remained intact and the Footer component was not affected.
- Successfully removed 'community' and 'wisdom' from `navLinks` array.
- Successfully removed 'community' and 'wisdom' from `sections` array in `useEffect` scroll handler.
- Ensured `navLinks` and `sections` array are consistent.
No unused lucide-react imports found in src/components/HomePage.tsx after the removal of Community and QuoteSection. All imported icons (Heart, Sparkles, Menu, X, Network, Users, BookOpen, Compass, Star, Lock, CheckCircle) are still actively used within the file.

View File

@@ -0,0 +1,167 @@
# Plan: Ignis Network Rebrand & Content Refresh
## Context
### Original Request
Refresh the website with "Ignis Network" branding, specific mission statement, and handle "@ignisnet". Explicit instruction to "not modify styles too much".
### Analysis Findings
- **Tech Stack**: Astro + React + Tailwind CSS.
- **Current State**: "Purrfectly Zen" branding with heavy cat themes (text & icons).
- **Scope**: "Purrfectly Zen" appears in ~14 files (Pages, Components, Metadata).
- **Architecture**:
- Landing: `src/pages/index.astro`
- Components: `src/components/HomePage.tsx` (Hero, Footer)
- Layout: `src/layouts/Layout.astro` (Global Meta)
---
## Work Objectives
### Core Objective
Rebrand the entire application from "Purrfectly Zen" to "Ignis Network", replacing cat-themed content with the provided "Digital Transformation" mission statement.
### Concrete Deliverables
- [ ] Updated Brand Name globally ("Ignis Network")
- [ ] Updated Hero Section (Headline & Description)
- [ ] Updated Footer (Added "@ignisnet" link)
- [ ] Updated Metadata (Titles & Descriptions in Astro files)
- [ ] Neutralized Thematic Elements (Replace Cat icons/text with Tech equivalents where appropriate)
### Definition of Done
- [ ] `grep -r "Purrfectly Zen" .` returns 0 results
- [ ] Hero displays the new mission statement
- [ ] Footer contains "@ignisnet"
### Must Have
- Exact mission statement provided by user.
- Preservation of existing layout and CSS classes (Tailwind).
### Must NOT Have
- Major layout restructuring.
- Changes to color palette (unless requested later).
- Broken links or missing assets.
---
## Verification Strategy
### Manual QA (Primary)
Since this is a content/UI refresh, verification will be done via **static analysis** (grep/reading files) and **manual verification instructions**.
---
## Task Flow
```
1. Global Rename -> 2. Layout & Meta -> 3. Hero Update -> 4. Footer Update -> 5. Theme Cleanup
```
---
## TODOs
- [ ] 1. Global Brand Name Replacement
**What to do**:
- Perform a global search and replace of "Purrfectly Zen" to "Ignis Network".
- Check `package.json`, `README.md`, `src/layouts/Layout.astro`, `src/pages/*.astro`, and `src/components/*.tsx`.
- Ensure case sensitivity is handled (e.g., check for all caps if applicable).
**References**:
- `src/pages/index.astro` - Page Title
- `src/layouts/Layout.astro` - Meta Title template
- `package.json` - Project metadata
**Acceptance Criteria**:
- [ ] `grep -r "Purrfectly Zen" src` returns no results.
- [ ] `grep -r "Ignis Network" src` returns multiple results.
**Parallelizable**: NO (Base step)
- [ ] 2. Update Layout & Metadata
**What to do**:
- Update `src/layouts/Layout.astro` meta description to reflect the new mission (shortened version).
- Update `src/pages/index.astro` meta content.
- Short Mission for Meta: "Redefining the digital frontier with next-generation internet services and advanced AI."
**References**:
- `src/layouts/Layout.astro`
- `src/pages/index.astro`
**Acceptance Criteria**:
- [ ] `grep "Redefining the digital frontier" src/layouts/Layout.astro` returns match.
**Parallelizable**: YES (with 3, 4)
- [ ] 3. Refresh Hero Section (HomePage.tsx)
**What to do**:
- Modify `src/components/HomePage.tsx`.
- **Headline**: Change "Let's find your inner Zen with a cat!" to "Ignis Network".
- **Sub-headline/Description**: Replace with: "To redefine the digital frontier by fusing next-generation internet services with advanced AI and software solutions. Our mission is to accelerate digital transformation, transforming how the world connects, computes, and evolves through intelligent, data-driven technologies."
- **Button/CTA**: Change "Find your zen" to "Get Started" or "Explore Solutions".
**References**:
- `src/components/HomePage.tsx:Hero` section
**Acceptance Criteria**:
- [ ] Content check: File contains the exact mission statement text.
- [ ] No residual "cat" references in the Hero section.
**Parallelizable**: YES (with 2, 4)
- [ ] 4. Update Footer & Navigation
**What to do**:
- Locate Footer in `src/components/HomePage.tsx` (or imported component).
- Add "@ignisnet" as a social link (e.g., X/Twitter).
- Ensure the link is clickable: `href="https://x.com/ignisnet"` (Assumed X based on handle format).
- Update Copyright text to "© 2026 Ignis Network".
**References**:
- `src/components/HomePage.tsx` - Footer section
**Acceptance Criteria**:
- [ ] Footer contains "@ignisnet" text.
- [ ] Footer contains "Ignis Network" copyright.
**Parallelizable**: YES (with 2, 3)
- [ ] 5. Thematic Cleanup (Tech vs Cat)
**What to do**:
- Search for "cat", "kitten", "meow", "purr", "clowder" in `src/components/*.tsx`.
- Replace with neutral/tech terms:
- "clowder" -> "community" or "network"
- "meow" -> "hello" or remove
- "purr" -> "hum" or remove
- **Icons**: If `PawPrint` icon is imported from `lucide-react`, swap it for `Network`, `Cpu`, or `Zap` to match "Ignis" (Fire/Tech).
- _Constraint_: Do not break layout. Only swap if 1:1 replacement is possible.
**References**:
- `src/components/HomePage.tsx`
- `src/components/AboutPage.tsx`
- `src/components/ZenApp.tsx`
**Acceptance Criteria**:
- [ ] `grep -r "meow" src` returns 0 results.
- [ ] `grep -r "PawPrint" src` returns 0 results (if swapped).
**Parallelizable**: NO (Run last to catch stragglers)
---
## Success Criteria
### Final Checklist
- [ ] Branding is "Ignis Network" everywhere.
- [ ] Hero Mission Statement matches user input exactly.
- [ ] Footer includes @ignisnet.
- [ ] No obvious "Cat" text remains in a "Tech" website.
- [ ] Layout remains intact (no broken styles).

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Fauzira Alpiandi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
# 🐱 Ignis Network Template
[![Ignis Network](public/images/demo.png)](#ignis-network)
> Find your zen. One breath, one paw print at a time.
>
> A cat-themed meditation and mindfulness web application designed to help you disconnect from noise and reconnect with what matters: peace, presence, and gentle purrs.
---
This project is a beautiful, interactive, and lightweight meditation app template built with **Astro**, **React**, and **Tailwind CSS**. It's designed for developers who want to jumpstart a mindfulness-focused project with a charming "cat-wisdom" aesthetic.
---
## ✨ Template Features
| Feature | Description |
| --------------------------- | ----------------------------------------------------------- |
| 🧘 **Guided Sessions** | Interactive timer with customizable breathing animations. |
| 🌬️ **Breathing Techniques** | Pre-configured 4-4-4-4 and "Purring Breath" patterns. |
| 📱 **Astro Powered** | Hybrid rendering for speed and SEO optimization. |
| 🎨 **Design System** | Warm, soft color palette with "Fredoka" and "Nunito" fonts. |
| ✨ **Micro-interactions** | Smooth animations powered by Framer Motion. |
| 🆓 **Zero Backend** | Pure client-side logic with `localStorage` persistence. |
---
## 🚀 Technical Stack
- **Framework:** [Astro 5](https://astro.build/)
- **UI Library:** React (integrated via `@astrojs/react`)
- **Styling:** Tailwind CSS (v4 integration)
- **Animations:** Framer Motion
- **Icons:** Lucide React
- **Routing:** Built-in Astro file-based routing + client-side navigation
---
## 🛠️ Getting Started
### 1. Clone & Install
```bash
# Install dependencies
pnpm install # or npm install
```
### 2. Development
```bash
# Start the dev server
npm run dev
```
Visit `http://localhost:5000` to see the magic.
### 3. Build & Deploy
```bash
# Build for production
npm run build
```
---
## 📁 Project Structure
```
/src
├── components/ # React components (HomePage, ZenApp, etc.)
├── layouts/ # Astro layouts (Global styles & SEO)
├── lib/ # Utilities
└── pages/ # Astro routes
/public
└── images/ # Cat illustrations and portraits
```
---
## 🎨 Customization Tips
- **Branding:** Modify `src/layouts/Layout.astro` to change the CSS variables (`--primary`, `--background`) and update global SEO tags.
- **Content:** The main logic lives in `src/components/ZenApp.tsx`. You can easily add more breathing patterns or cat affirmations there.
- **Assets:** Swap out the images in `public/images/` to change the theme from cats to anything else (dogs, nature, etc.).
---
## 📝 License
[MIT](LICENSE) — Built for the community. Feel free to use, modify, and share.
Built with ❤️ and purrs. 🐾

13
astro.config.mjs Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
integrations: [react()],
vite: { plugins: [tailwindcss()] },
server: {
host: '0.0.0.0',
port: 5000,
allowedHosts: true,
},
});

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "purrfectly-zen",
"private": true,
"description": "Find your zen. One breath, one paw print at a time.",
"license": "MIT",
"author": "Fauzira Alpiandi <fauziralpiandi.dev@gmail.com>",
"scripts": {
"astro": "astro",
"build": "astro build",
"dev": "astro dev",
"format": "prettier --write .",
"preview": "astro preview"
},
"prettier": {
"bracketSpacing": true,
"singleQuote": true,
"plugins": [
"prettier-plugin-astro",
"prettier-plugin-tailwindcss"
],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
},
"dependencies": {
"@astrojs/react": "^4.4.2",
"@icons-pack/react-simple-icons": "^13.8.0",
"astro": "^5.16.9",
"canvas-nest.js": "^2.0.4",
"clsx": "^2.1.1",
"framer-motion": "^12.26.2",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^25.0.8",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"prettier": "^3.8.0",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}

4301
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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>

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "astro/tsconfigs/strict",
"include": ["**/*.ts", "**/*.tsx", "src/**/*.astro"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@assets/*": ["./attached_assets/*"]
}
}
}