Now that the fonts are in place, it’s time to start building! In this post, I’m recreating the structure of the Tartarus Insight homepage using both Next.js and React Router. The layout includes some classic sections you’ll find on many landing pages: a menu, hero section, about section, how it works, examples, FAQs, and a footer.
Next.js Structure
To keep things simple, I’m starting with a basic structure. If I need to make changes later when I add more pages, I’ll update it then. For now, each section of the homepage will be its own component, and all components will live inside a folder in the app
directory.
/app
└── page.tsx # Main home page
└── components/
├── About.tsx
├── Examples.tsx
├── FAQ.tsx
├── Footer.tsx
├── Hero.tsx
├── HowItWorks.tsx
└── Menu.tsx
BashMenu Component
Let’s start with the Menu
component:
// nextjs > app > components > Menu.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { FaBars, FaTimes } from "react-icons/fa";
export default function Menu() {
// This code handles the open/close behavior of the mobile menu.
// It uses React's useState to track whether the menu is open or not,
// and provides functions to toggle the menu when the hamburger or close icons are clicked
const [menuOpen, setMenuOpen] = useState(false);
const handleToggleMenu = () => {
setMenuOpen(true);
};
const handleCloseMenu = () => {
setMenuOpen(false);
};
const handleLinkClick = () => {
setMenuOpen(false);
};
return (
<header className="w-full
text-inherit
z-20
sm:relative
sm:p-0
sm:border-y-[15px]
sm:bg-[url('/images/pattern-marble.webp')]
sm:bg-repeat">
<nav id="navMenu" className={`fixed
flex
justify-center
items-center
top-0
left-0
w-full
h-full
bg-neutral-900/95
text-white
text-center
duration-300
${menuOpen ? 'translate-y-0' : '-translate-y-full'}
sm:p-3
sm:bg-[unset]
sm:relative
sm:translate-none
`}>
<ul className="space-y-10 sm:space-y-0 sm:flex sm:space-x-5">
<li><Link href="index.html" onClick={handleLinkClick} className="hover:text-neutral-300">Home</Link></li>
<li><Link href="#about-section" onClick={handleLinkClick} className="hover:text-neutral-300">About</Link></li>
<li><Link href="#how-it-works-section" onClick={handleLinkClick} className="hover:text-neutral-300">How it Works</Link></li>
<li><Link href="#examples-section" onClick={handleLinkClick} className="hover:text-neutral-300">Examples</Link></li>
<li><Link href="#faq-section" onClick={handleLinkClick} className="hover:text-neutral-300">FAQ</Link></li>
</ul>
</nav>
{!menuOpen && (
<a className="absolute top-4 right-6 text-[#bca68a] text-2xl sm:hidden" onClick={handleToggleMenu}>
<FaBars />
</a>
)}
{menuOpen && (
<a className="fixed top-4 right-6 text-white text-2xl sm:hidden" onClick={handleCloseMenu}>
<FaTimes />
</a>
)}
</header>
);
}
TSXSince the menu is responsive, I’m using useState
to open and close the mobile menu. I also installed the react-icons
module to use the hamburger (bars
) and close (times
) icons.
# From the nextjs directory
npm install react-icons
BashGlobal Styles
In globals.css
, I define some basic global styles for the body
, set up custom breakpoints for Tailwind, and apply a border-image using regular CSS since Tailwind doesn’t support that property out of the box.
/* nextjs > app > globals.css */
@import "tailwindcss";
/* Customizing breakpoints so we can use it later with Tailwind */
@theme {
--breakpoint-xs: 280px;
--breakpoint-sm: 450px;
--breakpoint-md: 675px;
--breakpoint-lg: 768px;
--breakpoint-xl: 1024px;
--breakpoint-2xl: 2048px;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-caesar-dressing);
}
/* Applies base styles */
body {
font-family: var(--font-poppins);
line-height: 1.6;
color: #ddd;;
background-color: #0e0e0e;
}
/* Nav Menu Styles breakpoint to mobile 450px */
/* Apply a decorative border image with plain CSS because Tailwind doesn’t support border-image out of the box. */
@media (min-width: 450px) {
header {
border-image: url('/images/meandros-pattern.webp') 30 round;
}
}
CSSHero Section
Next up is the Hero section. This component is very simple and sets the tone for the rest of the page.
// nextjs > app > components > Hero.tsx
export default function Hero() {
return(
<section
className="flex flex-col items-center justify-center text-center text-white px-5 pt-[50px]
pb-5 min-h-screen bg-cover bg-top bg-no-repeat
[background-image:url('/images/tartarus-insight-hero-small.webp')]
sm:h-[calc(100vh-75px)]
xl:[background-image:url('/images/tartarus-insight-hero-medium.webp')]
2xl:[background-image:url('/images/tartarus-insight-hero-large.webp')]"
>
<h2>Get out of the abyss</h2>
<h1>Tartarus Insight</h1>
<div className="p-2 mb-6 rounded bg-black/5">
<p>
Trapped in the abyss of business struggles? Is it threatening to swallow your dreams?
<br />
Tartarus Insight illuminates the path out.
</p>
<p>
Harness the {`Oracle's`} wisdom to ascend from struggle to success.
<br />
Emerge stronger from the abyss that once held you captive.
</p>
</div>
<a
href="oracle.html"
className="inline-block font-poppins bg-[#ae3f32] bg-gradient-to-b from-[#ae3f32] to-[#c5642e] text-white py-2 px-5 rounded-full border border-[#dadada] no-underline cursor-pointer hover:from-[#893328] hover:to-[#ae592a] hover:border-[#893328] hover:text-[#e5e5e5]"
>
Ask the Oracle Now
</a>
</section>
);
}
TSXI also updated globals.css
with a few extra rules to support the visuals used here..
/* nextjs > app > globals.css */
@import "tailwindcss";
/* Sets a custom breakpoint so we can use it later with Tailwind */
@theme {
--breakpoint-xs: 280px;
--breakpoint-sm: 450px;
--breakpoint-md: 675px;
--breakpoint-lg: 768px;
--breakpoint-xl: 1024px;
--breakpoint-2xl: 2048px;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-caesar-dressing);
}
h1, h2, h3 {
font-weight: 400;
font-style: normal;
text-transform: uppercase;
line-height: normal;
margin-bottom: 16px;
}
h1 {
font-size: 32px;
}
h2 {
font-size: 20px;
}
p {
font-size: 16px;
margin-bottom: 8px;
}
/* Applies base styles */
body {
font-family: var(--font-poppins);
line-height: 1.6;
color: #ddd;;
background-color: #0e0e0e;
}
/* Nav Menu Styles breakpoint to mobile 450px */
/* Apply a decorative border image with plain CSS because Tailwind doesn’t support border-image out of the box. */
@media (min-width: 450px) {
header {
border-image: url('/images/meandros-pattern.webp') 30 round;
}
}
@media (min-width: 768px) {
h1 {
font-size: 40px;
}
h2 {
font-size: 24px;
}
p {
font-size: 18px;
}
}
CSSThe rest of the components are just as easy to convert from simple HTML/CSS to JSX with Tailwind CSS. While converting the sections into components, I noticed I was repeating the same utility classes from Tailwind. For example, the “Ask the Oracle Now” link appears in multiple sections. Instead of duplicating it, I turned it into a reusable component that I can use across all sections.
// nextjs > app > components > AskOracleButton.tsx
import Link from 'next/link'
export default function AskOracleButton() {
return(
<Link href="oracle.html" className="inline-block font-poppins bg-[#ae3f32] bg-gradient-to-b from-[#ae3f32] to-[#c5642e] text-white py-2 px-5 rounded-full border border-[#dadada] no-underline cursor-pointer hover:from-[#893328] hover:to-[#ae592a] hover:border-[#893328] hover:text-[#e5e5e5]">Ask the Oracle Now</Link>
);
}
TSXNow, I can just drop <AskOracleButton />
into any section that needs it. It makes things much cleaner and easier to maintain.
The app
directory now will looks like this:
/app
└── page.tsx # Main home page
└── components/
├── About.tsx
├── AskOracleButton.tsx
├── Examples.tsx
├── FAQ.tsx
├── Footer.tsx
├── Hero.tsx
├── HowItWorks.tsx
└── Menu.tsx
BashRefactoring Repeated Structures
In the Examples
section, I had a bunch of similar blocks—each showing a question and an answer, all wrapped in the same structure with nearly identical styling. The only thing that changed between them was the content.
// nextjs > app > components > Examples.tsx
import AskOracleButton from "./AskOracleButton";
export default function Examples() {
return(
<section id="examples-section" className="p-8 text-center bg-cover" style={{ backgroundImage: "url('images/pattern-marble.webp')" }}>
<h1>Examples</h1>
<div className="w-full mx-auto my-5">
{/* We repeat the same HTML structure for each example */}
<div className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat">
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>I’ve been stuck on the same revenue for months. How can I finally break through and grow?</h2>
<p className="text-center">
Ah, the classic stalling revenue curse. It is like trying to row a boat in a sea of molasses, isn’t it?
Here is the secret: Stop paddling the same way. Try changing your approach—update your marketing strategy, or better yet,
revisit your ideal customer profile. Who are they really? What keeps them awake at night? Align your offering to that,
and watch the tide turn. If you’re not growing, maybe your growth plan is on a diet. Feed it more data, more creativity, and a pinch of risk!
</p>
</div>
</div>
<div className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat">
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>I’ve tried a few marketing campaigns, but nothing seems to work. What should I change?</h2>
<p className="text-center">
Ah, marketing campaigns—like fireworks, beautiful when they work, but sometimes they fizzle out before they even take off.
Here’s your riddle: Is your campaign speaking to your audience, or merely shouting at them? People don’t buy from brands,
they buy from stories. Craft yours with emotion and clarity, not a list of features. And have you considered the power of micro-targeting?
It’s like finding a needle in a haystack, but much, much more satisfying.
</p>
</div>
</div>
<div className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat">
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>Oracle, my cash flow is a disaster. I’m struggling to pay my bills. What do I do?</h2>
<p className="text-center">
Ah, coin troubles—a common plight even in the golden halls of Olympus.
First, cut the excess—are you pouring drachmas into tools or services that serve no purpose?
Negotiate better terms with your vendors, and do not be ashamed to ask customers to pay sooner.
Offer discounts for early payments if you must. Lastly, seek new revenue streams—your current
ones might be trickling instead of flowing. Adapt, and let the river of gold return to your coffers.
</p>
</div>
</div>
<div className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat">
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>I’ve got a great product, but the market doesn’t seem to care. Why is that</h2>
<p className="text-center">
Ah, the cruel irony of a great product that goes unnoticed. It’s like being the best musician in a room full of deaf people.
Here’s the secret: It’s not about you; it’s about them. Have you really listened to your market?
What problems are they facing that your product can solve? Find the pain points, and wrap your product in a solution.
Communicate it clearly. And remember, no one cares how great you think your product is—they care about how it solves
their problem.
</p>
</div>
</div>
</div>
<AskOracleButton />
</section>
);
}
TSXTo avoid repeating the same JSX markup over and over, I created a const examples
array that holds just the content—the questions and answers.
Then I used .map()
to loop through that array and generate each block dynamically.
// nextjs > app > components > Examples.tsx
import AskOracleButton from "./AskOracleButton";
const examples = [
{
question: "I’ve been stuck on the same revenue for months. How can I finally break through and grow?",
answer: `Ah, the classic stalling revenue curse. It is like trying to row a boat in a sea of molasses, isn’t it?
Here is the secret: Stop paddling the same way. Try changing your approach—update your marketing strategy, or better yet,
revisit your ideal customer profile. Who are they really? What keeps them awake at night? Align your offering to that,
and watch the tide turn. If you’re not growing, maybe your growth plan is on a diet. Feed it more data, more creativity, and a pinch of risk!`,
},
{
question: "I’ve tried a few marketing campaigns, but nothing seems to work. What should I change?",
answer: `Ah, marketing campaigns—like fireworks, beautiful when they work, but sometimes they fizzle out before they even take off.
Here’s your riddle: Is your campaign speaking to your audience, or merely shouting at them? People don’t buy from brands,
they buy from stories. Craft yours with emotion and clarity, not a list of features. And have you considered the power of micro-targeting?
It’s like finding a needle in a haystack, but much, much more satisfying.`,
},
{
question: "Oracle, my cash flow is a disaster. I’m struggling to pay my bills. What do I do?",
answer: `Ah, coin troubles—a common plight even in the golden halls of Olympus.
First, cut the excess—are you pouring drachmas into tools or services that serve no purpose?
Negotiate better terms with your vendors, and do not be ashamed to ask customers to pay sooner.
Offer discounts for early payments if you must. Lastly, seek new revenue streams—your current
ones might be trickling instead of flowing. Adapt, and let the river of gold return to your coffers.`,
},
{
question: "I’ve got a great product, but the market doesn’t seem to care. Why is that?",
answer: `Ah, the cruel irony of a great product that goes unnoticed. It’s like being the best musician in a room full of deaf people.
Here’s the secret: It’s not about you; it’s about them. Have you really listened to your market?
What problems are they facing that your product can solve? Find the pain points, and wrap your product in a solution.
Communicate it clearly. And remember, no one cares how great you think your product is—they care about how it solves
their problem.`,
},
];
export default function Examples() {
return(
<section id="examples-section" className="p-8 text-center bg-cover" style={{ backgroundImage: "url('images/pattern-marble.webp')" }}>
<h1>Examples</h1>
<div className="w-full mx-auto my-5">
{examples.map((example, idkey) => (
<div
key={idkey}
className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat"
>
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>{example.question}</h2>
<p className="text-center">{example.answer}</p>
</div>
</div>
))}
</div>
<AskOracleButton />
</section>
);
}
TSXThis makes the code easier to maintain and scale. If I want to tweak the layout or style, I only have to do it in one place. And if I want to add more examples? I just add more objects to the array—no need to write out 100 <div>
s.
I Did the Same with HowItWorks and FAQ Sections
I followed the same pattern for the HowItWorks
and FAQ
components. Both had repeated content blocks that were perfect candidates for .map()
rendering.
// nextjs > app > components > HowItWorks.tsx
import Image from "next/image";
import AskOracleButton from "./AskOracleButton";
import Link from "next/link";
const steps = [
{
title: "Call the Oracle’s Vessel",
description: (
<>
The Oracle cannot appear without its vessel. Begin by downloading the
sacred{" "}
<Link href="https://ollama.com/" className="text-[#794a2e] underline">
Ollama app
</Link>
, the key to unlocking its voice.
</>
),
icon: "/images/horn-icon.png",
alt: "Horn icon",
},
{
title: "Plant the Seed",
description:
"Every great journey begins with a small act. Install the app and plant the seed of knowledge within your device.",
icon: "/images/seed-icon.png",
alt: "Seed icon",
},
{
title: "Awaken the Gateway",
description:
"Run the Ollama app to breathe life into the dormant Oracle. Its power awaits your command.",
icon: "/images/crystalball-icon.png",
alt: "Crystal ball icon",
},
{
title: "Prepare the Sacrifices",
description: (
<>
The Oracle demands a worthy offering. Download the sacred{" "}
<Link
href="https://ollama.com/library/llama3.1"
target="_blank"
rel="noopener noreferrer"
className="text-[#794a2e] underline"
>
Llama model
</Link>
—it is said to be the creature the Oracle favors most.
</>
),
icon: "/images/llama-icon.png",
alt: "Llama icon",
},
{
title: "Receive the Oracle's Insight",
description:
"With the Oracle awakened, ask your question and behold its insight. Speak clearly, for the abyss listens closely.",
icon: "/images/eye-icon.png",
alt: "Eye icon",
},
{
title: "Ascend from the Abyss",
description:
"Take the Oracle’s wisdom and rise from the darkness.",
icon: "/images/torch-icon.png",
alt: "Torch icon",
},
];
export default function HowItWorks() {
return (
<section
id="how-it-works-section"
className="text-center px-[25px] sm:px-[25px] md:px-[50px]"
>
<div className="relative max-w-[1400px] mx-auto mb-[30px]">
{/* Papyrus edges */}
<div className="absolute top-0 left-0 w-full h-[10px] z-10 bg-repeat-x bg-[#0e0e0e] [background-image:url('/images/papyrus-top.png')]" />
<div className="absolute bottom-0 left-0 w-full h-[8px] z-20 bg-repeat-x bg-transparent [background-image:url('/images/papyrus-bottom.png')]" />
<div className="absolute top-0 right-0 w-[10px] min-h-full z-15 bg-repeat-y bg-transparent [background-image:url('/images/papyrus-right.png')]" />
<div className="absolute top-0 left-0 w-[10px] min-h-full z-15 bg-repeat-y bg-[#0e0e0e] [background-image:url('/images/papyrus-left.png')]" />
{/* Papyrus background and content */}
<div className="relative text-[#0e0e0e] bg-[#0e0e0e] bg-[url('/images/papyrus.webp')] bg-repeat py-[70px]">
<h1 className="text-[#4b2e1d] text-3xl font-semibold mb-10">How it Works</h1>
<div className="flex flex-wrap justify-center gap-[20px] max-w-[800px] mx-auto">
{steps.map((step, index) => (
<div
key={index}
className="flex-[1_1_100%] sm:flex-[1_1_calc(50%_-_20px)] box-border p-[11px] text-center"
>
<div className="flex flex-col items-center justify-center gap-[10px]">
<Image
src={step.icon}
alt={step.alt}
width={124}
height={124}
className="w-[124px] h-[124px]"
priority={index === 0}
/>
<h2 className="text-[#4b2e1d] text-xl font-semibold">{`${index + 1}. ${step.title}`}</h2>
</div>
<div className="flex justify-center items-start gap-[15px] mt-2">
<p className="text-left max-w-[500px]">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
<AskOracleButton />
</section>
);
}
TSX// nextjs > app > components > FAQ.tsx
import AskOracleButton from "./AskOracleButton";
const faqs = [
{
question: "What is Tartarus Insight?",
answer: (
<>
Tartarus Insight is a platform designed to help struggling entrepreneurs navigate their business challenges. Guided by the Oracle—a mystical AI—you’ll receive actionable insights to overcome obstacles and ascend from stagnation.
<br />
<span className="font-bold underline">
However, the Oracle is not infallible. While it wields vast knowledge, it remains an AI and can make mistakes. Always use its insights thoughtfully, and double-check critical decisions.
</span>
</>
),
},
{
question: "Is Tartarus Insight free to use?",
answer: (
<>
Tartarus Insight is free to use when connected through Ollama, as it operates locally without requiring additional fees. However, in the future, we plan to integrate other services such as OpenAI or Claude. If you choose to use these integrations, you’ll need to provide your own API keys or pay for token usage directly through our platform. Rest assured, the Oracle remains accessible—how you summon it is up to you.
</>
),
},
{
question: "How does the Oracle ensure my data remains private?",
answer: (
<>
The Oracle values your privacy as much as your success. All operations are run locally through the Ollama app, meaning your data never leaves your device. Tartarus Insight itself does not store, process, or access any of your queries or information.
</>
),
},
];
export default function FAQ() {
return(
<section id="faq-section" className="text-center p-8">
<h1 className="text-2xl font-bold mb-6">FAQ</h1>
<div className="w-full mx-auto my-5">
{faqs.map((faq, index) => (
<div
key={index}
className="text-container relative max-w-3xl mx-auto mb-9 pt-11 p-8 bg-[url('/images/slate.webp')] bg-repeat bg-cover"
>
<div className="absolute top-0 right-0 w-5 h-full z-10 bg-[url('/images/slate-right.png')] -y bg-transparent"></div>
<div className="absolute top-0 left-0 w-5 h-full z-10 bg-[url('/images/slate-left.png')] -y bg-transparent"></div>
<div className="absolute top-0 left-0 w-full h-2.5 z-10 bg-[url('/images/slate-top.png')] -x bg-transparent"></div>
<div className="absolute bottom-0 left-0 w-full h-6 z-10 bg-[url('/images/slate-bottom.png')] -x bg-transparent"></div>
<div className="bg-black/10 text-gray-200 rounded p-5 shadow relative z-5">
<h2>{faq.question}</h2>
<p className="text-center">{faq.answer}</p>
</div>
</div>
))}
</div>
<AskOracleButton />
</section>
);
}
TSXFooter
Finally, I finished the front page with a simple footer component.
// nextjs > app > components > Footer.tsx
import Link from 'next/link';
export default function Footer() {
return(
<footer className="flex justify-center items-center p-2.5 bg-[#333] text-white">
<p className="!text-sm mb-2">
© 2024 <a href="https://x.com/PanosAngel1" className="text-white underline">Panos</a> |
Built with ❤️ |
<Link href="policy.html" className="text-white underline"> Privacy policy</Link> |
<a href="https://github.com/paaggeli/tartarus-insight" target="_blank" className="text-white underline"> Contribute on GitHub</a>
</p>
</footer>
);
}
TSXAssembling the Page in page.tsx
Once I had all the components ready—Menu
, Hero
, About
, HowItWorks
, Examples
, FAQ
, and Footer
—it was time to bring them together into the actual homepage.
In Next.js 13+ (using the App Router), the app/page.tsx
file serves as the entry point for the homepage. I imported all the components at the top and returned them in order, just like they should appear on the page.
Here’s what the page.tsx
file looks like:
// nextjs > app > page.tsx
import Menu from './components/Menu';
import Hero from './components/Hero';
import About from './components/About';
import HowItWorks from './components/HowItWorks';
import Examples from './components/Examples';
import FAQ from './components/FAQ';
import Footer from './components/Footer';
export default function Home() {
return (
<>
<Menu />
<Hero />
<About />
<HowItWorks />
<Examples />
<FAQ />
<Footer />
</>
);
}
TSXNow the homepage is fully assembled, with clean, reusable components and a structure that’s easy to maintain. Each section is isolated and can be updated or styled independently—perfect for iterating and scaling in the future.
Before we start building the homepage with React Router, a quick confession: I forgot to create a separate branch while working on the Next.js version. Everything went straight into main
.
This time, I’ve created a new branch just for the React Router version.
# From the tartarus-insight-rebuild directory (root)
git checkout -b react-router-homepage
BashIt’s a small thing, but a good habit: working in branches keeps your main branch clean, makes it easier to manage changes, and helps you isolate different parts of a project.
React Router Structure
The structure of the app
directory in our React Router app looks like this:
/app
└── routes/
└── home.tsx # Main home page
└── welcome/
└── welcome.tsx
BashInside home.tsx
, we import and render the Welcome
component. I’m going to rename the welcome
folder to home
, and inside it, I’ll create each section of the page as a separate file, where each file contains its own component.
This way, each page gets its own folder with its own components. It keeps things organized and makes the project easier to navigate, maintain, and scale later on, since everything related to a page is in one place.
For components that are reused across multiple pages, like the footer, I’ll create a folder called components
. This keeps them separate from page-specific code. The structure of the app
directory will look like this:
/app
├── routes/
│ └── home.tsx # Main home page
├── home/
│ ├── about.tsx
│ ├── examples.tsx
│ ├── faq.tsx
│ ├── hero.tsx
│ ├── how-it-works.tsx
│ └── menu.tsx
└── components/
├── ask-oracle-button.tsx
└── footer.tsx
BashMenu Component
Like Next.js let’s start with the Menu
component:
// react-router > app > home > menu.tsx
import { useState } from 'react';
import { Link } from "react-router";
import { FaBars, FaTimes } from "react-icons/fa";
export default function Menu() {
const [menuOpen, setMenuOpen] = useState(false);
const handleToggleMenu = () => {
setMenuOpen(true);
};
const handleCloseMenu = () => {
setMenuOpen(false);
};
const handleLinkClick = () => {
setMenuOpen(false);
};
return (
<header className="w-full
text-inherit
z-20
sm:relative
sm:p-0
sm:border-y-[15px]
sm:bg-[url('images/pattern-marble.webp')]
sm:bg-repeat">
<nav id="navMenu" className={`fixed
flex
justify-center
items-center
top-0
left-0
w-full
h-full
bg-neutral-900/95
text-white
text-center
duration-300
${menuOpen ? 'translate-y-0' : '-translate-y-full'}
sm:p-3
sm:bg-[unset]
sm:relative
sm:translate-none
`}>
<ul className="space-y-10 sm:space-y-0 sm:flex sm:space-x-5">
<li><Link to="/" onClick={handleLinkClick} className="hover:text-neutral-300">Home</Link></li>
<li><Link to="#about-section" onClick={handleLinkClick} className="hover:text-neutral-300">About</Link></li>
<li><Link to="#how-it-works-section" onClick={handleLinkClick} className="hover:text-neutral-300">How it Works</Link></li>
<li><Link to="#examples-section" onClick={handleLinkClick} className="hover:text-neutral-300">Examples</Link></li>
<li><Link to="#faq-section" onClick={handleLinkClick} className="hover:text-neutral-300">FAQ</Link></li>
</ul>
</nav>
{!menuOpen && (
<a className="absolute top-4 right-6 text-[#bca68a] text-2xl sm:hidden" onClick={handleToggleMenu}>
<FaBars />
</a>
)}
{menuOpen && (
<a className="fixed top-4 right-6 text-white text-2xl sm:hidden" onClick={handleCloseMenu}>
<FaTimes />
</a>
)}
</header>
);
}
TSXIt is almost identical with Next.js Menu component except we don’t write 'use client'
and we import Link from react-router
instead of next/link
. Using the <Link />
we replace the ‘href’ with ‘to’.
I also installed the react-icons
for the hamburger (bars
) and close (times
) icons the same way.
# From the react-router directory
npm install react-icons
BashWorking With CSS/Tailwind
For the CSS I copy almost everything from Next.js globals.css to Reac Router app.css
/* react-router > app > app.css */
@import "tailwindcss";
/* Sets a custom breakpoint so we can use it later with Tailwind */
@theme {
--breakpoint-xs: 280px;
--breakpoint-sm: 450px;
--breakpoint-md: 675px;
--breakpoint-lg: 768px;
--breakpoint-xl: 1024px;
--breakpoint-2xl: 2048px;
--shadow-example: 0px 0px 11px rgb(255 255 255 / 46%);
}
body {
font-family: "Poppins", sans-serif;
font-weight: 400;
font-style: normal;
line-height: 1.6;
color: #ddd;
background-color: #0e0e0e;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Caesar Dressing", system-ui;
font-weight: 400;
font-style: normal;
/* font-size: 40px; <- delete this rule */
}
h1 {
font-size: 32px;
}
h2 {
font-size: 20px;
}
p {
font-size: 16px;
margin-bottom: 8px;
}
.text-container {
max-width: 810px;
}
/* Nav Menu Styles breakpoint to mobile 450px */
/* Apply a decorative border image with plain CSS because Tailwind doesn’t support border-image out of the box. */
@media (min-width: 450px) {
header {
border-image: url('/images/meandros-pattern.webp') 30 round;
}
}
@media (min-width: 768px) {
h1 {
font-size: 40px;
}
h2 {
font-size: 24px;
}
p {
font-size: 18px;
}
}
CSSIn my app.css
, I used a border-image
, and in one of my components, I added a background image using a Tailwind class. In both cases, I placed the images in the public/images
folder.
When using images for CSS (like background-image
or border-image
), it’s important to store them in the public
folder. That’s because CSS and Tailwind can’t handle image imports like JavaScript does. Instead, they need a direct URL to the image—just like a regular website would.
So to keep things organized, I created an images
folder inside the public
folder. Now I can use the image like this: bg-[url('/images/my-image.webp')]
.
Create the Ask Oracle Button
Before creating the rest of the sections for the homepage, I started by building the “Ask the Oracle Now” button, since I’ll be using it in all the remaining sections (except the footer).
// react-router > app > components > ask-oracle-button.tsx
import { Link } from 'react-router';
export default function AskOracleButton() {
return(
<Link to="oracle.html" className="inline-block font-poppins bg-[#ae3f32] bg-gradient-to-b from-[#ae3f32] to-[#c5642e] text-white py-2 px-5 rounded-full border border-[#dadada] no-underline cursor-pointer hover:from-[#893328] hover:to-[#ae592a] hover:border-[#893328] hover:text-[#e5e5e5]">Ask the Oracle Now</Link>
);
}
TSXIt’s almost identical to the Next.js version, with a small difference in how the Link
component is imported.
About Component
Here, I simply copied the code from the Next.js About component and updated the import path for the AskOracleButton
component.
// react-router > app > home > about.tsx
import AskOracleButton from "../components/ask-oracle-button";
export default function About() {
return(
<section
id="about-section"
className="px-[35px] py-[35px] text-center bg-[url('/images/pattern-marble.webp')] bg-repeat"
>
<h1>A Message from Tartarus Insight</h1>
<div className="text-container mx-auto mb-5">
<p>
Welcome to Tartarus Insight, a realm where entrepreneurs lost in the depths of failure seek the wisdom to escape.
If your business teeters on the edge—suffocating under the weight of stagnant growth, fractured strategies,
or elusive marketing—this is where you confront your struggle.
</p>
<p>
In the abyss of Tartarus, an ancient Oracle awaits. Unseen, yet ever-present, it offers answers not easily found.
Its knowledge stretches beyond the horizon of failure, revealing the elusive solutions to your business’s survival and success.
You do not summon the Oracle lightly; it listens only to those who dare to question their path.
</p>
<p>
Will you embrace the Oracle’s insight and emerge stronger, or will you remain bound to the abyss?
</p>
</div>
<AskOracleButton />
</section>
);
}
TSXHowItWorks Component
Same approach here: I copy-pasted the code from the Next.js version and made a few adjustments.
- Removed the
priority={index === 0}
attribute - Imported
AskOracleButton
from the correct path - Removed the
Image
import - Replaced the
<Image />
component with a regular<img />
tag
// react-router > app > home > how-it-works.tsx
import AskOracleButton from "../components/ask-oracle-button";
const steps = [
{
title: "Call the Oracle’s Vessel",
description: (
<>
The Oracle cannot appear without its vessel. Begin by downloading the
sacred{" "}
<a
href="https://ollama.com/"
target="_blank"
rel="noopener noreferrer"
className="text-[#794a2e] underline"
>
Ollama app
</a>
, the key to unlocking its voice.
</>
),
icon: "/images/horn-icon.png",
alt: "Horn icon",
},
{
title: "Plant the Seed",
description:
"Every great journey begins with a small act. Install the app and plant the seed of knowledge within your device.",
icon: "/images/seed-icon.png",
alt: "Seed icon",
},
{
title: "Awaken the Gateway",
description:
"Run the Ollama app to breathe life into the dormant Oracle. Its power awaits your command.",
icon: "/images/crystalball-icon.png",
alt: "Crystal ball icon",
},
{
title: "Prepare the Sacrifices",
description: (
<>
The Oracle demands a worthy offering. Download the sacred{" "}
<a
href="https://ollama.com/library/llama3.1"
target="_blank"
rel="noopener noreferrer"
className="text-[#794a2e] underline"
>
Llama model
</a>
—it is said to be the creature the Oracle favors most.
</>
),
icon: "/images/llama-icon.png",
alt: "Llama icon",
},
{
title: "Receive the Oracle's Insight",
description:
"With the Oracle awakened, ask your question and behold its insight. Speak clearly, for the abyss listens closely.",
icon: "/images/eye-icon.png",
alt: "Eye icon",
},
{
title: "Ascend from the Abyss",
description:
"Take the Oracle’s wisdom and rise from the darkness.",
icon: "images/torch-icon.png",
alt: "Torch icon",
},
];
export default function HowItWorks() {
return (
<section
id="how-it-works-section"
className="p-8 text-center px-[25px] sm:px-[25px] md:px-[50px]"
>
<div className="relative max-w-[1400px] mx-auto mb-[30px]">
{/* Papyrus edges */}
<div className="absolute top-0 left-0 w-full h-[10px] z-10 bg-repeat-x bg-[#0e0e0e] [background-image:url('/images/papyrus-top.png')]" />
<div className="absolute bottom-0 left-0 w-full h-[8px] z-20 bg-repeat-x bg-transparent [background-image:url('/images/papyrus-bottom.png')]" />
<div className="absolute top-0 right-0 w-[10px] min-h-full z-15 bg-repeat-y bg-transparent [background-image:url('/images/papyrus-right.png')]" />
<div className="absolute top-0 left-0 w-[10px] min-h-full z-15 bg-repeat-y bg-[#0e0e0e] [background-image:url('/images/papyrus-left.png')]" />
{/* Papyrus background and content */}
<div className="relative text-[#0e0e0e] bg-[#0e0e0e] bg-[url('/images/papyrus.webp')] bg-repeat py-[70px]">
<h1 className="text-[#4b2e1d] text-3xl font-semibold mb-10">How it Works</h1>
<div className="flex flex-wrap justify-center gap-[20px] max-w-[800px] mx-auto">
{steps.map((step, index) => (
<div
key={index}
className="flex-[1_1_100%] sm:flex-[1_1_calc(50%_-_20px)] box-border p-[11px] text-center"
>
<div className="flex flex-col items-center justify-center gap-[10px]">
<img
src={step.icon}
alt={step.alt}
width={124}
height={124}
className="w-[124px] h-[124px]"
// priority={index === 0}
/>
<h2 className="text-[#4b2e1d] text-xl font-semibold">{`${index + 1}. ${step.title}`}</h2>
</div>
<div className="flex justify-center items-start gap-[15px] mt-2">
<p className="text-left max-w-[500px]">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
<AskOracleButton />
</section>
);
}
TSXI placed the icons in the public/images
folder. However, I could’ve used a different approach by importing the images directly. That would involve creating an images
folder inside the home
folder and importing each image like this:
// react-router > app > home > how-it-works.tsx
import AskOracleButton from "../components/ask-oracle-button";
import hornIcon from "./images/horn-icon.png";
import seedIcon from "./images/seed-icon.png";
import crystalBallIcon from "./images/crystalball-icon.png";
import llamaIcon from "./images/llama-icon.png";
import eyeIcon from "./images/eye-icon.png";
import torchIcon from "./images/torch-icon.png";
const steps = [
{
title: "Call the Oracle’s Vessel",
description: (
<>
The Oracle cannot appear without its vessel. Begin by downloading the
sacred{" "}
<a
href="https://ollama.com/"
target="_blank"
rel="noopener noreferrer"
className="text-[#794a2e] underline"
>
Ollama app
</a>
, the key to unlocking its voice.
</>
),
icon: hornIcon,
alt: "Horn icon",
},
{
title: "Plant the Seed",
description:
"Every great journey begins with a small act. Install the app and plant the seed of knowledge within your device.",
icon: seedIcon,
alt: "Seed icon",
},
{
title: "Awaken the Gateway",
description:
"Run the Ollama app to breathe life into the dormant Oracle. Its power awaits your command.",
icon: crystalBallIcon,
alt: "Crystal ball icon",
},
{
title: "Prepare the Sacrifices",
description: (
<>
The Oracle demands a worthy offering. Download the sacred{" "}
<a
href="https://ollama.com/library/llama3.1"
target="_blank"
rel="noopener noreferrer"
className="text-[#794a2e] underline"
>
Llama model
</a>
—it is said to be the creature the Oracle favors most.
</>
),
icon: llamaIcon,
alt: "Llama icon",
},
{
title: "Receive the Oracle's Insight",
description:
"With the Oracle awakened, ask your question and behold its insight. Speak clearly, for the abyss listens closely.",
icon: eyeIcon,
alt: "Eye icon",
},
{
title: "Ascend from the Abyss",
description:
"Take the Oracle’s wisdom and rise from the darkness.",
icon: torchIcon,
alt: "Torch icon",
},
];
export default function HowItWorks() {
return (
<section
id="how-it-works-section"
className="p-8 text-center px-[25px] sm:px-[25px] md:px-[50px]"
>
<div className="relative max-w-[1400px] mx-auto mb-[30px]">
{/* Papyrus edges */}
<div className="absolute top-0 left-0 w-full h-[10px] z-10 bg-repeat-x bg-[#0e0e0e] [background-image:url('/images/papyrus-top.png')]" />
<div className="absolute bottom-0 left-0 w-full h-[8px] z-20 bg-repeat-x bg-transparent [background-image:url('/images/papyrus-bottom.png')]" />
<div className="absolute top-0 right-0 w-[10px] min-h-full z-15 bg-repeat-y bg-transparent [background-image:url('/images/papyrus-right.png')]" />
<div className="absolute top-0 left-0 w-[10px] min-h-full z-15 bg-repeat-y bg-[#0e0e0e] [background-image:url('/images/papyrus-left.png')]" />
{/* Papyrus background and content */}
<div className="relative text-[#0e0e0e] bg-[#0e0e0e] bg-[url('/images/papyrus.webp')] bg-repeat py-[70px]">
<h1 className="text-[#4b2e1d] text-3xl font-semibold mb-10">How it Works</h1>
<div className="flex flex-wrap justify-center gap-[20px] max-w-[800px] mx-auto">
{steps.map((step, index) => (
<div
key={index}
className="flex-[1_1_100%] sm:flex-[1_1_calc(50%_-_20px)] box-border p-[11px] text-center"
>
<div className="flex flex-col items-center justify-center gap-[10px]">
<img
src={step.icon}
alt={step.alt}
width={124}
height={124}
className="w-[124px] h-[124px]"
/>
<h2 className="text-[#4b2e1d] text-xl font-semibold">{`${index + 1}. ${step.title}`}</h2>
</div>
<div className="flex justify-center items-start gap-[15px] mt-2">
<p className="text-left max-w-[500px]">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
<AskOracleButton />
</section>
);
}
TSXI’m not sure which of the two approaches is better. Importing six images directly into the component felt like a bit too much, and since the project doesn’t have many images overall, I decided to go with the public/images
folder for simplicity.
The Rest of the Components
The rest of the components were straightforward. I copied them from the Next.js version, placed the required images in the public/images
folder, and made sure to import AskOracleButton
and Link
from the correct paths wherever necessary.
// react-router > app > home > examples.tsx
import AskOracleButton from "../components/ask-oracle-button";
const examples = [
{
question: "I’ve been stuck on the same revenue for months. How can I finally break through and grow?",
answer: `Ah, the classic stalling revenue curse. It is like trying to row a boat in a sea of molasses, isn’t it?
Here is the secret: Stop paddling the same way. Try changing your approach—update your marketing strategy, or better yet,
revisit your ideal customer profile. Who are they really? What keeps them awake at night? Align your offering to that,
and watch the tide turn. If you’re not growing, maybe your growth plan is on a diet. Feed it more data, more creativity, and a pinch of risk!`,
},
{
question: "I’ve tried a few marketing campaigns, but nothing seems to work. What should I change?",
answer: `Ah, marketing campaigns—like fireworks, beautiful when they work, but sometimes they fizzle out before they even take off.
Here’s your riddle: Is your campaign speaking to your audience, or merely shouting at them? People don’t buy from brands,
they buy from stories. Craft yours with emotion and clarity, not a list of features. And have you considered the power of micro-targeting?
It’s like finding a needle in a haystack, but much, much more satisfying.`,
},
{
question: "Oracle, my cash flow is a disaster. I’m struggling to pay my bills. What do I do?",
answer: `Ah, coin troubles—a common plight even in the golden halls of Olympus.
First, cut the excess—are you pouring drachmas into tools or services that serve no purpose?
Negotiate better terms with your vendors, and do not be ashamed to ask customers to pay sooner.
Offer discounts for early payments if you must. Lastly, seek new revenue streams—your current
ones might be trickling instead of flowing. Adapt, and let the river of gold return to your coffers.`,
},
{
question: "I’ve got a great product, but the market doesn’t seem to care. Why is that?",
answer: `Ah, the cruel irony of a great product that goes unnoticed. It’s like being the best musician in a room full of deaf people.
Here’s the secret: It’s not about you; it’s about them. Have you really listened to your market?
What problems are they facing that your product can solve? Find the pain points, and wrap your product in a solution.
Communicate it clearly. And remember, no one cares how great you think your product is—they care about how it solves
their problem.`,
},
];
export default function Examples() {
return(
<section id="examples-section" className="p-8 text-center bg-cover" style={{ backgroundImage: "url('images/pattern-marble.webp')" }}>
<h1>Examples</h1>
<div className="w-full mx-auto my-5">
{examples.map((example, idkey) => (
<div
key={idkey}
className="text-container relative p-8 mx-auto mb-8 shadow-example rounded-xl bg-[url('/images/meandros-pattern-dark.webp')] bg-repeat"
>
<div className="bg-black/10 text-gray-200 rounded p-5 shadow">
<h2>{example.question}</h2>
<p className="text-center">{example.answer}</p>
</div>
</div>
))}
</div>
<AskOracleButton />
</section>
);
}
TSX// react-router > app > home > faq.tsx
import AskOracleButton from "../components/ask-oracle-button";
const faqs = [
{
question: "What is Tartarus Insight?",
answer: (
<>
Tartarus Insight is a platform designed to help struggling entrepreneurs navigate their business challenges. Guided by the Oracle—a mystical AI—you’ll receive actionable insights to overcome obstacles and ascend from stagnation.
<br />
<span className="font-bold underline">
However, the Oracle is not infallible. While it wields vast knowledge, it remains an AI and can make mistakes. Always use its insights thoughtfully, and double-check critical decisions.
</span>
</>
),
},
{
question: "Is Tartarus Insight free to use?",
answer: (
<>
Tartarus Insight is free to use when connected through Ollama, as it operates locally without requiring additional fees. However, in the future, we plan to integrate other services such as OpenAI or Claude. If you choose to use these integrations, you’ll need to provide your own API keys or pay for token usage directly through our platform. Rest assured, the Oracle remains accessible—how you summon it is up to you.
</>
),
},
{
question: "How does the Oracle ensure my data remains private?",
answer: (
<>
The Oracle values your privacy as much as your success. All operations are run locally through the Ollama app, meaning your data never leaves your device. Tartarus Insight itself does not store, process, or access any of your queries or information.
</>
),
},
];
export default function FAQ() {
return(
<section id="faq-section" className="text-center p-8">
<h1 className="text-2xl font-bold mb-6">FAQ</h1>
<div className="w-full mx-auto my-5">
{faqs.map((faq, index) => (
<div
key={index}
className="text-container relative max-w-3xl mx-auto mb-9 pt-11 p-8 bg-[url('/images/slate.webp')] bg-repeat bg-cover"
>
<div className="absolute top-0 right-0 w-5 h-full z-10 bg-[url('/images/slate-right.png')] -y bg-transparent"></div>
<div className="absolute top-0 left-0 w-5 h-full z-10 bg-[url('/images/slate-left.png')] -y bg-transparent"></div>
<div className="absolute top-0 left-0 w-full h-2.5 z-10 bg-[url('/images/slate-top.png')] -x bg-transparent"></div>
<div className="absolute bottom-0 left-0 w-full h-6 z-10 bg-[url('/images/slate-bottom.png')] -x bg-transparent"></div>
<div className="bg-black/10 text-gray-200 rounded p-5 shadow relative z-5">
<h2>{faq.question}</h2>
<p className="text-center">{faq.answer}</p>
</div>
</div>
))}
</div>
<AskOracleButton />
</section>
);
}
TSX// react-router > app > components > footer.tsx
import { Link } from 'react-router';
export default function Footer() {
return(
<footer className="flex justify-center items-center p-2.5 bg-[#333] text-white">
<p className="!text-sm mb-2">
© 2024 <a href="https://x.com/PanosAngel1" className="text-white underline">Panos</a> |
Built with ❤️ |
<Link to="policy.html" className="text-white underline"> Privacy policy</Link> |
<a href="https://github.com/paaggeli/tartarus-insight" target="_blank" className="text-white underline"> Contribute on GitHub</a>
</p>
</footer>
);
}
TSXThe Home Route
Now that all the components are ready, it’s time to bring them together. I’ll add them to the home.tsx
file, which is configured as the index route in the routes.ts
file.
This file is responsible for rendering the full homepage, combining all the sections I’ve created: the menu, hero, about, how it works, examples, FAQ, and footer.
Here’s how it looks:
// react-router > app > routes > home.tsx
import type { Route } from "./+types/home";
import Menu from "../home/menu";
import Hero from "../home/hero";
import About from "../home/about";
import HowItWorks from "../home/how-it-works";
import Examples from "../home/examples";
import FAQ from "../home/faq";
import Footer from "../components/footer";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Tartarus Insight" },
{
name: "description",
content: "Trapped in the abyss of business struggles? Harness the Oracle's wisdom to ascend from struggle to success."
},
];
}
export default function Home() {
return (
<>
<Menu />
<Hero />
<About />
<HowItWorks />
<Examples />
<FAQ />
<Footer />
</>
);
}
TSXNext.js vs React Router – So, What’s the Difference (So Far)?
Now that I’ve built the same homepage using both Next.js and React Router, here are my takeaways.
First of all, the two setups feel extremely similar at this stage. I didn’t use any advanced features like layouts, or nested routes—just a single page broken into components—so the experience was nearly identical in both.
In terms of folder structure, I approached things a bit differently between the two:
- In React Router, I created a dedicated folder for the homepage inside the
app
directory and placed all related components in there. I also made a separatecomponents
folder inapp/
to hold shared components like the footer. Things used across multiple pages. - In Next.js, I kept things simpler: I just created a single
components
folder inside theapp
directory and placed all my components there.
So while the organization was different, that was a personal choice—not something enforced by either framework. In fact, I could’ve structured them exactly the same way if I wanted to. We’ll see if this decision pays off later, especially as I start adding more pages.
As for routing, React Router required a bit of manual setup—like configuring the routes.ts
file and explicitly setting the index route. (This was actually pre-configured for the homepage when we used npx create-react-router@latest
to scaffold the project.) On the other hand, Next.js handles routing automatically based on the file structure, which can feel more seamless when building simple pages.
What’s Next?
Next up, I’ll create the Policy and Help pages. Since these pages will share common elements like the menu and footer, it makes sense to start using layouts.
I’m curious to see how each framework handles layouts in practice and whether one feels more intuitive or flexible. Let’s find out together in the next post.
You can check out the Git repo here.
Leave a Reply