added dockerfile, style changes

This commit is contained in:
Fortura Developer 2025-08-20 12:59:31 -06:00
parent fc8fb230e0
commit 57accaca11
51 changed files with 2281 additions and 1406 deletions

View File

@ -1,11 +0,0 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["-y", "shadcn@canary", "registry:mcp"],
"env": {
"REGISTRY_URL": "https://animate-ui.com/r/registry.json"
}
}
}
}

23
.env.local.example Normal file
View File

@ -0,0 +1,23 @@
# Environment variables for Fortura Data Solutions website
# Next.js environment
NODE_ENV=development
# API endpoints (if applicable)
# NEXT_PUBLIC_API_URL=http://localhost:3001/api
# Analytics (if self-hosted)
# NEXT_PUBLIC_PLAUSIBLE_DOMAIN=your-domain.com
# NEXT_PUBLIC_PLAUSIBLE_API_HOST=https://plausible.your-domain.com
# Contact form submission endpoint
# CONTACT_FORM_ENDPOINT=https://n8n.your-domain.com/webhook/contact-form
# Calendly integration (if applicable)
# CALENDLY_URL=https://calendly.com/your-calendly-url
# Email service (if applicable)
# NEXT_PUBLIC_EMAIL_SERVICE_ENDPOINT=https://api.email-service.com/send
# Feature flags
# NEXT_PUBLIC_FEATURE_FLAG_NEW_DESIGN=true

0
061cecebb592 Normal file
View File

0
46db0b326091 Normal file
View File

0
4cf371dfcf97 Normal file
View File

0
631b88c73ff0 Normal file
View File

0
7cdef5a33192 Normal file
View File

0
8fccc2e8a28d Normal file
View File

View File

@ -1,348 +0,0 @@
# Contributing to Animate UI
Thank you for your interest in **contributing to Animate UI**! Your support is highly appreciated, and we look forward to your contributions. This guide will help you understand the project structure and provide detailed instructions for adding a new component or effect to Animate UI.
**Note:** You only need to modify a few files to add a new component, and it should take you around 10 minutes to complete.
## Getting Started
### Fork and Clone the Repository
#### 1. Fork the Repository
Click [here](https://github.com/animate-ui/animate-ui/fork) to fork the repository.
#### 2. Clone your Fork to Your Local Machine
```bash
git clone https://github.com/<YOUR_USERNAME>/animate-ui.git
```
#### 3. Navigate to the Project Directory
```bash
cd animate-ui
```
#### 4. Create a New Branch for Your Changes
```bash
git checkout -b my-branch
```
#### 5. Install Dependencies
```bash
pnpm i
```
#### 6. Run the Project
```bash
pnpm dev
```
## Edit a Component
If you need to modify a component to correct or improve it, you must :
- add a screenshot (photo or video as appropriate) of before and after the modification
- clearly explain why you made the modification
### Edit the code
Edit the component in the `registry` folder. Don't forget to adapt the demo and documentation if necessary.
You shouldn't change your behavior completely unless there's a good reason.
### Build the Registry
To update the registry, run the following command:
```bash
pnpm registry:build
```
## Adding a New Component
The addition of a new component must comply with certain rules:
- The component must be animated in some way (css, motion, ...).
- You can't just copy/paste component code from other libraries. You can be inspired by a component, but it must have added value. For example, I took Shadcn's components and animated them. So I didn't copy and paste the component, I added something to it.
- If you take inspiration from a component (CodePen, another library, etc.), remember to add the “Credits” section to your documentation. It's important to respect the work of other developers.
To submit your component, please include a demo video in the MR. Once the component has been submitted, it must be validated by @imskyleen.
To **add a new component to Animate UI**, you will need to update several files. Follow these steps:
### Create the Component
#### Basics
Create your main component in `apps/www/registry/[category]/my-component/index.tsx`.
```tsx title="my-component/index.tsx"
'use client';
import * as React from 'react';
type MyComponentProps = {
myProps: string;
} & React.ComponentProps<'div'>;
function MyComponent({ myProps, ...props }) {
return <div {...props}>{/* Your component */}</div>;
}
export { MyComponent, type MyComponentProps };
```
#### Registry item
Create a `apps/www/registry/[category]/my-component/registry-item.json` file to export your component :
```json title="my-component/registry-item.json"
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "my-component",
"type": "registry:ui",
"title": "My Component",
"description": "My Component Description",
"dependencies": [...],
"devDependencies": [...],
"files": [
{
"path": "registry/[category]/my-component/index.tsx",
"type": "registry:ui",
"target": "components/animate-ui/demo/[category]/my-component.tsx"
}
]
}
```
### Create a demo
#### Basics
Create your demo in `apps/www/registry/demo/[category]/my-component/index.tsx`.
```tsx title="my-component/index.tsx"
'use client';
import {
MyComponent,
type MyComponentProps,
} from '@/registry/[category]/my-component';
type MyComponentDemoProps = {
myProps: string;
} & MyComponentProps;
export const MyComponentDemo = ({ myProps }) => {
return <MyComponent myProps={myProps} />;
};
```
#### Registry item
```json title="my-component/registry-item.json"
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "my-component-demo",
"type": "registry:ui",
"title": "My Component Deo",
"description": "Demo showing my component",
"registryDependencies": ["https://animate-ui.com/r/my-component"],
"files": [
{
"path": "registry/demo/[category]/my-component/index.tsx",
"type": "registry:ui",
"target": "components/[category]/demo/my-component.tsx"
}
]
}
```
#### Add a Tweakpane
You can add a Tweakpane allowing users to play with your demo props.
Your demo must accept the props you want in your tweakpane :
```tsx title="my-component-demo/index.tsx"
import { MyComponent } from '@/registry/[category]/my-component';
type MyComponentDemoProps = {
props1: number;
props2: number;
props3: string;
props4: string;
props5: boolean;
};
export function MyComponentDemo({
props1,
props2,
props3,
props4,
props5,
}: MyComponentDemoProps) {
return <MyComponent />;
}
```
You must then specify the demo props information in your demo's `registry-item.json` file:
```json title="my-component-demo/registry-item.json"
{
...
"meta": {
"demoProps": {
"MyComponent": {
"props1": { "value": 700, "min": 0, "max": 2000, "step": 100 },
"props2": { "value": 0 },
"props3": { "value": "foo" },
"props4": {
"value": "center",
"options": {
"start": "start",
"center": "center",
"end": "end"
}
},
"props5": { "value": true }
}
}
},
...
}
```
**You need to run `pnpm registry:build` to see the updated tweakpane in the demo.**
#### How to use `demoProps`
##### Number
Simple number input:
```json
"myNumber": { "value": 10 }
```
Slider:
```json
"myNumber": { "value": 10, "min": 0, "max": 100, "step": 1 }
```
Select:
```json
"myNumber": {
"value": 10,
"options": {
"Big": 30,
"Medium": 20,
"Small": 10
}
}
```
##### String
Simple text input:
```json
"myString": { "value": "Hello World" }
```
Select:
```json
"myNumber": {
"value": "small",
"options": {
"Big": "big",
"Medium": "medium",
"Small": "small"
}
}
```
##### Boolean
```json
"myBoolean": { "value": true }
```
### Update the Documentation Sidebar
Add your component to the documentation sidebar by updating the file `content/docs/meta.json`.
```json title="meta.json"
{
"title": "Animate UI",
"root": true,
"pages": [
...,
"[category]/my-component"
...
]
}
```
### Create the Component Documentation
Create an MDX file to document your component in `content/docs/[category]/my-component.mdx`.
```mdx
---
title: My Component
description: Description for the new component
author:
name: your name
url: https://link-to-your-profile.com
new: true
---
<ComponentPreview name="my-component-demo" />
## Installation
<ComponentInstallation name="my-component" />
## Usage
[Basic usage of the component]
## Props
<TypeTable
type={{
myProps: {
description: 'Description for my props',
type: 'string',
required: true,
},
}}
/>
## Credits
- Credits to [you](https://link-to-your-profile.com) for creating the component
```
### Build the Registry
To update the registry, run the following command:
```bash
pnpm registry:build
```
## Ask for Help
If you need any assistance or have questions, please feel free to open a [GitHub issue](https://github.com/animate-ui/animate-ui/issues/new). We are here to help!
Thank you again for your contribution to Animate UI! We look forward to seeing your improvements and new components.

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
# Use Node.js 20 as the base image
FROM node:20-alpine
# Set the working directory
WORKDIR /app
# Install pnpm globally
RUN npm install -g pnpm
# Copy package.json and pnpm-lock.yaml files
COPY package.json pnpm-lock.yaml ./
# Copy turbo.json
COPY turbo.json ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy the rest of the application code
COPY . .
# Build the application
RUN pnpm build
# Expose port 3000
EXPOSE 3000
# Start the application
CMD ["pnpm", "start"]

144
README.md
View File

@ -30,16 +30,148 @@ We built our platform for high-demand environments like VFX studios, where petab
This repository contains the code for the Fortura Data Solutions marketing website. It's built with Next.js, TypeScript, and Tailwind CSS.
For information on how to set up the development environment, run the site locally, or contribute, please see our [contributing guide](CONTRIBUTING.md).
### Prerequisites
- Node.js >= 20
- pnpm >= 8
- Docker (optional, for containerized deployment)
### Environment Variables
To run this project, you'll need to set up the following environment variables:
```bash
# Create a .env.local file in the root directory
cp .env.local.example .env.local
```
Then edit the `.env.local` file to include your configuration:
```env
# Next.js environment
NODE_ENV=development
# API endpoints (if applicable)
# NEXT_PUBLIC_API_URL=http://localhost:3001/api
# Analytics (if self-hosted)
# NEXT_PUBLIC_PLAUSIBLE_DOMAIN=your-domain.com
# NEXT_PUBLIC_PLAUSIBLE_API_HOST=https://plausible.your-domain.com
# Contact form submission endpoint
# CONTACT_FORM_ENDPOINT=https://n8n.your-domain.com/webhook/contact-form
# Calendly integration (if applicable)
# CALENDLY_URL=https://calendly.com/your-calendly-url
```
### Development
To run the development server:
```bash
# Install dependencies
pnpm install
# Run the development server
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
### Docker Deployment
To build and run the application using Docker:
1. Build the Docker image:
```bash
docker build -t fortura-website -f apps/www/Dockerfile .
```
2. Run the container:
```bash
docker run -p 3000:3000 --env-file .env.local fortura-website
```
Alternatively, use docker-compose:
```bash
docker-compose up --build
```
The application will be available at [http://localhost:3000](http://localhost:3000).
### Production Build
To create a production build:
```bash
# Build the application
pnpm build
# Start the production server
pnpm start
```
## Troubleshooting
### TS2742 from Fumadocs generated files (Next.js 15 type-check)
TypeScript can flag the generated Fumadocs output in `apps/www/.source/index.ts` because the inferred type of `docs` references private internals from `fumadocs-mdx/dist/...`. Next 15 enforces strict type portability and fails with TS2742.
Fast unblock (recommended and safe): exclude generated files from type-check.
1) Ensure `apps/www/tsconfig.json` excludes the generated folder and relaxes lib checks:
```json
{
"compilerOptions": {
"skipLibCheck": true
},
"exclude": [
".source",
".source/**/*"
]
}
```
2) Clean and rebuild the app:
```bash
rm -rf apps/www/.source
pnpm -C apps/www build
```
Proper fix (long-term): align Fumadocs + Zod and regenerate.
1) Upgrade deps (workspace-wide):
```bash
pnpm -w up fumadocs-mdx@latest fumadocs-core@latest fumadocs-ui@latest @types/mdx@latest
pnpm -w up zod@^4
```
2) Regenerate the `.source` output:
```bash
rm -rf apps/www/.source
pnpm -C apps/www build
```
3) If the generator still emits a generic that triggers TS2742, enforce a portable annotation in `apps/www/.source/index.ts` (temporary):
```ts
import { _runtime } from 'fumadocs-mdx';
type DocsData = ReturnType<typeof _runtime.docs>;
export const docs: DocsData = _runtime.docs(/* generated arrays */);
// or fallback: export const docs: unknown = _runtime.docs(/* generated arrays */);
```
Note: Keeping `.source` excluded from type-checks is fine even after upgrades; it also speeds up CI.
## Documentation
Detailed documentation for Fortura's services, migration guides, and operational procedures is available to clients on our internal portal.
## Contributing
Visit our [contributing guide](CONTRIBUTING.md) to learn how to contribute to the website's codebase.
## License
This website's code is licensed under a proprietary license. See [LICENSE.md](LICENSE.md) for details.
This website's code is licensed under a proprietary license. See [LICENSE.md](LICENSE.md) for details.

0
Running Normal file
View File

0
Using Normal file
View File

0
animate-ui@0.0.1 Normal file
View File

13
apps/www/.source-typed.ts Normal file
View File

@ -0,0 +1,13 @@
// Typed facade for the generated .source/index.ts
// Keep this file portable and avoid referencing internal fumadocs types.
// Minimal surface used by the app (extend if you need more methods)
export type DocsApi = {
toFumadocsSource(): unknown;
};
// Defer loading the generated module to runtime and avoid importing its types
export const docs: DocsApi = {
// eslint-disable-next-line @typescript-eslint/no-var-requires
toFumadocsSource: () => require('./.source/index').docs.toFumadocsSource(),
};

26
apps/www/Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Use Node.js 20 as the base image
FROM node:20-alpine
# Set the working directory
WORKDIR /app
# Install pnpm globally
RUN npm install -g pnpm
# Copy package.json and pnpm-lock.yaml files
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy the rest of the application code
COPY . .
# Build the application
RUN pnpm build
# Expose port 3000
EXPOSE 3000
# Start the application
CMD ["pnpm", "start"]

View File

@ -6,348 +6,256 @@ import * as React from 'react';
// Import layout components from our new shared `ui` library
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
// import { ScrollProgressBar } from '@workspace/ui/components/animate-ui/scroll-progress';
// import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@workspace/ui/components/animate-ui/collapsible';
import { motion, useScroll, useTransform } from 'motion/react';
// import { ParallaxLayers } from '@workspace/ui/components/animate-ui/parallax-layers';
// Import the useTone hook to access the global tone setting
import { useTone } from '@workspace/ui/hooks/use-tone';
// Import Lucide icons for the value proposition section
import { DollarSignIcon, WrenchIcon, MapIcon, LockIcon, CloudOffIcon, ZapIcon } from 'lucide-react';
import { PlaceholderIcon } from '@workspace/ui/components/icons';
import { WrenchIcon, LockIcon, CloudOffIcon, ZapIcon } from 'lucide-react';
// Import UI components from our shared library
import { Button } from '@workspace/ui/components/ui';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import Image from 'next/image';
// Import new content components
import { MetricsKPI } from '@workspace/ui/components/content';
import { LogosMarquee } from '@workspace/ui/components/content';
import { Steps } from '@workspace/ui/components/content';
import { FeatureCard, CaseStudyCard } from '@workspace/ui/components/content/cards';
// import { Steps } from '@workspace/ui/components/content';
import { TrustSection } from '@workspace/ui/components/content';
import { FAQAccordion } from '@workspace/ui/components/interactive';
// import { FAQAccordion } from '@workspace/ui/components/interactive';
import { FinalCTASection } from '@workspace/ui/components/content';
import { CostReductionChart } from '@workspace/ui/components/content/cost-reduction-chart';
export default function HomePage() {
const { tone } = useTone(); // Access the current tone
// Content definitions based on tone
const heroHeadline = tone === 'direct'
? "Own your infrastructure. Slash your bill."
: "Cut waste, keep performance.";
const heroSubcopyLine1 = tone === 'direct'
? "Your hosting provider is fucking you. Cloud rent bleeds $25k$500k/mo."
: "Cloud rent is bleeding your budget dry.";
const heroSubcopyLine2 = tone === 'direct'
? "Own the hardware. Keep the performance. Kill the waste."
: "Own your stack. Colocate it. Keep the performance, ditch the waste.";
const heroHeadline =
tone === 'direct'
? 'Own your infrastructure. Slash your bill.'
: 'Cut waste, keep performance.';
const heroSubcopyLine1 =
tone === 'direct'
? 'Your hosting provider is fucking you. Cloud rent bleeds $25k$500k/mo.'
: 'Cloud rent is bleeding your budget dry.';
const heroSubcopyLine2 =
tone === 'direct'
? 'Own the hardware. Keep the performance. Kill the waste.'
: 'Own your stack. Colocate it. Keep the performance, ditch the waste.';
const valuePropositions = tone === 'direct' ? [
// Core value propositions based on core selling points
const coreValuePropositions = [
{
icon: <DollarSignIcon className="size-6" />,
title: "Stop getting scammed.",
description: "Cloud rent bleeds $25k$500k/mo. Own the hardware. Keep the performance. Kill the waste."
title: 'Stop getting scammed',
description:
'Cloud providers are bleeding you dry. We help you cut costs by up to 65% by moving away from endless rental fees to a one-time hardware purchase plus low colocation costs.',
},
{
icon: <WrenchIcon className="size-6" />,
title: "Build on what works.",
description: "Keep your stack. We wire it together and automate the boring."
title: 'Keep your existing stack',
description:
"We don't force you to replace your tools. Instead, we integrate what works and automate the boring parts, so your team can focus on what they do best.",
},
{
icon: <MapIcon className="size-6" />,
title: "Pave desire paths.",
description: "We embed with your team, shape the system to how they actually work."
}
] : [
{
icon: <DollarSignIcon className="size-6" />,
title: "Cut waste, keep performance.",
description: "Move from renting to owning. Dramatically reduce monthly costs without sacrificing speed or reliability."
title: 'Work with your actual workflows',
description:
'We embed with your team, map your real processes, and build automation around how you actually work—not how consultants think you should work.',
},
{
icon: <WrenchIcon className="size-6" />,
title: "Integrate, don't replace.",
description: "We work with your existing tools and workflows. No disruptive rip-and-replace projects."
},
{
icon: <MapIcon className="size-6" />,
title: "Build around real workflows.",
description: "We map your actual processes, then build automation that fits like a glove."
}
];
// Mock data for LogosMarquee
const logos = Array(6).fill(null).map((_, i) => ({
name: `Client ${i + 1}`,
icon: <PlaceholderIcon className="size-8 md:size-10" />
}));
// Mock data for Process Steps
const processSteps = [
// Benefits of owned infrastructure
const benefits = [
{
title: "Discovery",
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
weDo: "Analyze costs, identify waste, and map technical dependencies."
title: 'Ownership',
description:
'Own your hardware and data. No more vendor lock-in or unpredictable pricing.',
},
{
title: "Shadow & Map",
description: "We observe how your teams actually work and map desire paths.",
youDo: "Participate in workflow observations and workshops.",
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
title: 'Performance',
description:
'Direct-attached storage and local networks outperform virtualized cloud infrastructure.',
},
{
title: "Architect",
description: "We design a tailored, owned infrastructure solution.",
youDo: "Review and approve the proposed architecture and migration plan.",
weDo: "Specify hardware, select software stack, and design for performance and cost."
title: 'Privacy',
description:
'Your data never leaves your control. AI models run locally with zero data leakage.',
},
{
title: "Pilot",
description: "We build and test a critical component in the new environment.",
youDo: "Provide feedback on performance, usability, and integration.",
weDo: "Deploy, test, and iterate on the pilot component with your team."
title: 'Cost Savings',
description:
"Break-even in under 12 months. After that, it's all savings.",
},
{
title: "Migrate",
description: "We execute the planned migration with zero downtime.",
youDo: "Validate functionality and performance post-migration.",
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
},
{
title: "Operate",
description: "We manage the infrastructure, you focus on your core business.",
youDo: "Use the system and provide ongoing feedback for improvements.",
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
}
];
// Mock data for Case Studies
const caseStudies = [
{
title: "VFX Studio Migration",
description: "Migrated a major VFX studio from cloud rendering to a colocated HPC cluster, reducing costs by 65%.",
kpi1: { value: "65%", label: "Cost ↓" },
kpi2: { value: "2x", label: "Performance ↑" },
ctaText: "See build",
ctaHref: "/case-studies/vfx-studio"
},
{
title: "Legal Document Management",
description: "Consolidated scattered legal tech stack into a secure, private Nextcloud instance with AI search.",
kpi1: { value: "$120k/mo", label: "Saved" },
kpi2: { value: "0", label: "Egress Fees" },
ctaText: "See build",
ctaHref: "/case-studies/legal-firm"
},
{
title: "Research Data Pipeline",
description: "Built a local ML training environment for a research institute, eliminating data transfer bottlenecks.",
kpi1: { value: "90%", label: "Time-to-Train ↓" },
kpi2: { value: "Break-even: 8mo", label: "" },
ctaText: "See build",
ctaHref: "/case-studies/research-institute"
}
];
// Mock data for Trust Section
// Trust factors
const trustItems = [
{
icon: <LockIcon className="size-5" />,
text: "Your data. Your hardware. Your rules."
},
{
icon: <CloudOffIcon className="size-5" />,
text: "Colocated. Monitored. Upgradable without downtime."
text: 'Your data. Your hardware. Your rules.',
},
{
icon: <ZapIcon className="size-5" />,
text: "AI-native, privacy-first. No third-party model training."
}
];
// Mock data for FAQ
// (This is just for homepage preview; full FAQ is on /faq page)
const faqItems = [
{
question: "What uptime can I expect?",
answer: "We design for 99.9%+ uptime through redundant hardware, proactive monitoring, and colocation in Tier III+ facilities."
text: 'AI-native, privacy-first. No third-party AI.',
},
{
question: "How is support handled?",
answer: "You get direct access to our engineering team via Slack, email, or phone. We provide 24/7 monitoring and alerting."
}
icon: <CloudOffIcon className="size-5" />,
text: 'No license fees. You own it, forever.',
},
{
icon: <WrenchIcon className="size-5" />,
text: 'Ultimate freedom. Any tool, any stack, reshaped to work together for your unique use-case.',
},
];
// Hero right panel scroll-linked fade
const heroRef = React.useRef<HTMLDivElement | null>(null);
const { scrollYProgress: heroProgress } = useScroll({
target: heroRef,
offset: ['start end', 'end start'],
});
const heroPanelOpacity = useTransform(heroProgress, [0, 1], [1, 0.96]);
const heroPanelY = useTransform(heroProgress, [0, 1], [0, 8]);
return (
<div className="flex flex-col min-h-screen">
<Navbar />
{/* Hero Section */}
<Section className="flex flex-col items-center text-center py-20 md:py-32">
<Container maxWidth="3xl">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
{heroHeadline}
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-2xl">
{heroSubcopyLine1}
</p>
<p className="mt-2 text-lg md:text-xl text-muted-foreground max-w-2xl">
{heroSubcopyLine2}
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" variant="default" asChild>
<a href="/contact">Book Architecture Call</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/pricing">Run My Numbers</a>
</Button>
</div>
{/* KPI Metrics */}
<div className="mt-16 grid grid-cols-1 sm:grid-cols-3 gap-8 max-w-2xl mx-auto">
<MetricsKPI value="$350/mo" label="after year 1" />
<MetricsKPI value="65%" label="avg. cost ↓" />
<MetricsKPI value="12mo" label="typical break-even" />
</div>
</Container>
</Section>
{/* Value Proposition Section */}
<Section background="muted">
<Container>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{valuePropositions.map((item, index) => (
<Card key={index} className="flex flex-col">
<CardHeader>
<div className="mb-2 text-primary">
{item.icon}
</div>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<CardDescription>{item.description}</CardDescription>
</CardContent>
</Card>
))}
</div>
</Container>
</Section>
{/* Proof Strip / Logos Marquee */}
<Section>
<Container>
<h2 className="text-2xl font-bold text-center mb-12">Trusted by forward-thinking teams</h2>
<LogosMarquee logos={logos} />
</Container>
</Section>
{/* Process Snapshot */}
<Section background="muted">
<Container>
<Steps
steps={processSteps.slice(0, 3)} // Show only first 3 steps on homepage
title="Our 6-Step Process"
ctaText="See Full Process"
ctaHref="/process"
/>
</Container>
</Section>
{/* Case Study Rail */}
<Section>
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Proven Results</h2>
<p className="mt-2 text-muted-foreground">
Real outcomes from real migrations.
</p>
</div>
<Grid columns={{ initial: 1, md: 3 }} gap="6">
{caseStudies.map((study, index) => (
<CaseStudyCard
key={index}
title={study.title}
description={study.description}
kpi1={study.kpi1}
kpi2={study.kpi2}
ctaText={study.ctaText}
ctaHref={study.ctaHref}
/>
))}
</Grid>
</Container>
</Section>
{/* Pricing Preview / Own vs Rent */}
<Section background="muted">
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Own vs Rent</h2>
<p className="mt-2 text-muted-foreground max-w-2xl mx-auto">
Cloud is a rental agreement with infinite term. Owned hardware + colocation breaks even in under 12 months.
</p>
</div>
<div className="max-w-4xl mx-auto bg-card border rounded-xl p-6 md:p-8">
<h3 className="text-xl font-semibold mb-4">Why Owned Infrastructure Wins</h3>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Capex:</span> You buy the hardware once. Depreciate over 3-5 years.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Low Opex:</span> Colocation is ~$200/server/mo. Managed ops is ~$500/server/mo.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">No Egress Fees:</span> Move data freely within your network.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">Performance:</span> Direct-attached storage and local networks are faster than VPCs.</span>
</li>
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">No Vendor Lock-in:</span> Standard hardware and open-source software.</span>
</li>
</ul>
<div className="mt-6 p-4 bg-muted rounded-lg">
<p className="text-center font-medium">
Break-even is typically &lt; 12 months. After that, it's pure savings.
{/* Hero Section (banded, two-column, grid background) */}
<AnimatedSection className="bg-grid-dark border-b border-muted">
<Container maxWidth="10xl">
<div
ref={heroRef}
className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center py-20 md:py-28"
>
<div className="text-left">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight tracking-tight text-balance">
{heroHeadline}
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-xl">
{heroSubcopyLine1}
</p>
<p className="mt-3 text-lg md:text-xl text-muted-foreground max-w-xl">
{heroSubcopyLine2}
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4">
<Button size="lg" variant="default" asChild>
<a href="/contact">Book Architecture Call</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/pricing">Run My Numbers</a>
</Button>
</div>
{/* KPI Metrics */}
<div className="mt-14 grid grid-cols-3 gap-6 max-w-xl">
<MetricsKPI value="$350/mo" label="after year 1" />
<MetricsKPI value="65%" label="avg. cost ↓" />
<MetricsKPI value="12mo" label="break-even" />
</div>
</div>
<motion.div
className="relative h-[260px] md:h-[360px] lg:h-[420px] ring-1 ring-border/50"
style={{ opacity: heroPanelOpacity, y: heroPanelY }}
>
<div className="absolute inset-0 rounded-sm border bg-card/50 backdrop-blur-sm" />
{/* Optional decorative hero art (drop /hero/home-hero-art.webp in public) */}
<Image
src="/hero/home-hero-art.webp"
alt=""
aria-hidden
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover rounded-sm opacity-35"
priority={false}
/>
<div className="absolute inset-0 bg-stripes-dark rounded-sm opacity-40" />
</motion.div>
</div>
</Container>
</Section>
</AnimatedSection>
{/* Core Value Propositions - Vertical List */}
<AnimatedSection className="py-20 border-b border-muted/80">
<Container maxWidth="10xl">
<div className="space-y-16">
<h2 className="text-4xl font-bold">Our Approach</h2>
{coreValuePropositions.map((item, index) => (
<a
key={index}
href="#"
className="group block border-b border-muted/80 pb-8 transition-all duration-300 hover:border-foreground/70 last:border-0"
>
<div className="flex justify-between items-start">
<div>
<div className="text-4xl font-light text-muted-foreground mb-4">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h3 className="text-4xl md:text-5xl font-bold transition-colors duration-300 group-hover:text-foreground/90">
{item.title}
<span className="ml-3 inline-block opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition">
</span>
</h3>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl transition-colors duration-300 group-hover:text-foreground/80">
{item.description}
</p>
</div>
</div>
</a>
))}
</div>
</Container>
</AnimatedSection>
{/* Cost Reduction Chart */}
<AnimatedSection className="py-20 bg-muted/30 border-b border-muted/80">
<Container maxWidth="10xl">
<CostReductionChart />
</Container>
</AnimatedSection>
{/* Benefits Section */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="text-center mb-20">
<h2 className="text-4xl font-bold">Why Owned Infrastructure?</h2>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl mx-auto">
The benefits of moving from cloud rental to hardware ownership
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
{benefits.map((benefit, index) => (
<div key={index} className="space-y-6">
<div className="text-5xl font-light text-muted-foreground">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h3 className="text-3xl font-bold">{benefit.title}</h3>
<p className="text-xl text-muted-foreground">
{benefit.description}
</p>
</div>
))}
</div>
</Container>
</AnimatedSection>
{/* Trust Section */}
<Section>
<Container>
<TrustSection items={trustItems} title="Your Data, Your Rules" />
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<TrustSection
items={trustItems}
title="Your Data, Your Rules"
description="We move your business off the cloud to custom servers you own and control. No more paying rent forever—colocate your hardware in vetted, local data centers for low ongoing costs with full ownership."
/>
</Container>
</Section>
{/* FAQ Accordion Preview */}
<Section background="muted">
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl font-bold">Common Questions</h2>
<p className="mt-2 text-muted-foreground">
Answers to key concerns about migrating and self-hosting.
</p>
</div>
<FAQAccordion />
<div className="mt-8 text-center">
<Button variant="link" asChild>
<a href="/faq">See All FAQs</a>
</Button>
</div>
</Container>
</Section>
</AnimatedSection>
{/* Final CTA */}
<Section>
<Container>
<AnimatedSection className="py-20 border-t border-muted">
<Container maxWidth="10xl">
<FinalCTASection
title="Ready to cut your cloud bill?"
description="Book a free architecture call to see how much you can save."
@ -357,7 +265,7 @@ export default function HomePage() {
secondaryCtaHref="/download-blueprint" // Placeholder
/>
</Container>
</Section>
</AnimatedSection>
<Footer />
</div>

View File

@ -3,127 +3,166 @@
'use client';
import * as React from 'react';
import { ExternalLinkIcon, AwardIcon, UsersIcon, ZapIcon } from 'lucide-react';
import { AwardIcon, UsersIcon, ZapIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { FinalCTASection } from '@workspace/ui/components/content';
import Image from 'next/image';
export default function AboutPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">About Fortura</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We built it for VFX first. That's why it works for everyone else.
{/* Hero Section */}
<AnimatedSection className="border-b border-muted">
<Container maxWidth="10xl">
<div className="text-center py-20 md:py-28">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">
About Fortura
</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl mx-auto">
We built it for VFX first. That&apos;s why it works for everyone else.
</p>
</div>
<Section>
<Container>
<div className="flex flex-col md:flex-row gap-8 items-center">
<div className="flex-1">
<h2 className="text-2xl font-bold mb-4">Our Origin Story</h2>
<p className="text-muted-foreground mb-4">
Fortura was born in the high-stakes world of Visual Effects (VFX) production. VFX studios operate on razor-thin margins, require massive computational power, and deal with enormous datasets (petabytes) that need to move fast.
</p>
<p className="text-muted-foreground mb-4">
We saw firsthand how the cloud billing model was financially unsustainable for these studios. They were hemorrhaging money on egress fees, rent for virtual machines that sat idle half the time, and paying premium prices for "enterprise" support that often lagged behind community forums.
</p>
<p className="text-muted-foreground">
So, we built a better way. A way to own the hardware, colocate it for low-cost power and bandwidth, and automate the hell out of operations so a small team could manage massive infrastructure.
</p>
</div>
<div className="flex-1 flex justify-center">
{/* Placeholder for an image or graphic */}
<div className="bg-muted border rounded-xl w-full h-64 flex items-center justify-center">
<span className="text-muted-foreground">VFX Studio Image/Graphic</span>
</div>
</div>
</div>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6 text-center">Why That Matters</h2>
<Grid columns={{ initial: 1, md: 3 }} gap="6">
<Card>
<CardHeader>
<AwardIcon className="size-8 text-primary mb-2" />
<CardTitle>Petabyte Scale</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
We cut our teeth on systems handling hundreds of terabytes to petabytes of data. This means we know how to design for scale, performance, and cost-efficiency from the ground up.
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader>
<UsersIcon className="size-8 text-primary mb-2" />
<CardTitle>Low Budgets</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
VFX studios have tight budgets. Every dollar spent on infrastructure is a dollar not spent on creative talent or compute rendering the next shot. This discipline forces us to find the absolute cheapest, most efficient solutions.
</CardDescription>
</CardContent>
</Card>
<Card>
<CardHeader>
<ZapIcon className="size-8 text-primary mb-2" />
<CardTitle>Real-Time Demands</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
In VFX, deadlines are absolute. A render farm that's down is a studio that's losing money. This means we obsess over uptime, redundancy, and zero-downtime upgrades. What works for 24/7 VFX pipelines works for any business-critical application.
</CardDescription>
</CardContent>
</Card>
</Grid>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Our Mission</h2>
<p className="text-muted-foreground mb-4 max-w-3xl">
Our mission is to fundamentally change how enterprises think about infrastructure. The cloud model of "rent forever" is a bad deal for anyone with significant scale or data. It's a wealth transfer from innovative businesses to cloud providers.
</p>
<p className="text-muted-foreground mb-6 max-w-3xl">
We show you how to own your infrastructure, colocate it for performance and cost, and operate it with a tiny fraction of the overhead of a cloud provider. You keep the performance, you keep the data, and you keep the savings.
</p>
<Callout variant="default">
<span className="font-semibold">"Your hosting provider is fucking you."</span> This isn't just a tagline; it's a technical reality. We're here to help you take control.
</Callout>
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Ready to take control?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Book a free architecture call to see how much you can save by moving from renting to owning.
</p>
<Button asChild size="lg">
<a href="/contact">
Book Architecture Call
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
</Container>
</Section>
</AnimatedSection>
{/* Origin Story */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="flex flex-col lg:flex-row gap-16 items-center">
<div className="flex-1">
<h2 className="text-4xl font-bold mb-8">Our Origin Story</h2>
<div className="space-y-6">
<p className="text-xl text-muted-foreground">
Fortura was born in the high-stakes world of Visual Effects
(VFX) production. VFX studios operate on razor-thin margins,
require massive computational power, and deal with enormous
datasets (petabytes) that need to move fast.
</p>
<p className="text-xl text-muted-foreground">
We saw firsthand how the cloud billing model was financially
unsustainable for these studios. They were hemorrhaging money
on egress fees, rent for virtual machines that sat idle half
the time, and paying premium prices for &quot;enterprise&quot; support
that often lagged behind community forums.
</p>
<p className="text-xl text-muted-foreground">
So, we built a better way. A way to own the hardware, colocate
it for low-cost power and bandwidth, and automate the hell out
of operations so a small team could manage massive
infrastructure.
</p>
</div>
</div>
<div className="flex-1 flex justify-center">
<div className="relative w-full h-80 rounded-xl overflow-hidden border">
<Image
src="/about/about-vfx-studio.jpg"
alt="VFX studio render and review environment"
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover"
priority={false}
/>
</div>
</div>
</div>
</Container>
</AnimatedSection>
{/* Why That Matters */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<h2 className="text-4xl font-bold mb-16 text-center">
Why That Matters
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="space-y-4 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary mb-4">
<AwardIcon className="size-6" />
</div>
<h3 className="text-2xl font-bold">Petabyte Scale</h3>
<p className="text-muted-foreground">
We cut our teeth on systems handling hundreds of terabytes to
petabytes of data. This means we know how to design for scale,
performance, and cost-efficiency from the ground up.
</p>
</div>
<div className="space-y-4 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary mb-4">
<UsersIcon className="size-6" />
</div>
<h3 className="text-2xl font-bold">Low Budgets</h3>
<p className="text-muted-foreground">
VFX studios have tight budgets. Every dollar spent on
infrastructure is a dollar not spent on creative talent or
compute rendering the next shot. This discipline forces us to
find the absolute cheapest, most efficient solutions.
</p>
</div>
<div className="space-y-4 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary mb-4">
<ZapIcon className="size-6" />
</div>
<h3 className="text-2xl font-bold">Real-Time Demands</h3>
<p className="text-muted-foreground">
In VFX, deadlines are absolute. A render farm that&apos;s down is a
studio that&apos;s losing money. This means we obsess over uptime,
redundancy, and zero-downtime upgrades. What works for 24/7 VFX
pipelines works for any business-critical application.
</p>
</div>
</div>
</Container>
</AnimatedSection>
{/* Our Mission */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<h2 className="text-4xl font-bold mb-16">Our Mission</h2>
<div className="space-y-8 max-w-4xl">
<p className="text-xl text-muted-foreground">
Our mission is to fundamentally change how enterprises think about
infrastructure. The cloud model of &quot;rent forever&quot; is a bad deal
for anyone with significant scale or data. It&apos;s a wealth transfer
from innovative businesses to cloud providers.
</p>
<p className="text-xl text-muted-foreground">
We show you how to own your infrastructure, colocate it for
performance and cost, and operate it with a tiny fraction of the
overhead of a cloud provider. You keep the performance, you keep
the data, and you keep the savings.
</p>
<Callout variant="default" className="text-xl p-8">
<span className="font-semibold">
&quot;Your hosting provider is fucking you.&quot;
</span>{' '}
This isn&apos;t just a tagline; it&apos;s a technical reality. We&apos;re here to
help you take control.
</Callout>
</div>
</Container>
</AnimatedSection>
{/* Final CTA */}
<AnimatedSection className="py-20 border-t border-muted">
<Container maxWidth="10xl">
<FinalCTASection
title="Ready to take control?"
description="Book a free architecture call to see how much you can save by moving from renting to owning."
primaryCtaText="Book Architecture Call"
primaryCtaHref="/contact"
/>
</Container>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -1,4 +1,5 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source);
// Fast unblock: relax types here to avoid coupling to full LoaderOutput shape
export const { GET } = createFromSource(source as any);

View File

@ -0,0 +1,89 @@
"use client";
import * as React from "react";
import { Container } from "@workspace/ui/components/layout";
import { Navbar, Footer } from "@workspace/ui/components/layout";
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
export default function CaseStudiesPage() {
const caseStudies = [
{
title: "VFX Studio Migration",
description:
"Migrated a major VFX studio from cloud rendering to a colocated HPC cluster, reducing costs by 65%.",
kpi1: { value: "65%", label: "Cost ↓" },
kpi2: { value: "2x", label: "Performance ↑" },
ctaText: "See build",
ctaHref: "/case-studies/vfx-studio",
},
{
title: "Legal Document Management",
description:
"Consolidated scattered legal tech stack into a secure, private Nextcloud instance with AI search.",
kpi1: { value: "$120k/mo", label: "Saved" },
kpi2: { value: "0", label: "Egress Fees" },
ctaText: "See build",
ctaHref: "/case-studies/legal-firm",
},
{
title: "Research Data Pipeline",
description:
"Built a local ML training environment for a research institute, eliminating data transfer bottlenecks.",
kpi1: { value: "90%", label: "Time-to-Train ↓" },
kpi2: { value: "Break-even: 8mo", label: "" },
ctaText: "See build",
ctaHref: "/case-studies/research-institute",
},
];
return (
<div className="flex flex-col min-h-screen">
<Navbar />
{/* Hero Section */}
<AnimatedSection className="border-b border-muted">
<Container maxWidth="10xl">
<div className="py-20 md:py-28">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">Case Studies</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl">
Context constraints architecture migration outcomes. Real numbers, real stacks.
</p>
</div>
</Container>
</AnimatedSection>
{/* Case Studies */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="space-y-8 max-w-4xl">
{caseStudies.map((study, index) => (
<a
key={index}
href={study.ctaHref}
className="group block w-full border-b border-muted pb-8 transition-all duration-300 hover:border-foreground/70"
>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<div className="text-4xl font-light text-muted-foreground mb-2">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h2 className="text-3xl font-bold">{study.title}</h2>
<p className="text-xl text-muted-foreground mt-3 max-w-3xl">{study.description}</p>
</div>
<div className="flex items-center gap-8 shrink-0">
<div className="text-lg"><span className="font-semibold">{study.kpi1.value}</span> <span className="text-muted-foreground">{study.kpi1.label}</span></div>
<div className="text-lg"><span className="font-semibold">{study.kpi2.value}</span> <span className="text-muted-foreground">{study.kpi2.label}</span></div>
<span className="text-primary text-lg">See build </span>
</div>
</div>
</a>
))}
</div>
</Container>
</AnimatedSection>
<Footer />
</div>
);
}

View File

@ -7,8 +7,8 @@ import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { Button } from '@workspace/ui/components/ui';
import { Input } from '@workspace/ui/components/ui';
import { Textarea } from '@workspace/ui/components/ui';
@ -54,122 +54,122 @@ export default function ContactPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Contact Us</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
{/* Hero Section */}
<AnimatedSection className="border-b border-muted">
<Container maxWidth="10xl">
<div className="text-center py-20 md:py-28">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">Contact Us</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl mx-auto">
Book a free architecture call, request a cost estimate, or ask a question.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
</Container>
</AnimatedSection>
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 max-w-6xl mx-auto">
{/* Contact Form */}
<Card>
<CardHeader>
<CardTitle>Send us a message</CardTitle>
<CardDescription>
Fill out the form and we'll get back to you as soon as possible.
</CardDescription>
</CardHeader>
<CardContent>
{submitSuccess ? (
<div className="bg-green-500/10 border border-green-500/30 rounded-md p-4 text-green-500">
<p className="font-semibold">Message Sent!</p>
<p>Thank you for reaching out. We'll get back to you soon.</p>
<div className="space-y-6">
<h2 className="text-3xl font-bold">Send us a message</h2>
<p className="text-xl text-muted-foreground">
Fill out the form and we&apos;ll get back to you as soon as possible.
</p>
{submitSuccess ? (
<div className="bg-green-500/10 border border-green-500/30 rounded-md p-6 text-green-500">
<p className="font-semibold text-xl">Message Sent!</p>
<p className="mt-2">Thank you for reaching out. We&apos;ll get back to you soon.</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="name" className="text-lg">Name *</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="mt-2 p-4 text-lg"
/>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="company">Company</Label>
<Input
id="company"
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</div>
<div>
<Label htmlFor="message">Message *</Label>
<Textarea
id="message"
rows={5}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
/>
</div>
{submitError && (
<div className="text-red-500 text-sm">{submitError}</div>
)}
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? 'Sending...' : 'Send Message'}
</Button>
</form>
)}
</CardContent>
</Card>
<div>
<Label htmlFor="email" className="text-lg">Email *</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-2 p-4 text-lg"
/>
</div>
<div>
<Label htmlFor="company" className="text-lg">Company</Label>
<Input
id="company"
value={company}
onChange={(e) => setCompany(e.target.value)}
className="mt-2 p-4 text-lg"
/>
</div>
<div>
<Label htmlFor="message" className="text-lg">Message *</Label>
<Textarea
id="message"
rows={5}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
className="mt-2 p-4 text-lg"
/>
</div>
{submitError && (
<div className="text-red-500 text-lg">{submitError}</div>
)}
<Button type="submit" disabled={isSubmitting} size="lg" className="w-full mt-4">
{isSubmitting ? 'Sending...' : 'Send Message'}
</Button>
</form>
)}
</div>
{/* Calendly Embed and Info */}
<div>
<Card className="mb-6">
<CardHeader>
<CardTitle>Book an Architecture Call</CardTitle>
<CardDescription>
30-minute consultation to discuss your infrastructure and potential savings.
</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4 text-muted-foreground">
During this call, we'll review your current setup, identify cost-cutting opportunities, and outline a migration path.
</p>
{/* Calendly Inline Embed */}
<div className="bg-muted rounded-lg p-4 text-center">
<p className="text-muted-foreground">Calendly Embed Placeholder</p>
<p className="text-sm mt-2">(This would be a real Calendly widget)</p>
</div>
</CardContent>
</Card>
<div className="space-y-8">
<div className="space-y-6 p-8 rounded-lg border border-muted bg-background">
<h2 className="text-3xl font-bold">Book an Architecture Call</h2>
<p className="text-xl text-muted-foreground">
30-minute consultation to discuss your infrastructure and potential savings.
</p>
<p className="text-muted-foreground">
During this call, we&apos;ll review your current setup, identify cost-cutting opportunities, and outline a migration path.
</p>
{/* Calendly Inline Embed */}
<div className="bg-muted rounded-lg p-8 text-center">
<p className="text-muted-foreground text-xl">Calendly Embed Placeholder</p>
<p className="text-sm mt-4">(This would be a real Calendly widget)</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Security Inquiries</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4 text-muted-foreground">
For sensitive security-related questions, please use our dedicated channel.
</p>
<Button asChild variant="outline" className="w-full">
<a href="mailto:security@fortura.ai">
security@fortura.ai
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</CardContent>
</Card>
<div className="space-y-6 p-8 rounded-lg border border-muted bg-background">
<h2 className="text-3xl font-bold">Security Inquiries</h2>
<p className="text-muted-foreground">
For sensitive security-related questions, please use our dedicated channel.
</p>
<Button asChild variant="outline" size="lg" className="w-full">
<a href="mailto:security@fortura.ai">
security@fortura.ai
<ExternalLinkIcon className="ml-2 size-5" />
</a>
</Button>
</div>
</div>
</div>
</Container>
</Section>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -32,7 +32,7 @@ export default async function Page(props: {
return (
<DocsPage
toc={page.data.toc}
toc={page.data.toc as any}
full={page.data.full}
footer={{ component: <Footer /> }}
tableOfContent={{ style: 'clerk' }}

View File

@ -17,7 +17,7 @@ export default function Layout({ children }: { children: ReactNode }) {
type: 'icon',
},
]}
tree={source.pageTree}
tree={source.pageTree as any}
themeSwitch={{
component: <ThemeSwitcher />,
}}

View File

@ -6,25 +6,35 @@ import * as React from 'react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { FAQAccordion } from '@workspace/ui/components/interactive';
export default function FAQPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Frequently Asked Questions</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
Answers to common questions about Fortura's approach, process, and services.
{/* Hero Section */}
<AnimatedSection className="border-b border-muted">
<Container maxWidth="10xl">
<div className="py-20 md:py-28">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">Frequently Asked Questions</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl">
Answers to common questions about our approach, process, and services.
</p>
</div>
</Container>
</AnimatedSection>
{/* FAQ Section */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<FAQAccordion />
</Container>
</Section>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -4,6 +4,7 @@ import type { Metadata } from 'next';
import '@workspace/ui/globals.css';
import { Providers } from './providers';
import { ParallaxLayers } from '@workspace/ui/components/animate-ui/parallax-layers';
// import { jsonLd } from '@/lib/json-ld'; // Commented out as it's Animate UI specific
// --- Update metadata for Fortura ---
@ -46,6 +47,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{/* Assuming RootProvider handles dark mode, we can nest our providers inside */}
<RootProvider theme={{ defaultTheme: 'dark' }}>
<Providers>
<ParallaxLayers />
{children}
</Providers>
</RootProvider>

View File

@ -6,25 +6,112 @@ import * as React from 'react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section } from '@workspace/ui/components/layout';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { CostCalculator } from '@workspace/ui/components/interactive';
import { MetricsKPI } from '@workspace/ui/components/content';
import { CostReductionChart } from '@workspace/ui/components/content/cost-reduction-chart';
import { Badge } from '@workspace/ui/components/content';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@workspace/ui/components/ui';
import Image from 'next/image';
export default function PricingPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Pricing</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
Calculate your Total Cost of Ownership (TCO) and see how much you can save by moving from renting to owning your infrastructure.
{/* Hero Section soft gradient band distinct from homepage/solutions */}
<AnimatedSection className="bg-gradient-to-b from-muted/30 to-transparent border-b border-muted">
<Container maxWidth="10xl">
<div className="relative py-20 md:py-28 text-center">
{/* Optional decorative pricing art (drop /hero/pricing-hero-art.webp in public) */}
<Image
src="/hero/pricing-hero-art.webp"
alt=""
aria-hidden
fill
sizes="100vw"
className="object-cover opacity-20 -z-10 rounded-sm"
priority={false}
/>
<div className="flex items-center justify-center gap-2">
<Badge variant="outline">TCO Focused</Badge>
<Badge variant="outline">CapEx + OpEx</Badge>
<Badge variant="outline">Transparent Assumptions</Badge>
</div>
<h1 className="mt-6 text-4xl md:text-5xl lg:text-6xl font-bold">
Pricing & ROI
</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl mx-auto">
Model your total cost of ownership and break-even timeline when
you own the hardware and colocate itno more cloud rent.
</p>
{/* KPI strip */}
<div className="mt-12 grid grid-cols-3 gap-6 max-w-xl mx-auto">
<MetricsKPI value="12mo" label="typical break-even" />
<MetricsKPI value="65%" label="avg. monthly savings" />
<MetricsKPI value="$350/mo" label="after year one" />
</div>
</div>
</Container>
</AnimatedSection>
{/* Cost Calculator bordered panel with subtle header */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<Card className="border-muted">
<CardHeader className="border-b border-muted bg-card/50">
<CardTitle className="text-2xl">Your Estimate</CardTitle>
</CardHeader>
<CardContent className="pt-8">
<CostCalculator />
</CardContent>
</Card>
</Container>
</AnimatedSection>
{/* Cost Curve visual summary to complement calculator */}
<AnimatedSection className="py-20 bg-muted/30 border-y border-muted">
<Container maxWidth="10xl">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl font-bold">Cost curve over time</h2>
<p className="mt-4 text-muted-foreground max-w-prose">
Upfront spend pays for itself quickly. After break-even, ongoing
costs are mostly power, space, and amortized hardwareall
predictable and under your control.
</p>
</div>
<div>
<CostReductionChart />
</div>
</div>
</Container>
</AnimatedSection>
{/* Notes & Assumptions concise context for numbers */}
<AnimatedSection className="py-16 border-b border-muted">
<Container maxWidth="10xl">
<div className="max-w-3xl mx-auto text-sm text-muted-foreground space-y-2">
<p>
Assumptions: commodity servers, 3648 month amortization, market
colo rates, and conservative utilization.
</p>
<p>
Exact results depend on workload profile, redundancy requirements,
and growth assumptions. Well tailor the model during your
architecture call.
</p>
</div>
<CostCalculator />
</Container>
</Section>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -3,99 +3,239 @@
'use client';
import * as React from 'react';
import { MoveUpRightIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { Steps } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { FinalCTASection } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@workspace/ui/components/ui';
import Image from 'next/image';
// Mock data for Process Steps (full 6 steps)
const PROCESS_STEPS = [
{
title: "Discovery",
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
weDo: "Analyze costs, identify waste, and map technical dependencies."
title: 'Discovery',
description:
'We deep-dive into your current infrastructure, workloads, and pain points.',
youDo:
'Share access to cloud billing, architecture docs, and team interviews.',
weDo: 'Analyze costs, identify waste, and map technical dependencies.',
},
{
title: "Shadow & Map",
description: "We observe how your teams actually work and map desire paths.",
youDo: "Participate in workflow observations and workshops.",
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
title: 'Shadow & Map',
description:
'We observe how your teams actually work and map desire paths.',
youDo: 'Participate in workflow observations and workshops.',
weDo: 'Document real workflows, identify automation opportunities, and draft the future state.',
},
{
title: "Architect",
description: "We design a tailored, owned infrastructure solution.",
youDo: "Review and approve the proposed architecture and migration plan.",
weDo: "Specify hardware, select software stack, and design for performance and cost."
title: 'Architect',
description: 'We design a tailored, owned infrastructure solution.',
youDo: 'Review and approve the proposed architecture and migration plan.',
weDo: 'Specify hardware, select software stack, and design for performance and cost.',
},
{
title: "Pilot",
description: "We build and test a critical component in the new environment.",
youDo: "Provide feedback on performance, usability, and integration.",
weDo: "Deploy, test, and iterate on the pilot component with your team."
title: 'Pilot',
description:
'We build and test a critical component in the new environment.',
youDo: 'Provide feedback on performance, usability, and integration.',
weDo: 'Deploy, test, and iterate on the pilot component with your team.',
},
{
title: "Migrate",
description: "We execute the planned migration with zero downtime.",
youDo: "Validate functionality and performance post-migration.",
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
title: 'Migrate',
description: 'We execute the planned migration with zero downtime.',
youDo: 'Validate functionality and performance post-migration.',
weDo: 'Cutover workloads, decommission old systems, and optimize the new stack.',
},
{
title: "Operate",
description: "We manage the infrastructure, you focus on your core business.",
youDo: "Use the system and provide ongoing feedback for improvements.",
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
}
title: 'Operate',
description:
'We manage the infrastructure, you focus on your core business.',
youDo: 'Use the system and provide ongoing feedback for improvements.',
weDo: 'Monitor, maintain, upgrade, and support the infrastructure 24/7.',
},
];
export default function ProcessPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Our Process</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
A proven 6-step approach to migrate from cloud rent to owned infrastructure with minimal retraining and maximum savings.
{/* Hero Section distinct gradient band to differentiate from Solutions */}
<AnimatedSection className="bg-gradient-to-b from-muted/30 to-transparent border-b border-muted">
<Container maxWidth="10xl">
<div className="py-20 md:py-28">
<div className="flex flex-wrap gap-2">
<Badge variant="outline">Desire Paths</Badge>
<Badge variant="outline">Zero-Downtime Migration</Badge>
<Badge variant="outline">SRE-Managed</Badge>
</div>
<h1 className="mt-6 text-4xl md:text-5xl lg:text-6xl font-bold">
Our Process
</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl">
A six-step path from audit to steady-state operationsminimizing
retraining, maximizing savings.
</p>
</div>
<Steps steps={PROCESS_STEPS} />
{/* Embedding Card */}
<Section background="muted" className="mt-16">
<Container>
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<span>Embedding</span>
<MoveUpRightIcon className="ml-2 size-5 text-primary" />
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
We sit with your teams, map real workflows, then automate the boring.
</CardDescription>
<p className="mt-2 text-sm">
Our "Desire Paths" methodology ensures the system we build fits how your team actually works, not how a generic cloud platform prescribes. This minimizes retraining and maximizes adoption.
</p>
<div className="mt-4">
<Button variant="outline" asChild>
<a href="/contact">Book Discovery Call</a>
</Button>
</div>
</CardContent>
</Card>
</Container>
</Section>
</Container>
</Section>
</AnimatedSection>
{/* Process Diagram (optional image placeholder) */}
<AnimatedSection className="py-16 border-b border-muted">
<Container maxWidth="10xl">
<Card className="border-muted">
<CardHeader>
<CardTitle>Process diagram</CardTitle>
</CardHeader>
<CardContent>
<div className="relative w-full h-64 md:h-80 lg:h-96">
<Image
src="/process/process-diagram.webp"
alt="Six-step migration process diagram"
fill
sizes="100vw"
className="object-cover rounded-sm"
/>
</div>
</CardContent>
</Card>
</Container>
</AnimatedSection>
{/* Process Steps with phase legend */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
{/* Phase legend */}
<div className="mb-10 grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="rounded-sm border border-muted p-4 bg-background/50">
<div className="text-xs tracking-wide text-muted-foreground">
Phase 1
</div>
<div className="mt-1 font-semibold">Discover & Map</div>
<div className="mt-2 text-sm text-muted-foreground">
Steps 12
</div>
</div>
<div className="rounded-sm border border-muted p-4 bg-background/50">
<div className="text-xs tracking-wide text-muted-foreground">
Phase 2
</div>
<div className="mt-1 font-semibold">Design & Pilot</div>
<div className="mt-2 text-sm text-muted-foreground">
Steps 34
</div>
</div>
<div className="rounded-sm border border-muted p-4 bg-background/50">
<div className="text-xs tracking-wide text-muted-foreground">
Phase 3
</div>
<div className="mt-1 font-semibold">Migrate & Operate</div>
<div className="mt-2 text-sm text-muted-foreground">
Steps 56
</div>
</div>
</div>
<Steps steps={PROCESS_STEPS} />
</Container>
</AnimatedSection>
{/* Deliverables */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<h2 className="text-3xl font-bold mb-8">Key Deliverables</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle>Architecture Blueprint</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground">
Hardware spec, network plan, security model, and service layout
mapped to your workloads.
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Migration Plan</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground">
Sequenced cutover with rollback points, data move playbooks, and
testing gates.
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Runbook & SLOs</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground">
Operability docs, monitoring dashboards, on-call rotations, and
measurable service objectives.
</CardContent>
</Card>
</div>
</Container>
</AnimatedSection>
{/* Engagement Model */}
<AnimatedSection className="py-20 bg-muted/30 border-y border-muted">
<Container maxWidth="10xl">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="border-muted">
<CardHeader>
<CardTitle>Embedded With You</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground space-y-2">
<p>
Weekly working sessions, async updates, and direct
engineer-to-engineer channels.
</p>
<p>
We map desire paths, automate toil, and keep your stack
familiar.
</p>
</CardContent>
</Card>
<Card className="border-muted">
<CardHeader>
<CardTitle>Support & Ops</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground space-y-2">
<p>
24/7 monitoring, upgrades, and incident response handled by
our SREs.
</p>
<p>
You own the hardware and data; we keep it healthy and fast.
</p>
</CardContent>
</Card>
</div>
</Container>
</AnimatedSection>
{/* Final CTA */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<FinalCTASection
title="Ready to kick off discovery?"
description="Well audit your spend, map your workflows, and design the fastest path to break-even."
primaryCtaText="Book Discovery Call"
primaryCtaHref="/contact"
secondaryCtaText="Download Blueprint"
secondaryCtaHref="/download-blueprint"
/>
</Container>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -1,143 +1,170 @@
// app/solutions/[industry]/page.tsx
'use client';
import * as React from 'react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { MoveUpRightIcon, DownloadIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { FeatureGrid } from '@workspace/ui/components/content/feature-grid';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
import Image from 'next/image';
// Types and mock data for industries and their specific details
type ProposedItem = { icon: React.ReactNode; title: string; description: string };
type Outcome = { metric: string; description: string };
type Industry = {
name: string;
title: string;
description: string;
painPoints: string[];
proposedStack: ProposedItem[];
migrationPlan: string[];
opsModel: string;
outcomes: Outcome[];
};
// Mock data for industries and their specific details
// In a real app, this would likely come from a CMS or database
const INDUSTRY_DATA: Record<string, any> = {
const INDUSTRY_DATA: Record<string, Industry> = {
technology: {
name: "Technology",
title: "Cloud Alternatives for Technology Companies",
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax.",
name: 'Technology',
title: 'Cloud Alternatives for Technology Companies',
description:
'From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax.',
painPoints: [
"High and unpredictable cloud bills that scale with user growth.",
"Vendor lock-in making it hard to switch providers or adopt hybrid models.",
"Complexity and overhead of managing cloud-native architectures.",
"Need for high performance and low latency for user-facing applications."
'High and unpredictable cloud bills that scale with user growth.',
'Vendor lock-in making it hard to switch providers or adopt hybrid models.',
'Complexity and overhead of managing cloud-native architectures.',
'Need for high performance and low latency for user-facing applications.',
],
proposedStack: [
{
icon: <PlaceholderIcon className="size-5" />,
title: "Kubernetes (K3s/RKE2)",
description: "Lightweight, certified Kubernetes for efficient container orchestration."
title: 'Kubernetes (K3s/RKE2)',
description:
'Lightweight, certified Kubernetes for efficient container orchestration.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "PostgreSQL/MySQL",
description: "Self-hosted, high-availability databases with point-in-time recovery."
title: 'PostgreSQL/MySQL',
description:
'Self-hosted, high-availability databases with point-in-time recovery.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "MinIO",
description: "S3-compatible object storage for unstructured data."
title: 'MinIO',
description: 'S3-compatible object storage for unstructured data.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "Nextcloud",
description: "Private file sync, sharing, and collaboration platform."
}
title: 'Nextcloud',
description: 'Private file sync, sharing, and collaboration platform.',
},
],
migrationPlan: [
"Audit current cloud spend and architecture.",
"Design a colocated Kubernetes cluster tailored to your workloads.",
"Migrate stateful services with zero downtime using blue-green deployments.",
"Implement CI/CD pipelines for automated testing and deployment.",
"Optimize for cost and performance post-migration."
'Audit current cloud spend and architecture.',
'Design a colocated Kubernetes cluster tailored to your workloads.',
'Migrate stateful services with zero downtime using blue-green deployments.',
'Implement CI/CD pipelines for automated testing and deployment.',
'Optimize for cost and performance post-migration.',
],
opsModel: "We provide 24/7 monitoring, managed upgrades, and direct access to our SREs. You retain control over application configuration and scaling decisions.",
opsModel:
'We provide 24/7 monitoring, managed upgrades, and direct access to our SREs. You retain control over application configuration and scaling decisions.',
outcomes: [
{
metric: "60%",
description: "Reduction in monthly infrastructure costs."
metric: '60%',
description: 'Reduction in monthly infrastructure costs.',
},
{
metric: "Sub-10ms",
description: "Improved database query latency."
metric: 'Sub-10ms',
description: 'Improved database query latency.',
},
{
metric: "Zero",
description: "Vendor lock-in with portable, open-source tools."
}
]
metric: 'Zero',
description: 'Vendor lock-in with portable, open-source tools.',
},
],
},
legal: {
name: "Legal",
title: "Secure Infrastructure for Legal Firms",
description: "Secure, private document management and collaboration environments with strict access controls.",
name: 'Legal',
title: 'Secure Infrastructure for Legal Firms',
description:
'Secure, private document management and collaboration environments with strict access controls.',
painPoints: [
"Stringent data privacy regulations (GDPR, CCPA) requiring data residency and control.",
"High cost of secure cloud storage and collaboration tools.",
"Risk of data breaches from third-party SaaS providers.",
"Need for comprehensive audit trails and access logging."
'Stringent data privacy regulations (GDPR, CCPA) requiring data residency and control.',
'High cost of secure cloud storage and collaboration tools.',
'Risk of data breaches from third-party SaaS providers.',
'Need for comprehensive audit trails and access logging.',
],
proposedStack: [
{
icon: <PlaceholderIcon className="size-5" />,
title: "Nextcloud",
description: "Enterprise-grade file sync, sharing, and collaboration with full audit capabilities."
title: 'Nextcloud',
description:
'Enterprise-grade file sync, sharing, and collaboration with full audit capabilities.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "OnlyOffice",
description: "Private, self-hosted office suite for document editing and collaboration."
title: 'OnlyOffice',
description:
'Private, self-hosted office suite for document editing and collaboration.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "Keycloak",
description: "Centralized identity management with SAML/OIDC integration for existing directories."
title: 'Keycloak',
description:
'Centralized identity management with SAML/OIDC integration for existing directories.',
},
{
icon: <PlaceholderIcon className="size-5" />,
title: "OpenSearch",
description: "Self-hosted search and analytics engine for document discovery."
}
title: 'OpenSearch',
description:
'Self-hosted search and analytics engine for document discovery.',
},
],
migrationPlan: [
"Inventory and classify all documents and data flows.",
"Implement a secure, private Nextcloud instance with custom compliance policies.",
"Migrate existing document repositories with full version history.",
"Integrate with existing case management and time-tracking systems.",
"Train staff on new tools and security protocols."
'Inventory and classify all documents and data flows.',
'Implement a secure, private Nextcloud instance with custom compliance policies.',
'Migrate existing document repositories with full version history.',
'Integrate with existing case management and time-tracking systems.',
'Train staff on new tools and security protocols.',
],
opsModel: "We manage the underlying infrastructure, security patches, and backups. Your team manages user accounts, permissions, and document workflows.",
opsModel:
'We manage the underlying infrastructure, security patches, and backups. Your team manages user accounts, permissions, and document workflows.',
outcomes: [
{
metric: "100%",
description: "Data sovereignty and compliance with legal regulations."
metric: '100%',
description: 'Data sovereignty and compliance with legal regulations.',
},
{
metric: "$50k/year",
description: "Savings vs. proprietary legal tech SaaS bundles."
metric: '$50k/year',
description: 'Savings vs. proprietary legal tech SaaS bundles.',
},
{
metric: "Zero",
description: "Third-party access to sensitive client data."
}
]
metric: 'Zero',
description: 'Third-party access to sensitive client data.',
},
],
},
// Add more industries here...
};
interface SolutionPageProps {
params: { industry: string };
}
export default function SolutionPage({ params }: SolutionPageProps) {
const { industry } = params;
export default async function SolutionPage({
params,
}: {
params: Promise<{ industry: string }>;
}) {
const { industry } = await params;
const solution = INDUSTRY_DATA[industry];
if (!solution) {
@ -149,19 +176,33 @@ export default function SolutionPage({ params }: SolutionPageProps) {
<Navbar />
<Section className="flex-grow">
<Container>
{/* Optional industry banner (drop /solutions/[industry]-hero.webp in public) */}
<div className="mb-8 rounded-sm border overflow-hidden bg-muted">
<div className="relative h-48 md:h-64 lg:h-72">
<Image
src={`/solutions/${industry}-hero.webp`}
alt=""
aria-hidden
fill
sizes="100vw"
className="object-cover"
priority={false}
/>
</div>
</div>
<div className="mb-8">
<Button variant="link" asChild className="px-0">
<a href="/solutions">&larr; All Solutions</a>
<Link href="/solutions">&larr; All Solutions</Link>
</Button>
</div>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">{solution.title}</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
{solution.description}
</p>
</div>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Key Pain Points</h2>
@ -175,14 +216,17 @@ export default function SolutionPage({ params }: SolutionPageProps) {
</ul>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6">Proposed Stack</h2>
<FeatureGrid features={solution.proposedStack} columns={{ initial: 1, sm: 2 }} />
<FeatureGrid
features={solution.proposedStack}
columns={{ initial: 1, sm: 2 }}
/>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Migration Plan</h2>
@ -198,39 +242,47 @@ export default function SolutionPage({ params }: SolutionPageProps) {
</ol>
</Container>
</Section>
<Section background="muted">
<Container>
<h2 className="text-2xl font-bold mb-6">Operations Model</h2>
<p className="text-muted-foreground">
{solution.opsModel}
</p>
<p className="text-muted-foreground">{solution.opsModel}</p>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Expected Outcomes</h2>
<Grid columns={{ initial: 1, sm: 3 }} gap="6">
{solution.outcomes.map((outcome: { metric: string; description: string }, index: number) => (
<Card key={index}>
<CardHeader>
<CardTitle className="text-3xl font-bold">{outcome.metric}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{outcome.description}</CardDescription>
</CardContent>
</Card>
))}
{solution.outcomes.map(
(
outcome: { metric: string; description: string },
index: number,
) => (
<Card key={index}>
<CardHeader>
<CardTitle className="text-3xl font-bold">
{outcome.metric}
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>{outcome.description}</CardDescription>
</CardContent>
</Card>
),
)}
</Grid>
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Ready to transform your infrastructure?</h2>
<h2 className="text-2xl font-bold mb-4">
Ready to transform your infrastructure?
</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Download our industry-specific blueprint or book a free architecture call.
Download our industry-specific blueprint or book a free
architecture call.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button asChild>
@ -253,4 +305,4 @@ export default function SolutionPage({ params }: SolutionPageProps) {
<Footer />
</div>
);
}
}

View File

@ -3,77 +3,145 @@
'use client';
import * as React from 'react';
import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { Button } from '@workspace/ui/components/ui';
import { FinalCTASection } from '@workspace/ui/components/content';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
import Image from 'next/image';
// Mock data for Industries
const INDUSTRIES = [
{
icon: <PlaceholderIcon className="size-6" />,
title: "Technology",
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax."
title: 'Technology',
description:
'From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax.',
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Legal",
description: "Secure, private document management and collaboration environments with strict access controls."
title: 'Legal',
description:
'Secure, private document management and collaboration environments with strict access controls.',
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Medical",
description: "HIPAA-compliant infrastructure for storing, processing, and analyzing sensitive patient data."
title: 'Medical',
description:
'HIPAA-compliant infrastructure for storing, processing, and analyzing sensitive patient data.',
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Media",
description: "High-bandwidth, low-latency solutions for content creation, storage, and distribution."
title: 'Media',
description:
'High-bandwidth, low-latency solutions for content creation, storage, and distribution.',
},
{
icon: <PlaceholderIcon className="size-6" />,
title: "Research",
description: "Powerful compute and storage for data-intensive scientific research and simulations."
}
title: 'Research',
description:
'Powerful compute and storage for data-intensive scientific research and simulations.',
},
];
export default function SolutionsPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Solutions by Industry</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We tailor our approach to the unique challenges and requirements of your vertical.
</p>
</div>
<FeatureGrid features={INDUSTRIES} columns={{ initial: 1, sm: 2, md: 3 }} />
<Section background="muted" className="text-center mt-16">
<Container>
<h2 className="text-2xl font-bold mb-4">Don't see your industry?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Our core principles apply broadly. Contact us to discuss how we can adapt our approach for your specific use case.
{/* Hero Section: banded with grid background to mirror homepage */}
<AnimatedSection className="bg-grid-dark border-b border-muted">
<Container maxWidth="10xl">
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center py-20 md:py-28">
<div className="text-left">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Solutions by Industry
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-xl">
Practical, high-impact stacks mapped to the realities of your
sector.
</p>
<Button asChild>
<a href="/contact">
Contact Us
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
<div className="mt-10 flex flex-col sm:flex-row gap-4">
<Button size="lg" variant="default" asChild>
<a href="#industries">Explore Industries</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="/contact">Talk To Us</a>
</Button>
</div>
</div>
{/* Right visual panel for rhythm with homepage hero */}
<div className="relative h-[220px] md:h-[300px] lg:h-[360px]">
<div className="absolute inset-0 rounded-sm border bg-card/60 backdrop-blur-sm" />
{/* Optional decorative solutions art (drop /hero/solutions-hero-art.webp in public) */}
<Image
src="/hero/solutions-hero-art.webp"
alt=""
aria-hidden
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover rounded-sm opacity-35"
priority={false}
/>
<div className="absolute inset-0 bg-stripes-dark rounded-sm opacity-50" />
</div>
</div>
</Container>
</Section>
</AnimatedSection>
{/* Industries */}
<AnimatedSection id="industries" className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="space-y-10 max-w-5xl">
{INDUSTRIES.map((item, index) => {
const href = `/solutions/${item.title.toLowerCase()}`;
return (
<a
key={item.title}
href={href}
className="group flex items-start gap-8 border-b border-muted pb-10 transition-all duration-300 hover:border-foreground/70 last:border-0"
>
<div className="text-4xl font-light text-muted-foreground mt-1">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<div className="flex-1">
<div className="flex items-baseline gap-4">
<h2 className="text-4xl md:text-5xl font-bold transition-colors duration-300 group-hover:text-foreground/90">
{item.title}
</h2>
<span className="text-primary text-sm tracking-wide opacity-80 group-hover:opacity-100">
Explore
</span>
</div>
<p className="text-lg md:text-xl text-muted-foreground mt-4 max-w-3xl transition-colors duration-300 group-hover:text-foreground/80">
{item.description}
</p>
</div>
</a>
);
})}
</div>
</Container>
</AnimatedSection>
{/* CTA Section: reuse shared FinalCTASection for consistency */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<FinalCTASection
title="Don't see your industry?"
description="Same principles apply broadly. Well map your stack to your operations and constraints."
primaryCtaText="Talk To Us"
primaryCtaHref="/contact"
secondaryCtaText="Download Blueprint"
secondaryCtaHref="/download-blueprint"
/>
</Container>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -7,11 +7,10 @@ import { ExternalLinkIcon } from 'lucide-react';
import { Navbar } from '@workspace/ui/components/layout';
import { Footer } from '@workspace/ui/components/layout';
import { Container, Section, Grid } from '@workspace/ui/components/layout';
import { FeatureGrid } from '@workspace/ui/components/content';
import { Container } from '@workspace/ui/components/layout';
import { AnimatedSection } from '@workspace/ui/components/animate-ui/section';
import { Callout } from '@workspace/ui/components/content';
import { Badge } from '@workspace/ui/components/content';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
import { Button } from '@workspace/ui/components/ui';
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
@ -106,66 +105,120 @@ export default function StackPage() {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
<Section className="flex-grow">
<Container>
<div className="text-center mb-12">
<h1 className="text-3xl md:text-4xl font-bold">Our Stack</h1>
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
We build on proven, open-source foundations. No vendor lock-in, no proprietary black boxes.
{/* Hero Section */}
<AnimatedSection className="border-b border-muted">
<Container maxWidth="10xl">
<div className="py-20 md:py-28">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold">Our Stack</h1>
<p className="mt-6 text-xl text-muted-foreground max-w-3xl">
Proven, open-source foundations. No lock-in. No black boxes.
</p>
</div>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Core Frameworks</h2>
<FeatureGrid features={CORE_FRAMEWORKS} columns={{ initial: 1, sm: 2, md: 3 }} />
</Container>
</Section>
<Section background="muted">
<Container>
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
<h2 className="text-2xl font-bold">AI-Native</h2>
<Badge variant="info">Private & Secure</Badge>
</div>
<p className="mb-6 text-muted-foreground max-w-3xl">
Leverage AI on your terms. All inference happens on hardware you control, over data you own. Zero leakage to third parties.
</p>
<FeatureGrid features={AI_NATIVE_STACK} columns={{ initial: 1, md: 3 }} />
<Callout variant="info" className="mt-8">
<span className="font-semibold">AI Compliance:</span> Our local AI stack ensures strict adherence to data privacy regulations (GDPR, HIPAA) and eliminates risks associated with sending sensitive data to external LLM providers.
</Callout>
</Container>
</Section>
<Section>
<Container>
<h2 className="text-2xl font-bold mb-6">Integrations</h2>
<p className="mb-6 text-muted-foreground max-w-3xl">
We wire your new stack into your existing ecosystem. Connect to SSO, directories, project management, communication tools, and more.
</p>
<FeatureGrid features={INTEGRATIONS} columns={{ initial: 1, sm: 2, md: 3 }} />
</Container>
</Section>
<Section background="muted" className="text-center">
<Container>
<h2 className="text-2xl font-bold mb-4">Want to see a specific tool?</h2>
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
Our stack is flexible. If you have a specific open-source tool in mind, we can likely integrate it.
</p>
<Button asChild>
<a href="/contact">
Discuss Your Stack
<ExternalLinkIcon className="ml-2 size-4" />
</a>
</Button>
</Container>
</Section>
</Container>
</Section>
</AnimatedSection>
{/* Core Frameworks */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<h2 className="text-4xl font-bold mb-16">Core Frameworks</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{CORE_FRAMEWORKS.map((f, index) => (
<div key={f.title} className="flex items-start gap-6 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary">
{f.icon}
</div>
<div>
<div className="text-4xl font-light text-muted-foreground mb-2">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h3 className="text-2xl font-bold">{f.title}</h3>
<p className="text-muted-foreground mt-2">{f.description}</p>
</div>
</div>
))}
</div>
</Container>
</AnimatedSection>
{/* AI-Native Stack */}
<AnimatedSection className="py-20 border-b border-muted bg-muted/30">
<Container maxWidth="10xl">
<div className="flex flex-wrap items-center justify-between gap-6 mb-16">
<h2 className="text-4xl font-bold">AI-Native</h2>
<Badge variant="info" className="text-lg py-2 px-4">Private & Secure</Badge>
</div>
<p className="text-xl text-muted-foreground max-w-4xl mb-16">
Leverage AI on your terms. All inference happens on hardware you control, over data you own. Zero leakage to third parties.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{AI_NATIVE_STACK.map((f, index) => (
<div key={f.title} className="flex items-start gap-6 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary">
{f.icon}
</div>
<div>
<div className="text-4xl font-light text-muted-foreground mb-2">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h3 className="text-2xl font-bold">{f.title}</h3>
<p className="text-muted-foreground mt-2">{f.description}</p>
</div>
</div>
))}
</div>
<Callout variant="info" className="mt-16 p-8 text-lg">
<span className="font-semibold">AI Compliance:</span> Our local AI stack ensures strict adherence to data privacy regulations (GDPR, HIPAA) and eliminates risks associated with sending sensitive data to external LLM providers.
</Callout>
</Container>
</AnimatedSection>
{/* Integrations */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<h2 className="text-4xl font-bold mb-16">Integrations</h2>
<p className="text-xl text-muted-foreground max-w-4xl mb-16">
We wire your new stack into your existing ecosystem. Connect to SSO, directories, project management, communication tools, and more.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{INTEGRATIONS.map((f, index) => (
<div key={f.title} className="flex items-start gap-6 p-8 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary">
{f.icon}
</div>
<div>
<div className="text-4xl font-light text-muted-foreground mb-2">
{index < 9 ? `0${index + 1}` : index + 1}
</div>
<h3 className="text-2xl font-bold">{f.title}</h3>
<p className="text-muted-foreground mt-2">{f.description}</p>
</div>
</div>
))}
</div>
</Container>
</AnimatedSection>
{/* CTA Section */}
<AnimatedSection className="py-20 border-b border-muted">
<Container maxWidth="10xl">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-4xl font-bold mb-6">Want to see a specific tool?</h2>
<p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
Our stack is flexible. If you have a specific open-source tool in mind, we can likely integrate it.
</p>
<Button asChild size="lg">
<a href="/contact">
Discuss Your Stack
<ExternalLinkIcon className="ml-2 size-5" />
</a>
</Button>
</div>
</Container>
</AnimatedSection>
<Footer />
</div>
);
}
}

View File

@ -1,10 +1,22 @@
import { docs } from '@/.source';
import { loader } from 'fumadocs-core/source';
import { createElement } from 'react';
import { createElement, type ComponentType } from 'react';
import { icons } from 'lucide-react';
import { attachFile } from './attach-file';
import { attachSeparator } from './attach-separator';
// Loosen types to avoid strict coupling to fumadocs internal types at build time
type PageShape = {
data: {
body: ComponentType<any>;
toc?: unknown;
full?: boolean;
title: string;
description?: string;
author?: { name: string; url?: string } | undefined;
};
};
export const source = loader({
baseUrl: '/docs',
source: docs.toFumadocsSource(),
@ -19,4 +31,10 @@ export const source = loader({
if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
},
});
}) as unknown as {
getPage: (slug?: string[]) => PageShape | undefined;
// Minimal shape used by sitemap: url + file.path
getPages: () => Array<{ url: string; file: { path: string } }>;
generateParams: () => Promise<any> | any;
pageTree: unknown;
};

View File

@ -12,6 +12,7 @@
"homepage": "https://animate-ui.com",
"author": "Skyleen",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev",
@ -49,7 +50,7 @@
"rimraf": "^6.0.1",
"shadcn": "2.4.0-canary.13",
"shiki": "^3.2.1",
"zod": "^3.24.2"
"zod": "^4.0.0"
},
"devDependencies": {
"@types/mdx": "^2.0.13",

View File

@ -18,7 +18,7 @@
"jsx": "preserve",
"incremental": true,
"paths": {
"@/.source": ["./.source/index.ts"],
"@/.source": ["./.source-typed.ts"],
"@/*": ["./*"],
"@workspace/ui/*": ["../../packages/ui/src/*"]
},
@ -29,5 +29,9 @@
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": [
"node_modules",
".source",
".source/**/*"
]
}

0
b4b7bd5fcfe1 Normal file
View File

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: apps/www/Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
# Add other environment variables as needed
# - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
# - DATABASE_URL=${DATABASE_URL}
env_file:
- .env.local
volumes:
- ./.next:/app/.next
restart: unless-stopped

View File

@ -17,18 +17,38 @@
"@commitlint/config-conventional": "^19.8.0",
"@eslint/js": "^9.28.0",
"@tailwindcss/postcss": "^4.1.4",
"@types/node": "22.13.8",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/node": "24.3.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"eslint": "^9.20.1",
"fumadocs-core": "^15.6.12",
"fumadocs-mdx": "^11.7.5",
"fumadocs-ui": "^15.6.12",
"husky": "^9.1.7",
"motion": "^12.23.12",
"next": "^15.5.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"radix-ui": "^1.4.3",
"react": "^19.1.1",
"react-day-picker": "^9.9.0",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.4",
"turbo": "^2.4.2",
"typescript": "5.7.3"
"tw-animate-css": "^1.3.7",
"typescript": "5.9.2",
"zod": "^4.0.17"
},
"pnpm": {
"overrides": {
"zod": "^4.0.0"
}
},
"packageManager": "pnpm@10.4.1",
"engines": {

View File

@ -23,7 +23,7 @@
"react-dom": "^19.1.0",
"tailwind-merge": "^3.0.1",
"tw-animate-css": "^1.2.4",
"zod": "^3.24.2"
"zod": "^4.0.0"
},
"devDependencies": {
"@turbo/gen": "^2.4.2"

View File

@ -0,0 +1,41 @@
"use client";
import * as React from "react";
import { motion, useScroll, useTransform } from "motion/react";
export function ParallaxLayers() {
const { scrollY } = useScroll();
const ySlow = useTransform(scrollY, [0, 800], [0, 20]);
const yMed = useTransform(scrollY, [0, 800], [0, 40]);
const yFast = useTransform(scrollY, [0, 800], [0, 60]);
return (
<div aria-hidden className="pointer-events-none fixed inset-0 -z-10">
{/* Layer 1: subtle grid */}
<motion.div
className="absolute inset-0 opacity-[0.06] bg-grid-dark"
style={{ y: ySlow }}
/>
{/* Layer 2: diagonal stripes */}
<motion.div
className="absolute inset-0 opacity-[0.05] bg-stripes-dark"
style={{ y: yMed }}
/>
{/* Layer 3: soft radial vignette */}
<motion.div
className="absolute inset-0"
style={{ y: yFast }}
>
<div
className="absolute inset-0"
style={{
background:
"radial-gradient(60% 50% at 50% 10%, color-mix(in oklch, var(--color-foreground), transparent 96%) 0%, transparent 100%)",
opacity: 0.08,
}}
/>
</motion.div>
</div>
);
}

View File

@ -0,0 +1,19 @@
"use client";
import * as React from "react";
import { motion, useScroll } from "motion/react";
export function ScrollProgressBar({ className = "" }: { className?: string }) {
const { scrollYProgress } = useScroll();
return (
<motion.div
aria-hidden
className={`fixed left-0 right-0 top-20 h-px z-[60] ${className}`}
>
<motion.div
className="h-full origin-left bg-primary/30"
style={{ scaleX: scrollYProgress }}
/>
</motion.div>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import { motion } from "motion/react";
import { Section } from "@workspace/ui/components/layout/layout-utils";
type AnimatedSectionProps = React.ComponentProps<typeof Section> & {
/** Delay in seconds for staggered entrance */
delay?: number;
};
export function AnimatedSection({ children, className, delay = 0, ...props }: AnimatedSectionProps) {
return (
<Section className={className} {...props}>
<motion.div
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.22, ease: [0.2, 0.8, 0.2, 1], delay }}
>
{children}
</motion.div>
</Section>
);
}

View File

@ -0,0 +1,86 @@
"use client";
import * as React from "react";
// Import recharts components
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
ResponsiveContainer,
Cell
} from 'recharts';
export function CostReductionChart() {
// Data for the chart
const data = [
{ name: 'Traditional Cloud', cost: 100000 },
{ name: 'Our Solution', cost: 10000 },
];
// Colors that align with the site's aesthetic
// Using muted foreground and primary colors
const COLORS = ['oklch(0.556 0 0)', 'oklch(0.340 0 0)']; // muted-foreground and primary
return (
<div className="w-full py-8">
<h2 className="text-4xl font-bold mb-16">Cost Reduction</h2>
<div className="h-64 md:h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
layout="vertical"
>
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} strokeOpacity={0.3} />
<XAxis
type="number"
tickFormatter={(value) => `${value.toLocaleString()}`}
domain={[0, 120000]}
tick={{ fontSize: 12 }}
/>
<YAxis
type="category"
dataKey="name"
tickLine={false}
axisLine={false}
width={120}
tick={{ fontSize: 12 }}
/>
<Bar
dataKey="cost"
name="Monthly Cost"
radius={[0, 4, 4, 0]}
barSize={40}
>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index]}
fillOpacity={0.8}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-8 text-center">
<div className="text-2xl font-bold" style={{ color: 'oklch(0.205 0 0)' }}>
99.65%
</div>
<div className="text-sm text-muted-foreground mt-2">
*Based on typical workloads. Your savings may vary.
</div>
</div>
</div>
);
}

View File

@ -25,24 +25,26 @@ export function FinalCTASection({
return (
<div
className={cn(
"flex flex-col items-center justify-center text-center p-8 md:p-12 rounded-lg border bg-card",
"flex flex-col items-center justify-center text-center py-20 px-6 rounded-lg w-full",
className
)}
{...props}
>
<h2 className="text-2xl font-bold">{title}</h2>
<p className="mt-2 text-muted-foreground max-w-xl">
{description}
</p>
<div className="mt-6 flex flex-col sm:flex-row gap-3">
<Button asChild size="lg">
<a href={primaryCtaHref}>{primaryCtaText}</a>
</Button>
{secondaryCtaText && secondaryCtaHref && (
<Button asChild variant="outline" size="lg">
<a href={secondaryCtaHref}>{secondaryCtaText}</a>
<div className="max-w-3xl">
<h2 className="text-4xl font-bold mb-16">{title}</h2>
<p className="text-xl text-muted-foreground mb-10 max-w-2xl mx-auto">
{description}
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<Button asChild size="lg" className="px-10 py-6 text-lg">
<a href={primaryCtaHref}>{primaryCtaText}</a>
</Button>
)}
{secondaryCtaText && secondaryCtaHref && (
<Button asChild variant="outline" size="lg" className="px-10 py-6 text-lg">
<a href={secondaryCtaHref}>{secondaryCtaText}</a>
</Button>
)}
</div>
</div>
</div>
);

View File

@ -7,4 +7,5 @@ export * from './callout';
export * from './badge';
export * from './trust-section';
export * from './final-cta-section';
export * from './toast'; // Add Toast export
export * from './toast'; // Add Toast export
export * from './cost-reduction-chart';

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { motion } from 'motion/react';
import { cn } from '@workspace/ui/lib/utils';
import { Button } from '@workspace/ui/components/ui/button'; // Assuming Button exists
import { Button } from '@workspace/ui/components/ui/button';
// Define types for steps
interface StepItem {
@ -22,45 +22,37 @@ export interface StepsProps extends React.HTMLAttributes<HTMLDivElement> {
export function Steps({ steps, title, ctaText, ctaHref, className, ...props }: StepsProps) {
return (
<div className={cn("w-full", className)} {...props}>
{title && <h2 className="text-2xl font-bold mb-8">{title}</h2>}
<div className="relative">
{/* Vertical line */}
<div className="absolute left-4 top-0 h-full w-0.5 bg-border transform -translate-x-1/2 md:left-8 md:w-1"></div>
<ol className="relative space-y-12 pl-10 md:pl-20">
<div className={cn('w-full', className)} {...props}>
{title && <h2 className="text-2xl font-bold mb-8 text-center">{title}</h2>}
{/* Desktop / Wide: horizontal timeline */}
<div className="relative hidden md:block">
{/* Horizontal line */}
<div className="absolute left-0 right-0 top-5 h-px bg-border" />
<ol className="relative flex items-start justify-between gap-6">
{steps.map((step, index) => (
<motion.li
key={index}
className="relative"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="relative flex-1 min-w-0"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-10%' }}
transition={{ duration: 0.22, delay: index * 0.05, ease: [0.2, 0.8, 0.2, 1] }}
>
{/* Step number circle */}
<div className="absolute left-[-34px] top-0 flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground md:left-[-52px] md:h-12 md:w-12">
<span className="text-sm font-bold md:text-base">{index + 1}</span>
<div className="relative z-10 mx-auto w-10 h-10 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">
<span className="text-sm">{index + 1}</span>
</div>
<div>
<h3 className="text-lg font-semibold">{step.title}</h3>
<p className="mt-1 text-muted-foreground">{step.description}</p>
{/* Optional bullets */}
<div className="mt-3 text-center px-2">
<h3 className="text-sm font-semibold truncate">{step.title}</h3>
<p className="mt-1 text-xs text-muted-foreground line-clamp-3">{step.description}</p>
{(step.youDo || step.weDo) && (
<ul className="mt-3 space-y-1 text-sm">
<ul className="mt-2 space-y-1 text-xs text-left mx-auto max-w-xs">
{step.youDo && (
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">You:</span> {step.youDo}</span>
</li>
<li className="flex items-start"><span className="mr-2 text-primary"></span><span><span className="font-medium">You:</span> {step.youDo}</span></li>
)}
{step.weDo && (
<li className="flex items-start">
<span className="mr-2 text-primary"></span>
<span><span className="font-medium">We:</span> {step.weDo}</span>
</li>
<li className="flex items-start"><span className="mr-2 text-primary"></span><span><span className="font-medium">We:</span> {step.weDo}</span></li>
)}
</ul>
)}
@ -69,10 +61,43 @@ export function Steps({ steps, title, ctaText, ctaHref, className, ...props }: S
))}
</ol>
</div>
{/* CTA Button */}
{/* Mobile: centered stacked items */}
<div className="md:hidden">
<ol className="space-y-8">
{steps.map((step, index) => (
<motion.li
key={index}
className="relative"
initial={{ opacity: 0, y: 8 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-10%' }}
transition={{ duration: 0.22, delay: index * 0.05, ease: [0.2, 0.8, 0.2, 1] }}
>
<div className="mx-auto w-9 h-9 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold">
<span className="text-xs">{index + 1}</span>
</div>
<div className="mt-3 text-center px-1">
<h3 className="text-base font-semibold">{step.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{step.description}</p>
{(step.youDo || step.weDo) && (
<ul className="mt-2 space-y-1 text-sm text-left mx-auto max-w-sm">
{step.youDo && (
<li className="flex items-start"><span className="mr-2 text-primary"></span><span><span className="font-medium">You:</span> {step.youDo}</span></li>
)}
{step.weDo && (
<li className="flex items-start"><span className="mr-2 text-primary"></span><span><span className="font-medium">We:</span> {step.weDo}</span></li>
)}
</ul>
)}
</div>
</motion.li>
))}
</ol>
</div>
{ctaText && ctaHref && (
<div className="mt-12 flex justify-center">
<div className="mt-10 flex justify-center">
<Button asChild>
<a href={ctaHref}>{ctaText}</a>
</Button>
@ -80,4 +105,4 @@ export function Steps({ steps, title, ctaText, ctaHref, className, ...props }: S
)}
</div>
);
}
}

View File

@ -11,22 +11,33 @@ interface TrustItem {
export interface TrustSectionProps extends React.HTMLAttributes<HTMLDivElement> {
items: TrustItem[];
title?: string; // Optional title for the section
description?: string; // Optional description for the section
}
export function TrustSection({ items, title, className, ...props }: TrustSectionProps) {
export function TrustSection({ items, title, description, className, ...props }: TrustSectionProps) {
return (
<div className={cn("w-full", className)} {...props}>
{title && <h2 className="text-2xl font-bold mb-8 text-center">{title}</h2>}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item, index) => (
<div key={index} className="flex items-start space-x-3 p-4 rounded-lg border bg-card">
<div className="mt-0.5 text-primary">
{item.icon}
</div>
<p className="text-sm text-muted-foreground">{item.text}</p>
<div className={cn('w-full', className)} {...props}>
<div className="max-w-6xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
<div className="space-y-6">
{title && <h2 className="text-4xl font-bold">{title}</h2>}
{description && <p className="text-xl text-muted-foreground">{description}</p>}
</div>
))}
<div className="grid grid-cols-2 gap-6">
{items.map((item, index) => (
<div
key={index}
className="flex flex-col items-center text-center space-y-4 p-6 rounded-lg border border-muted bg-background transition-all duration-300 hover:border-foreground/20"
>
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 text-primary">
{item.icon}
</div>
<p className="text-base text-muted-foreground">{item.text}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
}
}

View File

@ -50,7 +50,7 @@ const SOCIAL_LINKS = [
export function Footer() {
return (
<footer className="border-t bg-background/80 backdrop-blur-sm">
<div className="container py-12 md:py-16 px-4 md:px-6">
<div className="w-full py-14 md:py-20 px-4 md:px-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8">
{/* Branding and Description */}
<div className="lg:col-span-1">
@ -120,4 +120,4 @@ export function Footer() {
</div>
</footer>
);
}
}

View File

@ -3,17 +3,27 @@ import * as React from 'react';
import { cn } from '@workspace/ui/lib/utils';
// Container component for consistent horizontal padding and optional max-width centering
type MaxWidth = '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | '10xl' | 'full';
const maxWidthClassMap: Record<MaxWidth, string> = {
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'6xl': 'max-w-6xl',
'7xl': 'max-w-7xl',
'10xl': 'max-w-8xl',
'full': 'max-w-none',
};
export function Container({
children,
className,
maxWidth = '7xl', // Tailwind max-width class prefix (e.g., '7xl' for max-w-7xl)
maxWidth = 'full',
...props
}: React.HTMLAttributes<HTMLDivElement> & { maxWidth?: string }) {
}: React.HTMLAttributes<HTMLDivElement> & { maxWidth?: MaxWidth }) {
const maxW = maxWidthClassMap[maxWidth] ?? maxWidthClassMap['7xl'];
return (
<div
className={cn('w-full mx-auto px-4 md:px-6', maxWidth && `max-w-${maxWidth}`, className)}
{...props}
>
<div className={cn('w-full mx-auto px-6 md:px-8', maxW, className)} {...props}>
{children}
</div>
);
@ -36,11 +46,12 @@ export function Section({
const backgroundClass = background === 'muted' ? 'bg-muted' : background === 'card' ? 'bg-card' : '';
const borderClass = border ? 'border-t' : '';
// More generous spacing for a cleaner aesthetic
const paddingClasses = {
'default': 'py-12 md:py-20',
'sm': 'py-6 md:py-10',
'lg': 'py-16 md:py-24',
'xl': 'py-20 md:py-32',
'default': 'py-16 md:py-20',
'sm': 'py-8 md:py-12',
'lg': 'py-20 md:py-28',
'xl': 'py-28 md:py-40',
'none': '',
};
const paddingClass = paddingClasses[padding] || paddingClasses['default'];
@ -95,4 +106,4 @@ export function Grid({
{children}
</div>
);
}
}

View File

@ -2,6 +2,7 @@
import * as React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { motion, AnimatePresence } from 'motion/react';
import { MenuIcon, XIcon } from 'lucide-react';
@ -15,15 +16,14 @@ import { cn } from '@workspace/ui/lib/utils';
// import { Switch } from '@workspace/ui/components/ui/switch'; // Assuming you have a Switch component
const NAV_ITEMS = [
{ name: 'Home', href: '/' },
{ name: 'Solutions', href: '/solutions' },
{ name: 'Case Studies', href: '/case-studies' },
{ name: 'Stack', href: '/stack' },
{ name: 'Process', href: '/process' },
{ name: 'Pricing', href: '/pricing' },
{ name: 'HOME', href: '/' },
{ name: 'SOLUTIONS', href: '/solutions' },
{ name: 'CASE STUDIES', href: '/case-studies' },
{ name: 'STACK', href: '/stack' },
{ name: 'PROCESS', href: '/process' },
{ name: 'PRICING', href: '/pricing' },
{ name: 'FAQ', href: '/faq' },
{ name: 'About', href: '/about' },
{ name: 'Contact', href: '/contact' },
{ name: 'ABOUT', href: '/about' },
];
export function Navbar() {
@ -39,98 +39,97 @@ export function Navbar() {
};
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-sm">
<div className="container flex h-16 items-center justify-between px-4 md:px-6">
{/* Logo / Brand */}
<Link href="/" className="flex items-center space-x-2" aria-label="Fortura Data Solutions">
{/* Replace with your actual logo component or image */}
<span className="font-bold text-xl">Fortura</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-6">
{NAV_ITEMS.map((item) => (
<Link
key={item.name}
href={item.href}
className={cn(
'text-sm font-medium transition-colors hover:text-primary',
pathname === item.href ? 'text-primary' : 'text-muted-foreground'
)}
aria-current={pathname === item.href ? 'page' : undefined}
>
{item.name}
<header className="sticky top-4 z-50 w-full bg-transparent">
<div className="px-4 md:px-6">
{/* Floating pill nav */}
<div className="mx-auto max-w-6xl border rounded-full bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60 shadow-md">
<div className="flex h-16 items-center gap-4 px-4 md:px-6">
{/* Logo / Brand */}
<Link href="/" className="flex items-center gap-3" aria-label="Fortura Data Solutions">
<Image src="/logo-dark.png" alt="Fortura logo" width={36} height={36} className="hidden dark:block" />
<Image src="/logo-light.png" alt="Fortura logo" width={36} height={36} className="block dark:hidden" />
<span className="font-bold text-sm tracking-wider uppercase">Fortura</span>
</Link>
))}
</nav>
{/* Desktop CTA (Optional, e.g., Book Call Button) */}
<div className="hidden md:block">
<Button size="sm" variant="default" asChild>
<Link href="/contact">Book Architecture Call</Link>
</Button>
</div>
{/* Mobile Menu Trigger */}
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden" aria-label="Toggle navigation menu">
{isOpen ? <XIcon className="size-6" /> : <MenuIcon className="size-6" />}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
<div className="flex flex-col h-full">
{/* Mobile Logo / Close Button Area */}
<div className="flex items-center justify-between pt-4 px-2">
<Link href="/" className="flex items-center space-x-2" onClick={handleLinkClick} aria-label="Fortura Data Solutions">
<span className="font-bold text-xl">Fortura</span>
{/* Desktop Navigation (center) */}
<nav className="hidden md:flex items-center gap-8 mx-auto">
{NAV_ITEMS.map((item) => (
<Link
key={item.name}
href={item.href}
className={cn(
'text-[12px] tracking-wider transition-colors pb-0.5 border-b border-transparent hover:text-primary/90 hover:border-primary/30',
pathname === item.href ? 'text-primary border-primary/80' : 'text-muted-foreground'
)}
aria-current={pathname === item.href ? 'page' : undefined}
>
{item.name}
</Link>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Close menu">
<XIcon className="size-6" />
</Button>
</SheetTrigger>
</div>
))}
</nav>
{/* Mobile Navigation Links */}
<nav className="flex flex-col mt-8 px-2">
{NAV_ITEMS.map((item) => (
<Link
key={item.name}
href={item.href}
onClick={handleLinkClick}
className={cn(
'py-3 text-base font-medium transition-colors hover:text-primary border-b',
pathname === item.href ? 'text-primary' : 'text-muted-foreground'
)}
aria-current={pathname === item.href ? 'page' : undefined}
>
{item.name}
</Link>
))}
</nav>
{/* Mobile CTA */}
<div className="mt-auto p-4">
<Button className="w-full" variant="default" asChild>
<Link href="/contact" onClick={handleLinkClick}>Book Architecture Call</Link>
</Button>
</div>
{/* Theme Toggle (if implemented)
<div className="flex items-center justify-between p-4 border-t">
<span className="text-sm font-medium">Dark Mode</span>
<Switch
checked={isDarkMode}
onCheckedChange={() => setTheme(isDarkMode ? 'light' : 'dark')}
aria-label="Toggle dark mode"
/>
</div>
*/}
{/* Desktop CTA (far right) */}
<div className="hidden md:block ml-auto">
<Link href="/contact" className="text-[12px] tracking-wider text-primary hover:underline">
BOOK CALL
</Link>
</div>
</SheetContent>
</Sheet>
{/* Mobile Menu Trigger */}
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden" aria-label="Toggle navigation menu">
{isOpen ? <XIcon className="size-6" /> : <MenuIcon className="size-6" />}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
<div className="flex flex-col h-full">
{/* Mobile Logo / Close Button Area */}
<div className="flex items-center justify-between pt-4 px-2">
<Link href="/" className="flex items-center space-x-2" onClick={handleLinkClick} aria-label="Fortura Data Solutions">
<span className="font-bold text-xl">Fortura</span>
</Link>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Close menu">
<XIcon className="size-6" />
</Button>
</SheetTrigger>
</div>
{/* Mobile Navigation Links */}
<nav className="flex flex-col mt-8 px-2">
{NAV_ITEMS.map((item) => (
<Link
key={item.name}
href={item.href}
onClick={handleLinkClick}
className={cn(
'py-3 text-sm tracking-wider transition-colors hover:text-primary border-b border-border/60',
pathname === item.href ? 'text-primary' : 'text-muted-foreground'
)}
aria-current={pathname === item.href ? 'page' : undefined}
>
{item.name}
</Link>
))}
</nav>
{/* Mobile CTA */}
<div className="mt-auto p-4">
<Link
href="/contact"
onClick={handleLinkClick}
className="block w-full text-center text-sm tracking-wider text-primary hover:underline"
>
BOOK CALL
</Link>
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</div>
</header>
);
}
}

View File

@ -61,7 +61,8 @@
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
/* Industrial vibe: tight 2px radius */
--radius: 2px;
--sidebar: oklch(0.985 0 0);
@ -226,6 +227,50 @@
--color-sidebar-ring: var(--sidebar-ring);
}
/* Subtle background patterns inspired by reference sites */
.bg-stripes-dark {
background-image: repeating-linear-gradient(
135deg,
color-mix(in oklch, var(--color-background), var(--color-foreground) 6%) 0,
color-mix(in oklch, var(--color-background), var(--color-foreground) 6%) 2px,
transparent 2px,
transparent 12px
);
background-size: 16px 16px;
}
.bg-grid-dark {
background-image:
radial-gradient(circle at 1px 1px,
color-mix(in oklch, var(--color-foreground), transparent 92%) 1px,
transparent 0);
background-size: 20px 20px;
}
/* Keyword highlighting */
.keyword {
background-color: oklch(0.9 0 0); /* Light gray background */
padding: 0.125rem 0.25rem;
border-radius: var(--radius-sm);
font-weight: 500;
}
.dark .keyword {
background-color: oklch(0.3 0 0); /* Darker gray for dark mode */
}
/* Platform list styling */
.group:hover .text-muted-foreground {
color: oklch(0.7 0 0); /* Slightly darker on hover */
}
.dark .group:hover .text-muted-foreground {
color: oklch(0.8 0 0); /* Slightly lighter on hover */
}
/* Minimal, theme-aware scrollbar styling */
/* Remove custom scrollbar to restore native */
@layer base {
* {
@apply border-border outline-ring/50;
@ -253,4 +298,67 @@
opacity: 0.6;
}
}
/* Generous spacing for a cleaner aesthetic */
.space-y-6 > * + * {
margin-top: 1.5rem;
}
.space-y-8 > * + * {
margin-top: 2rem;
}
.space-y-12 > * + * {
margin-top: 3rem;
}
.space-y-16 > * + * {
margin-top: 4rem;
}
.space-y-20 > * + * {
margin-top: 5rem;
}
.space-y-24 > * + * {
margin-top: 6rem;
}
/* Section padding adjustments for more generous spacing */
.py-16 {
padding-top: 4rem;
padding-bottom: 4rem;
}
.py-20 {
padding-top: 5rem;
padding-bottom: 5rem;
}
.py-24 {
padding-top: 6rem;
padding-bottom: 6rem;
}
.py-28 {
padding-top: 7rem;
padding-bottom: 7rem;
}
/* Generous gap utilities */
.gap-12 {
gap: 3rem;
}
.gap-16 {
gap: 4rem;
}
.gap-20 {
gap: 5rem;
}
.gap-24 {
gap: 6rem;
}
}

318
pnpm-lock.yaml generated
View File

@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
zod: ^4.0.0
importers:
.:
@ -17,7 +20,7 @@ importers:
devDependencies:
'@commitlint/cli':
specifier: ^19.8.0
version: 19.8.1(@types/node@22.13.8)(typescript@5.7.3)
version: 19.8.1(@types/node@24.3.0)(typescript@5.9.2)
'@commitlint/config-conventional':
specifier: ^19.8.0
version: 19.8.1
@ -28,13 +31,13 @@ importers:
specifier: ^4.1.4
version: 4.1.12
'@types/node':
specifier: 22.13.8
version: 22.13.8
specifier: 24.3.0
version: 24.3.0
'@types/react':
specifier: ^19.0.10
specifier: ^19.1.10
version: 19.1.10
'@types/react-dom':
specifier: ^19.0.4
specifier: ^19.1.7
version: 19.1.7(@types/react@19.1.10)
'@workspace/eslint-config':
specifier: workspace:*
@ -42,27 +45,72 @@ importers:
'@workspace/typescript-config':
specifier: workspace:*
version: link:packages/typescript-config
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
eslint:
specifier: ^9.20.1
version: 9.33.0(jiti@2.5.1)
fumadocs-core:
specifier: ^15.6.12
version: 15.6.12(@types/react@19.1.10)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
fumadocs-mdx:
specifier: ^11.7.5
version: 11.7.5(acorn@8.15.0)(fumadocs-core@15.6.12(@types/react@19.1.10)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)
fumadocs-ui:
specifier: ^15.6.12
version: 15.6.12(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tailwindcss@4.1.12)
husky:
specifier: ^9.1.7
version: 9.1.7
motion:
specifier: ^12.23.12
version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
next:
specifier: ^15.5.0
version: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
postcss:
specifier: ^8.5.3
version: 8.5.6
prettier:
specifier: ^3.5.3
version: 3.6.2
radix-ui:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react:
specifier: ^19.1.1
version: 19.1.1
react-day-picker:
specifier: ^9.9.0
version: 9.9.0(react@19.1.1)
react-dom:
specifier: ^19.1.1
version: 19.1.1(react@19.1.1)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tailwindcss:
specifier: ^4.1.4
version: 4.1.12
turbo:
specifier: ^2.4.2
version: 2.5.6
tw-animate-css:
specifier: ^1.3.7
version: 1.3.7
typescript:
specifier: 5.7.3
version: 5.7.3
specifier: 5.9.2
version: 5.9.2
zod:
specifier: ^4.0.0
version: 4.0.17
apps/www:
dependencies:
@ -86,7 +134,7 @@ importers:
version: 4.1.0
eslint-config-next:
specifier: ^15.3.3
version: 15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
version: 15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
fumadocs-core:
specifier: ^15.2.14
version: 15.6.12(@types/react@19.1.10)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -134,13 +182,13 @@ importers:
version: 6.0.1
shadcn:
specifier: 2.4.0-canary.13
version: 2.4.0-canary.13(@types/node@22.13.8)(typescript@5.7.3)
version: 2.4.0-canary.13(@types/node@24.3.0)(typescript@5.9.2)
shiki:
specifier: ^3.2.1
version: 3.11.0
zod:
specifier: ^3.24.2
version: 3.25.76
specifier: ^4.0.0
version: 4.0.17
devDependencies:
'@types/mdx':
specifier: ^2.0.13
@ -203,7 +251,7 @@ importers:
version: 4.1.0
eslint-config-next:
specifier: ^15.3.3
version: 15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
version: 15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
fumadocs-core:
specifier: ^15.2.6
version: 15.6.12(@types/react@19.1.10)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -241,12 +289,12 @@ importers:
specifier: ^1.2.4
version: 1.3.7
zod:
specifier: ^3.24.2
version: 3.25.76
specifier: ^4.0.0
version: 4.0.17
devDependencies:
'@turbo/gen':
specifier: ^2.4.2
version: 2.5.6(@types/node@22.13.8)(typescript@5.7.3)
version: 2.5.6(@types/node@24.3.0)(typescript@5.9.2)
packages:
@ -490,6 +538,9 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
@ -2079,6 +2130,9 @@ packages:
'@types/node@22.13.8':
resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==}
'@types/node@24.3.0':
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
@ -2767,6 +2821,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@ -4659,6 +4716,12 @@ packages:
date-fns: ^2.28.0 || ^3.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-day-picker@9.9.0:
resolution: {integrity: sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8.0'
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies:
@ -5348,6 +5411,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
hasBin: true
uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'}
@ -5360,6 +5428,9 @@ packages:
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@ -5549,9 +5620,6 @@ packages:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.0.17:
resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==}
@ -5779,11 +5847,11 @@ snapshots:
'@types/tough-cookie': 4.0.5
tough-cookie: 4.1.4
'@commitlint/cli@19.8.1(@types/node@22.13.8)(typescript@5.7.3)':
'@commitlint/cli@19.8.1(@types/node@24.3.0)(typescript@5.9.2)':
dependencies:
'@commitlint/format': 19.8.1
'@commitlint/lint': 19.8.1
'@commitlint/load': 19.8.1(@types/node@22.13.8)(typescript@5.7.3)
'@commitlint/load': 19.8.1(@types/node@24.3.0)(typescript@5.9.2)
'@commitlint/read': 19.8.1
'@commitlint/types': 19.8.1
tinyexec: 1.0.1
@ -5830,15 +5898,15 @@ snapshots:
'@commitlint/rules': 19.8.1
'@commitlint/types': 19.8.1
'@commitlint/load@19.8.1(@types/node@22.13.8)(typescript@5.7.3)':
'@commitlint/load@19.8.1(@types/node@24.3.0)(typescript@5.9.2)':
dependencies:
'@commitlint/config-validator': 19.8.1
'@commitlint/execute-rule': 19.8.1
'@commitlint/resolve-extends': 19.8.1
'@commitlint/types': 19.8.1
chalk: 5.6.0
cosmiconfig: 9.0.0(typescript@5.7.3)
cosmiconfig-typescript-loader: 6.1.0(@types/node@22.13.8)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3)
cosmiconfig: 9.0.0(typescript@5.9.2)
cosmiconfig-typescript-loader: 6.1.0(@types/node@24.3.0)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2)
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
@ -5893,6 +5961,8 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@date-fns/tz@1.4.1': {}
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.4
@ -6199,17 +6269,17 @@ snapshots:
'@img/sharp-win32-x64@0.34.3':
optional: true
'@inquirer/confirm@5.1.15(@types/node@22.13.8)':
'@inquirer/confirm@5.1.15(@types/node@24.3.0)':
dependencies:
'@inquirer/core': 10.1.15(@types/node@22.13.8)
'@inquirer/type': 3.0.8(@types/node@22.13.8)
'@inquirer/core': 10.1.15(@types/node@24.3.0)
'@inquirer/type': 3.0.8(@types/node@24.3.0)
optionalDependencies:
'@types/node': 22.13.8
'@types/node': 24.3.0
'@inquirer/core@10.1.15(@types/node@22.13.8)':
'@inquirer/core@10.1.15(@types/node@24.3.0)':
dependencies:
'@inquirer/figures': 1.0.13
'@inquirer/type': 3.0.8(@types/node@22.13.8)
'@inquirer/type': 3.0.8(@types/node@24.3.0)
ansi-escapes: 4.3.2
cli-width: 4.1.0
mute-stream: 2.0.0
@ -6217,20 +6287,20 @@ snapshots:
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.8
'@types/node': 24.3.0
'@inquirer/external-editor@1.0.1(@types/node@22.13.8)':
'@inquirer/external-editor@1.0.1(@types/node@24.3.0)':
dependencies:
chardet: 2.1.0
iconv-lite: 0.6.3
optionalDependencies:
'@types/node': 22.13.8
'@types/node': 24.3.0
'@inquirer/figures@1.0.13': {}
'@inquirer/type@3.0.8(@types/node@22.13.8)':
'@inquirer/type@3.0.8(@types/node@24.3.0)':
optionalDependencies:
'@types/node': 22.13.8
'@types/node': 24.3.0
'@internationalized/date@3.8.2':
dependencies:
@ -7416,17 +7486,17 @@ snapshots:
'@tsconfig/node16@1.0.4': {}
'@turbo/gen@2.5.6(@types/node@22.13.8)(typescript@5.7.3)':
'@turbo/gen@2.5.6(@types/node@24.3.0)(typescript@5.9.2)':
dependencies:
'@turbo/workspaces': 2.5.6(@types/node@22.13.8)
'@turbo/workspaces': 2.5.6(@types/node@24.3.0)
commander: 10.0.1
fs-extra: 10.1.0
inquirer: 8.2.7(@types/node@22.13.8)
inquirer: 8.2.7(@types/node@24.3.0)
minimatch: 9.0.5
node-plop: 0.26.3
picocolors: 1.0.1
proxy-agent: 6.5.0
ts-node: 10.9.2(@types/node@22.13.8)(typescript@5.7.3)
ts-node: 10.9.2(@types/node@24.3.0)(typescript@5.9.2)
update-check: 1.5.4
validate-npm-package-name: 5.0.1
transitivePeerDependencies:
@ -7436,14 +7506,14 @@ snapshots:
- supports-color
- typescript
'@turbo/workspaces@2.5.6(@types/node@22.13.8)':
'@turbo/workspaces@2.5.6(@types/node@24.3.0)':
dependencies:
commander: 10.0.1
execa: 5.1.1
fast-glob: 3.3.3
fs-extra: 10.1.0
gradient-string: 2.0.2
inquirer: 8.2.7(@types/node@22.13.8)
inquirer: 8.2.7(@types/node@24.3.0)
js-yaml: 4.1.0
ora: 4.1.1
picocolors: 1.0.1
@ -7459,7 +7529,7 @@ snapshots:
'@types/conventional-commits-parser@5.0.1':
dependencies:
'@types/node': 22.13.8
'@types/node': 24.3.0
'@types/cookie@0.6.0': {}
@ -7531,6 +7601,10 @@ snapshots:
dependencies:
undici-types: 6.20.0
'@types/node@24.3.0':
dependencies:
undici-types: 7.10.0
'@types/raf@3.4.3':
optional: true
@ -7578,6 +7652,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@typescript-eslint/scope-manager': 8.40.0
'@typescript-eslint/type-utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.40.0
eslint: 9.33.0(jiti@2.5.1)
graphemer: 1.4.0
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.40.0
@ -7590,6 +7681,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.40.0
'@typescript-eslint/types': 8.40.0
'@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.40.0
debug: 4.4.1
eslint: 9.33.0(jiti@2.5.1)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.40.0(typescript@5.7.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.7.3)
@ -7599,6 +7702,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.40.0(typescript@5.9.2)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.9.2)
'@typescript-eslint/types': 8.40.0
debug: 4.4.1
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.40.0':
dependencies:
'@typescript-eslint/types': 8.40.0
@ -7608,6 +7720,10 @@ snapshots:
dependencies:
typescript: 5.7.3
'@typescript-eslint/tsconfig-utils@8.40.0(typescript@5.9.2)':
dependencies:
typescript: 5.9.2
'@typescript-eslint/type-utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)':
dependencies:
'@typescript-eslint/types': 8.40.0
@ -7620,6 +7736,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/type-utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)':
dependencies:
'@typescript-eslint/types': 8.40.0
'@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
debug: 4.4.1
eslint: 9.33.0(jiti@2.5.1)
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.40.0': {}
'@typescript-eslint/typescript-estree@8.40.0(typescript@5.7.3)':
@ -7638,6 +7766,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.40.0(typescript@5.9.2)':
dependencies:
'@typescript-eslint/project-service': 8.40.0(typescript@5.9.2)
'@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.9.2)
'@typescript-eslint/types': 8.40.0
'@typescript-eslint/visitor-keys': 8.40.0
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.2
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
@ -7649,6 +7793,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0(jiti@2.5.1))
'@typescript-eslint/scope-manager': 8.40.0
'@typescript-eslint/types': 8.40.0
'@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.2)
eslint: 9.33.0(jiti@2.5.1)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.40.0':
dependencies:
'@typescript-eslint/types': 8.40.0
@ -8127,30 +8282,30 @@ snapshots:
core-js@3.45.0:
optional: true
cosmiconfig-typescript-loader@6.1.0(@types/node@22.13.8)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3):
cosmiconfig-typescript-loader@6.1.0(@types/node@24.3.0)(cosmiconfig@9.0.0(typescript@5.9.2))(typescript@5.9.2):
dependencies:
'@types/node': 22.13.8
cosmiconfig: 9.0.0(typescript@5.7.3)
'@types/node': 24.3.0
cosmiconfig: 9.0.0(typescript@5.9.2)
jiti: 2.5.1
typescript: 5.7.3
typescript: 5.9.2
cosmiconfig@8.3.6(typescript@5.7.3):
cosmiconfig@8.3.6(typescript@5.9.2):
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.0
parse-json: 5.2.0
path-type: 4.0.0
optionalDependencies:
typescript: 5.7.3
typescript: 5.9.2
cosmiconfig@9.0.0(typescript@5.7.3):
cosmiconfig@9.0.0(typescript@5.9.2):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.0
parse-json: 5.2.0
optionalDependencies:
typescript: 5.7.3
typescript: 5.9.2
create-require@1.1.1: {}
@ -8232,6 +8387,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {}
debug@3.2.7:
@ -8515,21 +8672,21 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-next@15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3):
eslint-config-next@15.5.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2):
dependencies:
'@next/eslint-plugin-next': 15.5.0
'@rushstack/eslint-patch': 1.12.0
'@typescript-eslint/eslint-plugin': 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
'@typescript-eslint/eslint-plugin': 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
eslint: 9.33.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-react: 7.37.5(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.33.0(jiti@2.5.1))
optionalDependencies:
typescript: 5.7.3
typescript: 5.9.2
transitivePeerDependencies:
- eslint-import-resolver-webpack
- eslint-plugin-import-x
@ -8558,22 +8715,22 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
eslint: 9.33.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.5.1))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -8584,7 +8741,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.33.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.5.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -8596,7 +8753,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.7.3)
'@typescript-eslint/parser': 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@ -9321,9 +9478,9 @@ snapshots:
strip-ansi: 6.0.1
through: 2.3.8
inquirer@8.2.7(@types/node@22.13.8):
inquirer@8.2.7(@types/node@24.3.0):
dependencies:
'@inquirer/external-editor': 1.0.1(@types/node@22.13.8)
'@inquirer/external-editor': 1.0.1(@types/node@24.3.0)
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-cursor: 3.1.0
@ -10233,12 +10390,12 @@ snapshots:
ms@2.1.3: {}
msw@2.10.5(@types/node@22.13.8)(typescript@5.7.3):
msw@2.10.5(@types/node@24.3.0)(typescript@5.9.2):
dependencies:
'@bundled-es-modules/cookie': 2.0.1
'@bundled-es-modules/statuses': 1.0.1
'@bundled-es-modules/tough-cookie': 0.1.6
'@inquirer/confirm': 5.1.15(@types/node@22.13.8)
'@inquirer/confirm': 5.1.15(@types/node@24.3.0)
'@mswjs/interceptors': 0.39.6
'@open-draft/deferred-promise': 2.2.0
'@open-draft/until': 2.1.0
@ -10254,7 +10411,7 @@ snapshots:
type-fest: 4.41.0
yargs: 17.7.2
optionalDependencies:
typescript: 5.7.3
typescript: 5.9.2
transitivePeerDependencies:
- '@types/node'
@ -10707,6 +10864,13 @@ snapshots:
date-fns: 4.1.0
react: 19.1.1
react-day-picker@9.9.0(react@19.1.1):
dependencies:
'@date-fns/tz': 1.4.1
date-fns: 4.1.0
date-fns-jalali: 4.1.0-0
react: 19.1.1
react-dom@19.1.1(react@19.1.1):
dependencies:
react: 19.1.1
@ -11065,14 +11229,14 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
shadcn@2.4.0-canary.13(@types/node@22.13.8)(typescript@5.7.3):
shadcn@2.4.0-canary.13(@types/node@24.3.0)(typescript@5.9.2):
dependencies:
'@antfu/ni': 23.3.1
'@babel/core': 7.28.3
'@babel/parser': 7.28.3
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3)
commander: 10.0.1
cosmiconfig: 8.3.6(typescript@5.7.3)
cosmiconfig: 8.3.6(typescript@5.9.2)
deepmerge: 4.3.1
diff: 5.2.0
execa: 7.2.0
@ -11080,7 +11244,7 @@ snapshots:
fs-extra: 11.3.1
https-proxy-agent: 6.2.1
kleur: 4.1.5
msw: 2.10.5(@types/node@22.13.8)(typescript@5.7.3)
msw: 2.10.5(@types/node@24.3.0)(typescript@5.9.2)
node-fetch: 3.3.2
ora: 6.3.1
postcss: 8.5.6
@ -11089,7 +11253,7 @@ snapshots:
stringify-object: 5.0.0
ts-morph: 18.0.0
tsconfig-paths: 4.2.0
zod: 3.25.76
zod: 4.0.17
transitivePeerDependencies:
- '@types/node'
- supports-color
@ -11427,26 +11591,30 @@ snapshots:
dependencies:
typescript: 5.7.3
ts-api-utils@2.1.0(typescript@5.9.2):
dependencies:
typescript: 5.9.2
ts-morph@18.0.0:
dependencies:
'@ts-morph/common': 0.19.0
code-block-writer: 12.0.0
ts-node@10.9.2(@types/node@22.13.8)(typescript@5.7.3):
ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.13.8
'@types/node': 24.3.0
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.7.3
typescript: 5.9.2
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
@ -11557,6 +11725,8 @@ snapshots:
typescript@5.7.3: {}
typescript@5.9.2: {}
uglify-js@3.19.3:
optional: true
@ -11569,6 +11739,8 @@ snapshots:
undici-types@6.20.0: {}
undici-types@7.10.0: {}
unicorn-magic@0.1.0: {}
unified@11.0.5:
@ -11819,8 +11991,6 @@ snapshots:
yoctocolors-cjs@2.1.2: {}
zod@3.25.76: {}
zod@4.0.17: {}
zwitch@2.0.4: {}

0
turbo Normal file
View File