Full Code of zinedkaloc/aipage.dev for AI

master 508cff651c7c cached
105 files
179.8 KB
47.9k tokens
139 symbols
1 requests
Download .txt
Showing preview only (203K chars total). Download the full file or copy to clipboard to get everything.
Repository: zinedkaloc/aipage.dev
Branch: master
Commit: 508cff651c7c
Files: 105
Total size: 179.8 KB

Directory structure:
gitextract_yf13pds3/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│   ├── (aipage)/
│   │   ├── layout.tsx
│   │   ├── loading.tsx
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   └── profile/
│   │       ├── invoices/
│   │       │   ├── loading.tsx
│   │       │   └── page.tsx
│   │       ├── layout.tsx
│   │       ├── projects/
│   │       │   ├── [id]/
│   │       │   │   ├── domains/
│   │       │   │   │   ├── layout.tsx
│   │       │   │   │   └── page.tsx
│   │       │   │   ├── integrations/
│   │       │   │   │   └── page.tsx
│   │       │   │   ├── layout.tsx
│   │       │   │   ├── loading.tsx
│   │       │   │   ├── page.tsx
│   │       │   │   └── settings/
│   │       │   │       └── page.tsx
│   │       │   ├── loading.tsx
│   │       │   └── page.tsx
│   │       └── settings/
│   │           ├── loading.tsx
│   │           └── page.tsx
│   ├── (preview)/
│   │   ├── not-found.tsx
│   │   └── profile/
│   │       └── projects/
│   │           └── [id]/
│   │               └── preview/
│   │                   ├── loading.tsx
│   │                   └── page.tsx
│   ├── api/
│   │   ├── chat/
│   │   │   └── route.ts
│   │   ├── create-checkout-session/
│   │   │   └── route.ts
│   │   ├── domain/
│   │   │   ├── check-domain/
│   │   │   │   └── route.ts
│   │   │   ├── remove-domain/
│   │   │   │   └── route.ts
│   │   │   └── verify-domain/
│   │   │       └── route.ts
│   │   ├── logout/
│   │   │   └── route.ts
│   │   ├── message/
│   │   │   └── route.ts
│   │   ├── og/
│   │   │   └── route.tsx
│   │   ├── project/
│   │   │   ├── [id]/
│   │   │   │   ├── domain/
│   │   │   │   │   └── route.ts
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   └── user/
│   │       └── route.ts
│   ├── auth-redirect/
│   │   └── route.ts
│   ├── layout.tsx
│   └── not-found.tsx
├── components/
│   ├── AddDomainModal.tsx
│   ├── AlertCircleFill.tsx
│   ├── AlertDialog.tsx
│   ├── AuthModal.tsx
│   ├── Badge.tsx
│   ├── BrowserWindow.tsx
│   ├── Button.tsx
│   ├── Chart.tsx
│   ├── CheckCircleFill.tsx
│   ├── ConfiguredSection.tsx
│   ├── ConfirmDialog.tsx
│   ├── DeleteAccountConfirmDialog.tsx
│   ├── DeleteProjectConfirmDialog.tsx
│   ├── Divider.tsx
│   ├── DomainCard.tsx
│   ├── DomainConfiguration.tsx
│   ├── Drawer.tsx
│   ├── HTMLPreview.tsx
│   ├── Header.tsx
│   ├── IconMenu.tsx
│   ├── ListProjects.tsx
│   ├── LoadingIcon.tsx
│   ├── Logo.tsx
│   ├── LogoutIcon.tsx
│   ├── Modal.tsx
│   ├── NavLink.tsx
│   ├── Popover.tsx
│   ├── PricesModal.tsx
│   ├── Product.tsx
│   ├── Products.tsx
│   ├── ProfileLayout.tsx
│   ├── ProfileMenu.tsx
│   ├── ProjectDesign.tsx
│   ├── ProjectIcon.tsx
│   ├── ProjectSelect.tsx
│   ├── RateModal.tsx
│   ├── ShowRate.tsx
│   ├── Switch.tsx
│   ├── UserDropdown.tsx
│   ├── XCircleFill.tsx
│   ├── customDropdown.tsx
│   ├── loadingSpinner.module.css
│   ├── loadingSpinner.tsx
│   └── tweetButton.tsx
├── context/
│   └── AuthContext.tsx
├── custom.css
├── hooks/
│   ├── useProject.tsx
│   ├── useProjectList.tsx
│   └── useSearchParams.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public/
│   └── site.webmanifest
├── styles/
│   ├── custom.css
│   └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── types/
│   └── index.ts
└── utils/
    ├── altogic.ts
    ├── auth.ts
    ├── helpers.ts
    └── redis.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a bug report for AI landing page generator
title: "[BUG]"
labels: ''
assignees: ''

---

# Bug Report

**Description**
⚠️ Please provide a clear and concise description of the bug.

**Steps to Reproduce**
⚠️ Please provide step-by-step instructions to reproduce the bug.

**Expected Behavior**
⚠️ Please describe what you expected to happen.

**Actual Behavior**
⚠️ Please describe what actually happened.

**Additional Information**
⚠️ Add any additional information or context about the bug here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

# Feature Request

**Description**
Please provide a clear and concise description of the feature request.

**Proposed Solution**
Please describe the proposed solution or new feature in detail.

**Alternatives Considered**
Please describe any alternative solutions or features you've considered.

**Additional Information**
Add any additional information or context about the feature request here.


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
.env
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/
/.idea

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

As contributors and maintainers of the AI Landing Page Generator project, we pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

- Being respectful and considerate of others' opinions and ideas.
- Using inclusive language and being mindful of our words and their impact.
- Being open to constructive feedback and providing feedback in a respectful manner.
- Showing empathy and kindness towards others.
- Focusing on collaboration and fostering a supportive community.

Examples of unacceptable behavior include:

- Harassment, discrimination, or derogatory comments and personal attacks.
- Any form of offensive or inappropriate language or imagery.
- Trolling, flaming, or insulting/derogatory comments.
- Intimidating or bullying behavior.
- Any other conduct that could be considered inappropriate in a professional setting.

## Scope

This Code of Conduct applies to all project contributors, both online and offline, as well as in all project-related spaces.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [zinedkaloc](https://twitter.com/zinedkaloc). The project team is committed to reviewing and addressing all reported incidents promptly and fairly.

## Consequences

Any contributor who engages in behavior violating this Code of Conduct may be temporarily or permanently excluded from project participation at the discretion of the project team.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to AI Landing Page Generator

We welcome and appreciate contributions from the community! By contributing to the AI Landing Page Generator project, you can help us make it even better.

## Ways to Contribute

There are several ways you can contribute to this project:

- Report bugs: If you come across any issues or bugs, please [open a new issue](https://github.com/zinedkaloc/ai-page/issues) and provide as much detail as possible.

- Suggest new features: Have an idea for a new feature or improvement? We'd love to hear it! [Open a new issue](https://github.com/zinedkaloc/ai-page/issues) and let us know.

- Submit pull requests: If you have code changes or enhancements you'd like to contribute, you can do so by opening a pull request. Make sure to follow the guidelines below when submitting your pull request.

## Guidelines for Pull Requests

To ensure smooth collaboration and maintain code quality, please follow these guidelines when submitting a pull request:

1. Fork the repository and create a new branch for your changes.

2. Before making changes, make sure your branch is up to date with the master repository.

3. Follow the coding style and conventions used in the project.

4. Include clear and concise commit messages that describe the purpose of your changes.

5. Provide a detailed description of the changes you've made in the pull request.

6. Test your changes thoroughly to ensure they work as intended.

7. Make sure your code is properly documented.

8. Ensure that your changes do not introduce any breaking changes to the existing functionality.

9. Be responsive to any feedback or questions regarding your pull request.

10. Once your changes are approved, they will be merged into the master repository.

## Code of Conduct

Please note that by participating in this project, you are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). We kindly ask you to respect the guidelines and treat others with respect and kindness.

## Questions or Concerns

If you have any questions or concerns regarding the project or the contribution process, please feel free to [zinedkaloc](https://twitter.com/zinedkaloc).

Thank you for your interest in contributing to the AI Landing Page Generator project!


================================================
FILE: LICENSE
================================================
MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# AI Landing Page Generator

[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## Description

![A landing page generator](/public/logo.png)

The AI Landing Page Generator is a powerful tool that allows you to quickly create stunning landing pages using artificial intelligence. With this generator, you can save time and effort by automating the process of designing and coding landing pages. It is now experimental and only supports the creation of landing pages with HTML and Tailwind CSS.

## Installation

1. Clone the repository: `git clone https://github.com/zinedkaloc/aipage.dev.git`
2. Install the required dependencies: `pnpm install`
3. Navigate to the project directory `cd aipage.dev`
4. Copy the `.env.example` file to `.env` and fill in the required information.

> Note: If you are not comfortable with creating OPENAI_API_KEY, please reach out to me at [zinedkaloc](https://twitter.com/zinedkaloc) and I will be happy to help you.

## Usage

1. Run the generator: `pnpm dev`
2. Open the browser and go to `http://localhost:3000`
3. Describe your website and click on the "Enter".
4. Wait for the genie to generate the landing page.

### Test prompts

> A landing page for a design studio located in New york. They make websites and custom logos. All the work is very stylish and minimalist. The page should include a contact form a some call to actions as well as a link to the portfolio.

> A landing page for an interior architecture company based in Istanbul. They specialize in creating exceptional spatial designs and innovative interior solutions. Their work showcases a unique blend of style, minimalism, and functionality. The landing page should feature a captivating design with using gradients, including a contact form, compelling call-to-action elements, and a prominent link to their portfolio.

> A landing page for a cutting-edge technology company based in Istanbul. They specialize in developing innovative software solutions and advanced technological products. Their work is known for its sleek design, seamless user experience, and transformative capabilities. The landing page should feature a modern design, including a contact form, compelling call-to-action elements, and a prominent link to their product offerings.

## Roadmap

- [x] Create a basic landing page generator.
- [x] Display the generated landing page in the main page with an iframe.
- [ ] Update text input to textarea and add submit button.
- [ ] Add support to download the generated landing page as a zip file.
- [ ] Add support to deploy the generated landing page to Vercel or Netlify with a single click.
- [ ] Add support for dark mode.
- [ ] Add support for multiple pages.
- [ ] Add support for multiple languages.

This project is still in its early stages of development. If you have any suggestions or ideas, please feel free to open an issue or submit a pull request.

## Contributing

Thank you for considering contributing to this project! To contribute, follow these steps:

1. Fork the repository.
2. Create a new branch for your feature/fix: `git checkout -b feature/your-feature-name`.
3. Add your changes with: `git add .`.
4. Make your changes and commit them: `git commit -m "Add your feature description"`.
5. Push to the branch: `git push origin feature/your-feature-name`.
6. Open a pull request to the `master` branch of the original repository. Provide a clear and descriptive title and description for your pull request. Include any relevant information or context that would help with the review process.
7. Wait for the maintainers to review your pull request. Make any necessary changes or address any feedback provided.
8. Once your pull request is approved, it will be merged into the `master` branch.

Congratulations on your contribution!

## License

This project is licensed under the [MIT License](LICENSE).

## Contact

For any inquiries or feedback, please reach out to us at [zinedkaloc](https://twitter.com/zinedkaloc).


================================================
FILE: app/(aipage)/layout.tsx
================================================
import "@smastrom/react-rating/style.css";
import { ReactNode } from "react";
import Header from "@/components/Header";

export default async function AipageLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    <>
      <Header />
      {children}
    </>
  );
}


================================================
FILE: app/(aipage)/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function RootLoading() {
  return (
    <div className="pt-[72px] flex items-center justify-center">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(aipage)/not-found.tsx
================================================
import Link from "next/link";
import Button from "@/components/Button";

export default async function NotFound() {
  return (
    <section className="bg-white fixed inset-0">
      <div className="container flex items-center min-h-screen px-6 py-12 mx-auto">
        <div>
          <p className="text-2xl font-medium text-blue-500">404 NOT FOUND</p>
          <h1 className="mt-3 text-3xl font-semibold text-gray-800 md:text-4xl">
            We can’t find that page
          </h1>
          <p className="mt-4 text-gray-500">
            Sorry, the page you are looking for doesn't exist or has been moved.
          </p>

          <div className="flex items-center mt-6 gap-x-3">
            <Link href="/profile/projects">
              <Button variant="pill">Go to projects</Button>
            </Link>
          </div>
        </div>
      </div>
    </section>
  );
}


================================================
FILE: app/(aipage)/page.tsx
================================================
"use client";
import { useChat } from "ai/react";
import { useEffect, useRef, useState } from "react";
import Frame from "react-frame-component";
import Image from "next/image";
import html2canvas from "html2canvas";
import TweetButton from "@/components/tweetButton";
import { useAuth } from "@/context/AuthContext";
import useSearchParams from "@/hooks/useSearchParams";
import RateModal from "@/components/RateModal";
import { cn, updateProject } from "@/utils/helpers";
import Link from "next/link";

enum DeviceSize {
  Mobile = "w-1/2",
  Tablet = "w-3/4",
  Desktop = "w-full",
}

export default function Chat() {
  const { user, setUser } = useAuth();
  const [lastMessageId, setLastMessageId] = useState<string | null>(null);
  const [hasNoCreditsError, setHasNoCreditsError] = useState(false);
  const { set } = useSearchParams();

  const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
    useChat({
      onResponse: (message) => {
        setHasNoCreditsError(false);
        setLastMessageId(null);
        decreaseCredit();
      },
      onFinish: async (message) => {
        try {
          const res = JSON.parse(message.content) as { credits: number };
          setCredits(res.credits);
          setHasNoCreditsError(res.credits === 0);
        } catch {
          await saveResult(message.content);
        }
      },
    });

  function decreaseCredit(by: number = 1) {
    if (user) {
      setUser({ ...user, credits: user.credits - by });
    }
  }

  function setCredits(credits: number) {
    if (user) {
      setUser({ ...user, credits });
    }
  }

  async function saveResult(result: string) {
    const { _id } = await updateProject({
      result,
    });
    setLastMessageId(_id);
    set("rateModal", "true");
  }

  const [iframeContent, setIframeContent] = useState("");
  const [imageSrc, setImageSrc] = useState<string>("");

  const [deviceSize, setDeviceSize] = useState(DeviceSize.Desktop);
  const iframeRef = useRef(null);
  const [fileName, setFileName] = useState("");
  const [selectedElement, setSelectedElement] = useState<Element | null>(null);
  const [editedContent, setEditedContent] = useState<string>("");
  const [editingMode, setEditingMode] = useState(false);
  const [codeViewActive, setCodeViewActive] = useState(false);
  const [isStopped, setIsStopped] = useState(false);

  const appendToIframe = (content: any) => {
    if (iframeRef.current) {
      const iframeDocument = (iframeRef.current as HTMLIFrameElement)
        .contentDocument;
      if (iframeDocument) {
        const newNode = iframeDocument.createElement("div");
        newNode.innerHTML = content;
        newNode.querySelectorAll<HTMLElement>("*").forEach((element) => {
          element.addEventListener("mouseover", () => {
            element.classList.add("outline-blue"); // Blue border
          });
          element.addEventListener("mouseout", () => {
            element.style.outline = "none";
          });
          element.addEventListener("click", () => {
            setSelectedElement(element);
            setEditedContent(element.innerHTML);
          });
        });
        requestAnimationFrame(() => {
          iframeDocument.body.appendChild(newNode);
        });
      }
    }
  };

  const captureIframeContent = async () => {
    if (iframeRef.current) {
      const iframeDocument = (iframeRef.current as HTMLIFrameElement)
        .contentDocument;
      if (iframeDocument) {
        const canvas = await html2canvas(iframeDocument.body);
        const imgURL = canvas.toDataURL();
        // You can use imgURL as the src for an image tag to display the image representation of the iframe content
        // For simplicity, let's just set it to a state variable
        setImageSrc(imgURL);
      }
    }
  };

  useEffect(() => {
    const stream = new EventSource("/api/chat");
    stream.onmessage = (event) => {
      appendToIframe(event.data);
    };

    return () => stream.close();
  }, []);

  useEffect(() => {
    const lastMessage = messages[messages.length - 1];
    if (lastMessage && lastMessage.role !== "user") {
      setIframeContent(lastMessage.content);
    }
  }, [messages]);

  const handleSave = () => {
    const element = document.createElement("a");
    const file = new Blob([iframeContent], { type: "text/html" });
    element.href = URL.createObjectURL(file);
    element.download = fileName || "index.html";
    document.body.appendChild(element);
    element.click();
    document.body.removeChild(element);
    const completionInput = iframeContent;
  };

  const listenersMap = useRef<
    Map<HTMLElement, { mouseover: () => void; mouseout: () => void }>
  >(new Map());

  // Create a map to store the listeners for each element

  const handleEdit = () => {
    if (editingMode) {
      // Save the updated iframe content
      if (iframeRef.current) {
        const iframeDocument = (iframeRef.current as HTMLIFrameElement)
          .contentDocument;
        if (iframeDocument) {
          setIframeContent(iframeDocument.documentElement.innerHTML);
        }
      }

      // Disable editing mode by setting the contentEditable property of all elements to false and remove the event listeners
      if (iframeRef.current) {
        const iframeDocument = (iframeRef.current as HTMLIFrameElement)
          .contentDocument;
        if (iframeDocument) {
          iframeDocument
            .querySelectorAll<HTMLElement>("*")
            .forEach((element) => {
              element.contentEditable = "false";

              // Get the listeners for the element from the map
              const listeners = listenersMap.current.get(element);
              if (listeners) {
                // Remove the listeners
                element.removeEventListener("mouseover", listeners.mouseover);
                element.removeEventListener("mouseout", listeners.mouseout);
                // Remove the element from the map
                listenersMap.current.delete(element);
              }
            });
        }
      }
    } else {
      // Enable editing mode by setting the contentEditable property of all elements to true and add event listeners
      if (iframeRef.current) {
        const iframeDocument = (iframeRef.current as HTMLIFrameElement)
          .contentDocument;
        if (iframeDocument) {
          iframeDocument
            .querySelectorAll<HTMLElement>("*")
            .forEach((element) => {
              element.contentEditable = "true";

              // Create the event listeners
              const mouseoverListener = () => {
                element.classList.add("outline-blue");
                console.log("Mouseover event fired");
              };
              const mouseoutListener = () => {
                console.log("Mouseout event fired");
                element.classList.remove("outline-blue");
              };

              // Add the listeners to the element
              element.addEventListener("mouseover", mouseoverListener);
              element.addEventListener("mouseout", mouseoutListener);

              // Store the listeners in the map
              listenersMap.current.set(element, {
                mouseover: mouseoverListener,
                mouseout: mouseoutListener,
              });
            });
        }
      }
    }

    setEditingMode(!editingMode);
  };

  const handleUpdate = () => {
    if (selectedElement) {
      selectedElement.innerHTML = editedContent;
      setSelectedElement(null);
      setEditedContent("");
      if (iframeRef.current) {
        const iframeDocument = (iframeRef.current as HTMLIFrameElement)
          .contentDocument;
        if (iframeDocument) {
          setIframeContent(iframeDocument.documentElement.innerHTML);
        }
      }
    }
  };

  function onFocusHandler() {
    if (!user) {
      set("authModal", "true");
    }
  }

  const handleStop = async () => {
    stop();
    setIsStopped(true);
    await saveResult(iframeContent);
  };

  return (
    <>
      <div className="flex flex-col w-full min-h-screen bg-gradient-to-b from-white via-white to-slate-300 mx-auto px-4 md:px-16 lg:px-24 overflow-hidden items-center pt-24 md:pt-36">
        <section>
          <div className="fixed bottom-16 right-6 cursor-pointer transition-colors group">
            <div className="tooltip opacity-0 group-hover:opacity-100 bg-gray-700 text-white text-xs rounded py-1 px-2 absolute right-8 bottom-4 transform translate-y-2 w-64">
              Help spread the word! 📢 Post a tweet of your creation on Twitter
              and tag @aipagedev for early access to our exclusive beta—packed
              with stunning features. 🚀
            </div>
            <TweetButton />
          </div>
        </section>

        {/* Display the image if imageSrc is set */}

        <section>
          <div className="fixed bottom-6 right-6 cursor-pointer transition-colors group">
            <div className="tooltip opacity-0 group-hover:opacity-100 bg-gray-700 text-white text-xs rounded py-1 px-2 absolute  right-8 bottom-4 transform translate-y-2 w-48">
              Star us on Github to show your support
            </div>
            <a
              href="https://github.com/zinedkaloc/aipage.dev"
              target="_blank"
              rel="noreferrer"
              className="text-2xl"
            >
              ⭐️
            </a>
          </div>
        </section>

        {isLoading ? null : (
          <div className="relative py-6 flex flex-col justify-center">
            <Image
              src="/logoa.png"
              alt="AIPage.dev logo"
              width={200}
              height={200}
              className="mx-auto h-32 w-32"
            />
            <div className="text-center sm:w-11/12 md:w-[800px]">
              <h1 className="text-5xl font-bold text-ellipsis tracking-tight">
                🚧 Under Renovation 🚧
              </h1>

              <p className="text-lg text-gray-700 mt-4 tracking-tight">
                A refreshed experience is on the horizon. <br /> Follow us on
                <Link href="https://x.com/aipagedev">
                  <b> X</b>
                </Link>{" "}
                to stay updated!
              </p>
              <p className="text-xs pt-2 ml-4 font-medium text-gray-500 cursor-pointer animate-pulse"></p>
            </div>
          </div>
        )}

        {editingMode && selectedElement && (
          <div className="absolute z-50">
            <p>Edit the selected element:</p>
            <input
              value={editedContent}
              onChange={(e) => setEditedContent(e.target.value)}
            />
            <button onClick={handleUpdate}>Update</button>
          </div>
        )}

        {hasNoCreditsError ? (
          <div className="flex flex-col items-center justify-center">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="100"
              height="100"
              viewBox="0 0 100 100"
            >
              <polygon points="50,15 85,85 15,85" fill="#FF6B6B" />
              <text
                x="50"
                y="75"
                fontSize="40"
                fontWeight="bold"
                textAnchor="middle"
                fill="#FFFFFF"
              >
                !
              </text>
            </svg>
            <h1 className="text-3xl text-gray-800 mb-2">No Credits</h1>
            <p className="text-gray-600 mb-6">
              You do not have enough credits to proceed with this request today.
              Please try again tomorrow.
            </p>
          </div>
        ) : (
          iframeContent && (
            <div className="flex flex-col items-center py-4 w-full">
              <div className={cn(deviceSize)}>
                <div className="border flex items-center bg-white rounded-t-xl justify-between p-3 border-b lg:px-12 sticky top-4 z-10">
                  <div className="flex items-center space-x-2">
                    <div className="w-3 h-3 bg-red-500 rounded-full"></div>
                    <div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
                    <div className="w-3 h-3 bg-green-500 rounded-full"></div>
                  </div>
                  <div className="flex-1 text-center">
                    <div className="flex items-center justify-between space-x-2 bg-gray-200 rounded-xl mx-8 py-1 px-2">
                      <div className="flex items-center w-3 h-3"></div>
                      <div className="flex items-center space-x-2">
                        acme.co
                        {isLoading && (
                          <span className="ml-4 animate-spin">🟠</span>
                        )}
                        <button
                          className="ml-4 hidden md:flex"
                          onClick={() => setDeviceSize(DeviceSize.Mobile)}
                        >
                          📱
                        </button>
                        <button
                          className=" hidden md:flex"
                          onClick={() => setDeviceSize(DeviceSize.Tablet)}
                        >
                          💻
                        </button>
                        <button
                          className=" hidden md:flex"
                          onClick={() => setDeviceSize(DeviceSize.Desktop)}
                        >
                          🖥️
                        </button>
                        <button
                          className=""
                          onClick={() => setCodeViewActive(!codeViewActive)}
                        >
                          {codeViewActive ? "🖼️" : "🖨️"}
                        </button>
                      </div>
                      {/* Clear and Stop buttons */}
                      <div className="flex items-center space-x-4">
                        <button
                          className={`${
                            isLoading ? "" : "opacity-70 cursor-not-allowed"
                          }`}
                          onClick={handleStop}
                        >
                          <span role="img" aria-label="stop">
                            🟥
                          </span>
                        </button>
                        <button
                          className={`${
                            isStopped ? "" : "opacity-70 cursor-not-allowed"
                          }`}
                          onClick={() => {
                            setIframeContent("");
                            setIsStopped(false);
                          }}
                        >
                          <span role="img" aria-label="clear">
                            🧽
                          </span>
                        </button>
                      </div>
                    </div>
                  </div>
                  <div></div>
                  <div className="flex justify-end space-x-4">
                    <button onClick={handleSave}>
                      <span role="img" aria-label="paper-plane">
                        📩
                      </span>
                    </button>
                    {!isLoading && iframeContent && (
                      <button onClick={handleEdit} className="ml-4">
                        {editingMode ? "💾" : "✏️"}
                      </button>
                    )}
                  </div>
                </div>
                <div className="border bg-white rounded-b-xl border-t-0 h-[calc(100vh-100px)] overflow-auto">
                  <Frame
                    ref={iframeRef}
                    sandbox="allow-same-origin allow-scripts"
                    className="w-full h-full"
                  >
                    {codeViewActive ? (
                      <pre>{iframeContent}</pre>
                    ) : (
                      <div
                        dangerouslySetInnerHTML={{ __html: iframeContent }}
                      />
                    )}
                  </Frame>
                </div>
              </div>
            </div>
          )
        )}
      </div>
      <RateModal key={lastMessageId} show={!!lastMessageId} />
    </>
  );
}


================================================
FILE: app/(aipage)/profile/invoices/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function InvoicesLoading() {
  return (
    <div className="w-ful mx-auto l flex h-full items-center justify-center max-w-screen-xl p-6">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/invoices/page.tsx
================================================
import { fetchInvoices } from "@/utils/auth";
import { moneyFormat, stripePrice } from "@/utils/helpers";
import Button from "@/components/Button";
import { FileText } from "lucide-react";
import { Invoice } from "@/types";

export default async function InvoicesPage() {
  const invoices = await fetchInvoices();

  const hasInvoices = invoices?.length > 0;

  return (
    <section className="w-full px-6 py-6 bg-gray-50 h-full flex-1 flex flex-col">
      <div className="mx-auto w-full sm:max-w-screen-2xl sm:px-2.5 lg:px-20 space-y-4">
        {hasInvoices && (
          <div>
            <h2 className="text-lg leading-6 font-medium text-gray-900">
              Invoices
            </h2>
            <p className="mt-1 text-sm text-gray-500">
              Check the invoices for your purchases.
            </p>
          </div>
        )}
        {!hasInvoices ? (
          <div className="flex flex-1 flex-col items-center justify-center rounded-md border border-gray-200 bg-white py-12">
            <h2 className="z-10 text-xl font-semibold text-gray-700">
              You don't have any invoices yet!
            </h2>
            <img
              alt="No domains yet"
              loading="lazy"
              width={500}
              className="pointer-events-none blur-0"
              src="/no-project.png"
            />
          </div>
        ) : (
          <>
            <div className="hidden md:block">
              <DesktopTable invoices={invoices} />
            </div>
            <div className="block md:hidden">
              <MobileTable invoices={invoices} />
            </div>
          </>
        )}
      </div>
    </section>
  );
}

function DesktopTable({ invoices }: { invoices: Invoice[] }) {
  return (
    <div className="flex flex-col">
      <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
          <div className="overflow-hidden border rounded-lg">
            <table className="min-w-full divide-y divide-gray-200">
              <thead className="bg-gray-50">
                <tr>
                  <th
                    scope="col"
                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                  >
                    Product name
                  </th>
                  <th
                    scope="col"
                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                  >
                    Order date
                  </th>
                  <th
                    scope="col"
                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                  >
                    Status
                  </th>
                  <th
                    scope="col"
                    className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                  >
                    Total amount
                  </th>
                  <th scope="col" className="relative px-6 py-3"></th>
                </tr>
              </thead>
              <tbody>
                {invoices.map((invoice, index) => (
                  <tr
                    key={index}
                    className={index % 2 === 0 ? "bg-white" : "bg-gray-50"}
                  >
                    <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
                      {invoice.lines.data[0]?.price?.nickname ?? "Unknown"}{" "}
                      Credits
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                      {new Date(invoice.created * 1000).toLocaleDateString()}
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                      {invoice.status.toUpperCase()}
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                      {moneyFormat(stripePrice(invoice.total))}
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                      <div className="flex justify-end w-full">
                        <a
                          href={invoice.hosted_invoice_url}
                          className="block w-full sm:w-auto"
                          target="_blank"
                        >
                          <Button
                            disabled={!invoice.hosted_invoice_url}
                            className="w-full sm:w-auto gap-2 [&:not(:hover)]:text-gray-500"
                          >
                            <FileText className="h-4 w-4" />
                            View Invoice
                          </Button>
                        </a>
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
}

function MobileTable({ invoices }: { invoices: Invoice[] }) {
  return (
    <div className="flex gap-2 flex-col">
      {invoices?.map((invoice) => (
        <div
          key={invoice.id}
          className="bg-white border-gray-200 shadow-sm rounded-lg border p-4"
        >
          <div className="flex flex-col gap-2 sm:gap-0 sm:flex-row">
            <dl className="flex-1 grid sm:grid-cols-2 gap-2 md:grid-cols-4">
              <div>
                <dt className="font-medium text-gray-900">Product Name</dt>
                <dd className="mt-1 text-gray-500">
                  {invoice.lines.data[0]?.price?.nickname ?? "Unknown"} Credits
                </dd>
              </div>
              <div>
                <dt className="font-medium text-gray-900">Order date</dt>
                <dd className="mt-1 text-gray-500">
                  <time dateTime={invoice.created.toString()}>
                    {new Date(invoice.created * 1000).toLocaleDateString()}
                  </time>
                </dd>
              </div>
              <div>
                <dt className="font-medium text-gray-900">Order status</dt>
                <dd className="mt-1 font-medium text-gray-500">
                  {invoice.status}
                </dd>
              </div>
              <div>
                <dt className="font-medium text-gray-900">Total amount</dt>
                <dd className="mt-1 font-medium text-gray-500">
                  {moneyFormat(stripePrice(invoice.total))}
                </dd>
              </div>
            </dl>
            <div className="flex gap-1 sm:items-center justify-center">
              <a
                href={invoice.hosted_invoice_url}
                className="block w-full sm:w-auto"
                target="_blank"
              >
                <Button className="w-full sm:w-auto gap-2 [&:not(:hover)]:text-gray-500">
                  <FileText className="h-4 w-4" />
                  View Invoice
                </Button>
              </a>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/layout.tsx
================================================
import { ReactNode } from "react";
import ProfileLayout from "@/components/ProfileLayout";

export default async function ProfileBaseLayout({
  children,
}: {
  children: ReactNode;
}) {
  return <ProfileLayout>{children}</ProfileLayout>;
}


================================================
FILE: app/(aipage)/profile/projects/[id]/domains/layout.tsx
================================================
"use client";
import AddDomainModal from "@/components/AddDomainModal";
import Button from "@/components/Button";
import DomainCard from "@/components/DomainCard";
import useSearchParams from "@/hooks/useSearchParams";
import { ReactNode } from "react";

export default function DomainLayout({ children }: { children: ReactNode }) {
  const { set } = useSearchParams();

  return (
    <>
      <AddDomainModal />
      <div className="bg-gray-50 flex-1">
        <div className="flex h-36 items-center border-b border-gray-200 bg-white">
          <div className="mx-auto w-full max-w-screen-xl px-2.5 lg:px-20">
            <div className="flex items-center justify-between">
              <div className="flex items-center space-x-2">
                <h1 className="text-2xl text-gray-600">Domains</h1>
              </div>
              <Button
                onClick={() => set("domainModal", "true")}
                variant="default"
              >
                Add Domain
              </Button>
            </div>
          </div>
        </div>
        <div className="mx-auto w-full max-w-screen-xl px-2.5 lg:px-20 py-10">
          {children}
        </div>
      </div>
    </>
  );
}


================================================
FILE: app/(aipage)/profile/projects/[id]/domains/page.tsx
================================================
import DomainCard from "@/components/DomainCard";
import { toReversed } from "@/utils/helpers";
import { fetchProjectById } from "@/utils/auth";
import { SetProject } from "@/hooks/useProject";

export default async function ProjectDomains({
  params,
}: {
  params: { id: string };
}) {
  const project = await fetchProjectById(params.id);
  const hasDomains = !!(project && project?.domains.length > 0);

  return (
    <div className="flex flex-col gap-4">
      <SetProject project={project} />
      {hasDomains ? (
        toReversed(project?.domains).map((domain) => (
          <DomainCard domain={domain} key={domain._id} />
        ))
      ) : (
        <div className="flex flex-1 flex-col items-center justify-center rounded-md border border-gray-200 bg-white py-12">
          <h2 className="z-10 text-xl font-semibold text-gray-700">
            No domains yet
          </h2>
          <img
            alt="No links yet"
            loading="lazy"
            width={500}
            className="pointer-events-none blur-0"
            src="/no-project.png"
          />
        </div>
      )}
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/projects/[id]/integrations/page.tsx
================================================
export default function ProjectIntegrations({
  params,
}: {
  params: {
    id: string;
  };
}) {
  return <div></div>;
}


================================================
FILE: app/(aipage)/profile/projects/[id]/layout.tsx
================================================
import { ReactNode } from "react";
import { fetchProjectById } from "@/utils/auth";
import { notFound } from "next/navigation";
import { SetProject } from "@/hooks/useProject";

export default async function ProjectLayout({
  children,
  params,
}: {
  children: ReactNode;
  params: {
    id: string;
  };
}) {
  const project = await fetchProjectById(params.id);
  if (!project || project?.deletedAt) return notFound();

  return (
    <>
      <SetProject project={project} />
      {children}
    </>
  );
}


================================================
FILE: app/(aipage)/profile/projects/[id]/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function ProjectByIdLoading() {
  return (
    <div className="w-full mx-auto flex h-full items-center justify-center max-w-screen-xl p-6">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/projects/[id]/page.tsx
================================================
import { fetchProjectById, fetchProjects } from "@/utils/auth";
import { SetProjects } from "@/hooks/useProjectList";
import { SetProject } from "@/hooks/useProject";
import { FormEvent } from "react";
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetFooter,
  SheetHeader,
  SheetTitle,
} from "@/components/Drawer";
import Button from "@/components/Button";
import ProjectDesign from "@/components/ProjectDesign";

export const revalidate = 0;

export default async function ProjectDetail({
  params,
}: {
  params: { id: string };
}) {
  const projects = await fetchProjects();
  const project = await fetchProjectById(params.id);

  return (
    <div className="mx-auto project-detail-page grid flex-1 w-full max-w-screen-2xl py-4 sm:py-6 px-2.5 lg:px-20">
      <SetProjects projects={projects ?? []} />
      <SetProject project={project ?? null} />
      <div className="h-full flex-1 grid">
        <div className="flex-1">
          {project?.result ? (
            <ProjectDesign project={project} />
          ) : (
            <div className="flex-1 h-full flex items-center justify-center text-gray-400">
              No HTML to display
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

interface PanelProps {
  open: boolean;
  onOpenChange?: (open: boolean) => void;
  selected?: HTMLElement | null;
  onSave: (values: [string, string][]) => void;
}
function Panel(props: PanelProps) {
  const { open, onOpenChange, selected, onSave } = props;
  const includedKeys = [
    "padding",
    "color",
    "width",
    "height",
    "margin",
    "fontSize",
    "display",
    "position",
    "top",
    "left",
    "right",
    "bottom",
    "border",
    "background",
    "transform",
  ];

  function onSubmit(e: FormEvent) {
    e.preventDefault();
    if (!selected) return;
    const formData = new FormData(e.target as HTMLFormElement);
    onSave?.(Array.from(formData.entries()) as [string, string][]);
  }

  return (
    <Sheet open={open} onOpenChange={onOpenChange}>
      <SheetContent className="px-0">
        <form
          onSubmit={onSubmit}
          className="grid gap-0 max-h-full grid-rows-[auto_1fr_60px] px-0 h-full"
        >
          <SheetHeader className="sticky border-b px-6 py-4 top-0 bg-white">
            <SheetTitle>Edit selected section</SheetTitle>
          </SheetHeader>
          <div className="flex flex-col gap-2 px-6 py-4 overflow-y-auto">
            <div>
              <label>Text Content</label>
              <input
                name="textContent"
                className="border-gray-200 rounded w-full"
                type="text"
                defaultValue={selected?.innerText}
              />
            </div>
            {selected &&
              Object.entries(getComputedStyle(selected))
                .filter(
                  ([key]) =>
                    !key.match(/\d/) &&
                    includedKeys.includes(key) &&
                    !key.startsWith("webkit"),
                )
                .map(([key, value]) => (
                  <div key={key}>
                    <label>{key}</label>
                    <input
                      name={key}
                      className="border-gray-200 rounded w-full"
                      type="text"
                      defaultValue={value}
                    />
                  </div>
                ))}
          </div>
          <SheetFooter className="px-6 py-4 border-t flex items-center">
            <SheetClose asChild>
              <Button type="submit">Save changes</Button>
            </SheetClose>
          </SheetFooter>
        </form>
      </SheetContent>
    </Sheet>
  );
}


================================================
FILE: app/(aipage)/profile/projects/[id]/settings/page.tsx
================================================
"use client";

import useProject from "@/hooks/useProject";
import { FormEvent, useEffect, useState } from "react";
import { cn, updateProject } from "@/utils/helpers";
import NavLink from "@/components/NavLink";
import Button from "@/components/Button";
import DeleteProjectConfirmDialog from "@/components/DeleteProjectConfirmDialog";
import LoadingSpinner from "@/components/loadingSpinner";
import { useRouter } from "next/navigation";

export default function ProjectSettingsPage() {
  const { setProject, project } = useProject();
  const [name, setName] = useState(project?.name);
  const [loading, setLoading] = useState(false);
  const { refresh } = useRouter();

  async function onNameFormSubmit(event: FormEvent) {
    event.preventDefault();
    if (!name || name === project?.name) return;
    setLoading(true);

    const { message: projectFromAPI } = await updateProject(
      {
        name,
      },
      project?._id as string,
    );

    setLoading(false);
    setProject(projectFromAPI);
    refresh();
  }

  return (
    <div className="w-full max-w-screen-xl p-6 grid items-start gap-5 md:grid-cols-5">
      <div className="flex gap-1 md:grid">
        <NavLink
          className={cn(
            "rounded-md p-2.5 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200 font-semibold text-black",
            "data-[active]:bg-gray-100 data-[active]:active:bg-gray-200",
          )}
          href={`/profile/projects/${project?._id}/settings`}
        >
          General
        </NavLink>
      </div>
      <section className="grid gap-5 md:col-span-4">
        <form
          className="rounded-lg border border-gray-200 bg-white"
          onSubmit={onNameFormSubmit}
        >
          <div className="relative flex flex-col space-y-6 p-5 sm:p-10">
            <div className="flex flex-col space-y-3">
              <h2 className="text-xl font-medium">Project Name</h2>
              <p className="text-sm text-gray-500">
                This will be your project name on AIPage.dev
              </p>
            </div>
            <input
              maxLength={32}
              type="text"
              className="w-full max-w-md rounded-md border border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
          </div>
          <div className="flex items-center justify-between rounded-b-lg border-t border-gray-200 bg-gray-50 p-3">
            <p className="text-sm text-gray-500">Max 32 characters.</p>
            <div>
              <Button
                disabled={name === project?.name}
                type="submit"
                variant="default"
              >
                {loading && <LoadingSpinner />}
                <p>Save Changes</p>
              </Button>
            </div>
          </div>
        </form>
        <div className="rounded-lg border border-red-600 bg-white">
          <div className="flex flex-col space-y-3 p-5 sm:p-10">
            <h2 className="text-xl font-medium">Delete Project</h2>
            <p className="text-sm text-gray-500">
              Permanently delete your AIPage.dev project
            </p>
          </div>
          <div className="border-b border-red-600" />
          <div className="flex items-center justify-end p-3">
            <div>
              <DeleteProjectConfirmDialog project={project} />
            </div>
          </div>
        </div>
      </section>
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/projects/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function ProjectsLoading() {
  return (
    <div className="w-ful mx-auto l flex h-full items-center justify-center max-w-screen-xl p-6">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/projects/page.tsx
================================================
import { cn } from "@/utils/helpers";
import Link from "next/link";
import Button from "@/components/Button";
import ListProjects from "@/components/ListProjects";
import { fetchProjects } from "@/utils/auth";

export const revalidate = 0;

export default async function ProfileProjects() {
  const projects = await fetchProjects();
  const hasProjects = !!projects && projects.length > 0;

  return (
    <div className="w-full px-6 py-6 bg-gray-50 h-full flex-1 flex flex-col">
      <div
        className={cn(
          "mx-auto w-full sm:max-w-screen-2xl sm:px-2.5 lg:px-20",
          !hasProjects && "flex-1 flex flex-col",
        )}
      >
        <div
          className={cn(
            hasProjects
              ? "grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-5"
              : "flex flex-col flex-1",
          )}
        >
          {!hasProjects ? (
            <div className="flex flex-1 flex-col items-center justify-center rounded-md border border-gray-200 bg-white py-12">
              <h2 className="z-10 text-xl font-semibold text-gray-700">
                You don't have any projects yet!
              </h2>
              <img
                alt="No links yet"
                loading="lazy"
                width={500}
                className="pointer-events-none blur-0"
                src="/no-project.png"
              />
              <Link href="/">
                <Button variant="pill">Create a project</Button>
              </Link>
            </div>
          ) : (
            <ListProjects projects={projects} />
          )}
        </div>
      </div>
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/settings/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function SettingsLoading() {
  return (
    <div className="w-ful mx-auto l flex h-full items-center justify-center max-w-screen-xl p-6">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(aipage)/profile/settings/page.tsx
================================================
"use client";
import { useAuth } from "@/context/AuthContext";
import { useState, FormEvent } from "react";
import LoadingSpinner from "@/components/loadingSpinner";
import Button from "@/components/Button";
import { cn } from "@/utils/helpers";
import DeleteAccountConfirmDialog from "@/components/DeleteAccountConfirmDialog";
import NavLink from "@/components/NavLink";

export default function ProfileSettings() {
  const { user, setUser } = useAuth();
  const [name, setName] = useState(user?.name);
  const [loading, setLoading] = useState(false);

  async function onNameFormSubmit(event: FormEvent) {
    event.preventDefault();
    if (!name || name === user?.name) return;
    setLoading(true);
    const res = await fetch("/api/user", {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name }),
    });
    const { user: userFromAPI, errors } = await res.json();
    setLoading(false);
    if (!errors) {
      setUser(userFromAPI);
    }
  }

  return (
    <div className="w-full max-w-screen-xl p-6 grid items-start gap-5 md:grid-cols-5">
      <div className="flex gap-1 md:grid">
        <NavLink
          className={cn(
            "rounded-md p-2.5 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200 font-semibold text-black",
            "data-[active]:bg-gray-100 data-[active]:active:bg-gray-200",
          )}
          href="/profile/settings"
        >
          General
        </NavLink>
      </div>
      <div className="grid gap-5 md:col-span-4">
        <form
          className="rounded-lg border border-gray-200 bg-white"
          onSubmit={onNameFormSubmit}
        >
          <div className="relative flex flex-col space-y-6 p-5 sm:p-10">
            <div className="flex flex-col space-y-3">
              <h2 className="text-xl font-medium">Your Name</h2>
              <p className="text-sm text-gray-500">
                This will be your display name on AIPage.dev
              </p>
            </div>
            <input
              name="name"
              placeholder="Steve Jobs"
              maxLength={32}
              type="text"
              required
              className="w-full max-w-md rounded-md border border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
          </div>
          <div className="flex items-center justify-between rounded-b-lg border-t border-gray-200 bg-gray-50 p-3">
            <p className="text-sm text-gray-500">Max 32 characters.</p>
            <div>
              <Button
                disabled={name === user?.name}
                type="submit"
                variant="default"
              >
                {loading && <LoadingSpinner />}
                <p>Save Changes</p>
              </Button>
            </div>
          </div>
        </form>
        <div className="rounded-lg border border-red-600 bg-white">
          <div className="flex flex-col space-y-3 p-5 sm:p-10">
            <h2 className="text-xl font-medium">Delete Account</h2>
            <p className="text-sm text-gray-500">
              Permanently delete your AIPage.dev account and all of your data.
              This action cannot be undone - please proceed with caution.
            </p>
          </div>
          <div className="border-b border-red-600" />
          <div className="flex items-center justify-end p-3">
            <div>
              <DeleteAccountConfirmDialog />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}


================================================
FILE: app/(preview)/not-found.tsx
================================================
import Link from "next/link";
import Button from "@/components/Button";

export default async function NotFound() {
  return (
    <section className="bg-white fixed inset-0">
      <div className="container flex items-center min-h-screen px-6 py-12 mx-auto">
        <div>
          <p className="text-2xl font-medium text-blue-500">404 NOT FOUND</p>
          <h1 className="mt-3 text-3xl font-semibold text-gray-800 md:text-4xl">
            We can’t find that page
          </h1>
          <p className="mt-4 text-gray-500">
            Sorry, the page you are looking for doesn't exist or has been moved.
          </p>

          <div className="flex items-center mt-6 gap-x-3">
            <Link href="/profile/projects">
              <Button variant="pill">Go to projects</Button>
            </Link>
          </div>
        </div>
      </div>
    </section>
  );
}


================================================
FILE: app/(preview)/profile/projects/[id]/preview/loading.tsx
================================================
import LoadingSpinner from "@/components/loadingSpinner";

export default function PreviewLoading() {
  return (
    <div className="h-screen w-screen flex items-center justify-center">
      <LoadingSpinner />
    </div>
  );
}


================================================
FILE: app/(preview)/profile/projects/[id]/preview/page.tsx
================================================
import { fetchProjectById } from "@/utils/auth";
import { notFound } from "next/navigation";
import HTMLPreview from "@/components/HTMLPreview";

export default async function projectPreview({
  params,
}: {
  params: { id: string };
}) {
  const project = await fetchProjectById(params.id);
  if (!project || !project.result) return notFound();
  return <HTMLPreview html={project.result} />;
}


================================================
FILE: app/api/chat/route.ts
================================================
import { Configuration, OpenAIApi } from "openai-edge";
import { Ratelimit } from "@upstash/ratelimit";
import redis from "../../../utils/redis";
import { OpenAIStream, StreamingTextResponse } from "ai";
import { headers, cookies } from "next/headers";
import { NextResponse } from "next/server";

/* // REMOVE THIS IF YOU DON'T WANT RATE LIMITING
// START
const ratelimit = redis
  ? new Ratelimit({
      redis: redis,
      limiter: Ratelimit.fixedWindow(5, "1440 m"),
      analytics: true,
    })
  : undefined;

// END
 */
const config = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(config);

export const runtime = "edge";

export async function POST(req: Request) {
  const cookieStore = cookies();
  /*   // REMOVE THIS IF YOU DON'T WANT RATE LIMITING
  // START
  if (ratelimit) {
    const headersList = headers();
    const ipIdentifier = headersList.get("x-real-ip");

    const result = await ratelimit.limit(ipIdentifier ?? "");

    if (!result.success) {
      const fakeStream =
        "Too many requests in 1 day. Please try again in a 24 hours. Thank you. 🙏";
      return new Response(fakeStream, {
        status: 429,
        headers: {
          "X-RateLimit-Limit": result.limit,
          "X-RateLimit-Remaining": result.remaining,
        } as any,
      });
    }
  }
  // END */

  const { messages } = await req.json();

  // Implemented for to test the API

  const sessionToken = cookieStore.get("sessionToken")?.value as string;

  const storeMessage = await fetch(
    "https://c3-na.altogic.com/e:64d52ccfc66bd54b97bdd78a/test",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Session: sessionToken,
      },
      body: JSON.stringify({ content: messages[0].content }),
    },
  );

  const { credits } = await storeMessage.json();

  if (credits === 0) {
    return NextResponse.json({ code: "no-credits", credits });
  }

  const systemPrompt = `You are a talented UI designer who needs help creating a clear and concise HTML UI using Tailwind CSS. The UI should be visually appealing and responsive. Please design a UI component that includes the following elements:

1. A header Section: Include a logo and a navigation menu.
2. A hero Section: Create a captivating headline and a call-to-action button. Use a random image related to the prompt for the background image, generating it with the URL "https://source.unsplash.com/featured/1280x720/?{description}" (replace {description} with a relevant keyword).
3. A feature Section: Showcase three standout feature cards with eye-catching featured icons from the Fontawesome CDN icon library. Apply subtle CSS animations, such as fade-in or slide-in effects using Animate.css, to enhance visual appeal.
4. An individual Feature Sections: Create a separate section for each feature card. Each section should include a captivating title, description, and a call-to-action button. Use a random image related to the prompt for the background image, generating it with the URL "https://source.unsplash.com/featured/1280x720/?{description}" (replace {description} with a relevant keyword). You can float the image to the left or right of the text.
5. A testimonial Section: Display two testimonials with names, roles, and feedback. Apply a CSS animation, like fade-in or slide-in animation using Animate.css, to reveal testimonials when scrolled into view.
6. A blog Section: Include a section that displays recent blog posts with a title, short description, and a "Read More" link.
7. An FAQ Section: Add a section for frequently asked questions and answers.
8. A Team Section: Showcase the team with photos, names, roles, and social media links.
9. A Newsletter Subscription: Add a section for users to subscribe to a newsletter.
10. A Contact Form: Create fields for name, email, and message. Apply appropriate CSS animations or transitions using jQuery for smooth interactivity.
11. A map Section: Include a Google Maps section with a marker showing the location of the business (you may need a Google Maps API key).
12. A footer Section: Add links to social media profiles, utilizing the Fontawesome CDN icon library for social media icons.

Please ensure the HTML code is valid and properly structured, incorporating the necessary CDN links for Tailwind CSS, Fontawesome icons, jQuery, Animate.css, Google Maps API, and any additional CSS or JS files.

Remember to keep the design minimalistic, intuitive, and visually appealing. Your attention to detail is highly appreciated. Once you complete the design, provide the HTML code for the UI component. The code should be valid HTML, formatted for readability, and include the necessary CDN links for Tailwind CSS, icons, and any additional libraries used for data visualization.

  Given the prompt, generate the only HTML code for the UI component. The code should be valid HTML and include the necessary CDN links for Tailwind CSS, Fontawesome icons, and any additional CSS and JavaScript files.

  Start with <!DOCTYPE html> and end with </html>. The code should be formatted for readability.`;

  const combinedMessages = [
    ...messages,
    { role: "system", content: systemPrompt },
  ];

  let response;
  let stream;

  response = await openai.createChatCompletion({
    model: "gpt-3.5-turbo-16k",
    messages: combinedMessages.map((message: any) => ({
      role: message.role,
      content: message.content,
    })),
    stream: true,
  });

  stream = OpenAIStream(response);
  // Continue generating the response if incomplete

  // If rate limited, return a fake response
  return new StreamingTextResponse(stream);
}


================================================
FILE: app/api/create-checkout-session/route.ts
================================================
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import altogic from "@/utils/altogic";

export async function POST(req: Request) {
  const { priceId } = await req.json();
  const cookieStore = cookies();
  const session = cookieStore.get("sessionToken");

  if (!session) {
    return NextResponse.json(
      { error: "You must be logged in to purchase this product" },
      { status: 401 },
    );
  }

  // @ts-ignore
  altogic.auth.setSession({
    token: session.value,
  });

  const path = process.env.NEXT_PUBLIC_CREATE_SESSION_PATH as string;
  const { errors, data } = await altogic.endpoint.post(path, {
    priceId,
  });

  if (errors) {
    return NextResponse.json({ errors }, { status: errors.status });
  }

  return NextResponse.json({ url: data.url });
}


================================================
FILE: app/api/domain/check-domain/route.ts
================================================
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { domain } = await req.json();

  const [configResponse, domainResponse] = await Promise.all([
    fetch(`https://api.vercel.com/v6/domains/${domain}/config`, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
        "Content-Type": "application/json",
      },
    }),
    fetch(
      `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${domain}`,
      {
        method: "GET",
        headers: {
          Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
          "Content-Type": "application/json",
        },
      },
    ),
  ]);

  const configJson = await configResponse.json();
  const domainJson = await domainResponse.json();
  if (domainResponse.status !== 200) {
    return NextResponse.json(domainJson, { status: domainResponse.status });
  }

  /**
   * If domain is not verified, we try to verify now
   */
  let verificationResponse = null;
  if (!domainJson.verified) {
    const verificationRes = await fetch(
      `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${domain}/verify`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
          "Content-Type": "application/json",
        },
      },
    );
    verificationResponse = await verificationRes.json();
  }

  if (verificationResponse && verificationResponse.verified) {
    /**
     * Domain was just verified
     */
    return NextResponse.json(
      {
        configured: !configJson.misconfigured,
        ...verificationResponse,
      },
      { status: 200 },
    );
  }

  return NextResponse.json(
    {
      configured: !configJson.misconfigured,
      ...domainJson,
      ...(verificationResponse ? { verificationResponse } : {}),
    },
    { status: 200 },
  );
}


================================================
FILE: app/api/domain/remove-domain/route.ts
================================================
import { NextResponse } from "next/server";
import altogic from "@/utils/altogic";
import { getSessionCookie } from "@/utils/auth";

export async function POST(req: Request) {
  const { domain } = await req.json();

  console.log({ domain });

  const response = await fetch(
    `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${domain}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
      },
      method: "DELETE",
    },
  );

  const json = await response.json();

  console.log(JSON.stringify(json, null, 2));
  console.log("-----");

  // @ts-ignore
  altogic.auth.setSession({
    token: getSessionCookie() as string,
  });

  const { errors } = await altogic.db
    .model("messages.domains")
    .filter(`domain == '${domain}'`)
    .delete();

  console.log(JSON.stringify(errors, null, 2));

  if (errors) {
    return NextResponse.json(errors, { status: errors.status });
  }

  NextResponse.json(json);
}


================================================
FILE: app/api/domain/verify-domain/route.ts
================================================
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { domain } = await req.json();

  const response = await fetch(
    `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${domain}/verify`,
    {
      headers: {
        Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
        "Content-Type": "application/json",
      },
      method: "POST",
    },
  );

  const data = await response.json();
  return NextResponse.json(data, {
    status: response.status,
  });
}


================================================
FILE: app/api/logout/route.ts
================================================
import { NextResponse } from "next/server";
import { logout } from "@/utils/auth";

export async function GET(req: Request) {
  return logout(req, NextResponse);
}


================================================
FILE: app/api/message/route.ts
================================================
import altogic from "@/utils/altogic";
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";

export async function PUT(req: Request) {
  const cookieStore = cookies();
  const token = cookieStore.get("sessionToken");

  if (!token) {
    return NextResponse.json(
      {
        message: "You must be logged in to update your project.",
      },
      { status: 401 },
    );
  }

  const body = await req.json();
  // @ts-ignore
  altogic.auth.setSession({
    token: token.value,
  });

  const { data, errors } = await altogic.endpoint.put("/message-content", body);

  if (errors) return NextResponse.json({ errors }, { status: 500 });

  revalidatePath("/profile/projects");
  return NextResponse.json({ message: data }, { status: 200 });
}


================================================
FILE: app/api/og/route.tsx
================================================
import { ImageResponse } from "next/server";
import { useState } from "react";
// App router includes @vercel/og.
// No need to install it.

export const runtime = "edge";

export async function GET(request: Request) {
  const tweetIntents = [
    "Just used AI to craft an EPIC landing page in minutes with AIpage.dev ! 🤖 This is the future of web design! Check it out 👉 @aipagedev",
    "Creating a stunning webpage has never been easier thanks to AIpage.dev! 🚀 Give it a try 👉 @aipagedev",
    "Web design will never be the same after you try AIpage.dev! 🛠️ A whole new level of creativity unleashed! Check it out 👉 @aipagedev",
    "Revolutionize your web design process with AIpage.dev. The future is here! 👉 @aipagedev",
    "I just built an amazing webpage with AIpage.dev in minutes! 🌟 You have to try this 👉 @aipagedev",
    "AIpage.dev is a game-changer for web design! Say hello to efficiency 👋 @aipagedev",
    "Why spend hours on web design when AIpage.dev can do it in minutes? 🕒 Check it out! 👉 @aipagedev",
    "Impressed by the power of AI in web design with AIpage.dev! This is incredible 👀 @aipagedev",
    "I used AIpage.dev and it completely transformed how I approach web design. You need to try this! 🎉 @aipagedev",
    "Just when I thought web design couldn’t get any easier, I found AIpage.dev! 🎊 Try it now 👉 @aipagedev",
    "Unleashing my inner designer with the help of AIpage.dev. This is next level! 🚀 Check it out 👉 @aipagedev",
    "With AIpage.dev, I can focus on creativity while AI handles the coding. It’s amazing! 💥 @aipagedev",
  ];

  // Function to generate a random index for selecting a tweet text
  const getRandomIndex = () => {
    return Math.floor(Math.random() * tweetIntents.length);
  };

  // Function to generate a random tweet text
  const getRandomTweet = () => {
    return tweetIntents[getRandomIndex()];
  };

  const text = getRandomTweet();

  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          height: "100%",
          width: "100%",
          alignItems: "center",
          justifyContent: "center",
          flexDirection: "column",
          backgroundImage: "linear-gradient(to bottom, #7149b6, #715dd3)",
          fontSize: 40,
          letterSpacing: -2,
          fontWeight: 700,
          textAlign: "center",
        }}
      >
        <div
          style={{
            height: "100%",
            width: "100%",
            display: "flex",
            textAlign: "center",
            alignItems: "center",
            justifyContent: "center",
            flexDirection: "column",
            flexWrap: "nowrap",
            backgroundColor: "transparent",
            backgroundImage:
              "radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)",
            backgroundSize: "100px 100px",
          }}
        >
          <div
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="200"
              height="150"
              viewBox="0 0 200 150"
              style={{ margin: "0 75px" }}
            >
              <rect
                x="10"
                y="10"
                width="180"
                height="130"
                rx="10"
                ry="10"
                fill="#F0F0F0"
                stroke="#CCCCCC"
              />

              <rect
                x="10"
                y="10"
                width="180"
                height="30"
                rx="10"
                ry="10"
                fill="#333333"
              />

              <circle cx="25" cy="25" r="4" fill="#FF605C" />
              <circle cx="40" cy="25" r="4" fill="#FFBD44" />
              <circle cx="55" cy="25" r="4" fill="#28CA41" />

              <rect
                x="20"
                y="50"
                width="160"
                height="30"
                rx="10"
                ry="10"
                fill="#FFFFFF"
              />
              <rect
                x="25"
                y="58"
                width="120"
                rx="5"
                ry="5"
                height="14"
                fill="#F0F0F0"
              />

              <rect
                x="20"
                y="90"
                width="50"
                rx="10"
                ry="10"
                height="40"
                fill="#ddd"
              />
              <rect
                x="75"
                y="90"
                width="50"
                rx="10"
                ry="10"
                height="40"
                fill="#ddd"
              />
              <rect
                x="130"
                y="90"
                width="50"
                rx="5"
                ry="10"
                height="40"
                fill="#ddd"
              />

              <circle cx="165" cy="64" r="8" fill="#F0F0F0" />
            </svg>
          </div>

          <div
            style={{
              color: "white",
              width: "70%",
              paddingTop: "24px",
              textAlign: "center",
              WebkitBackgroundClip: "text",
            }}
          >
            {text}
          </div>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}


================================================
FILE: app/api/project/[id]/domain/route.ts
================================================
import { NextResponse } from "next/server";
import altogic from "@/utils/altogic";
import { getSessionCookie } from "@/utils/auth";

export async function POST(
  req: Request,
  { params }: { params: { id: string } },
) {
  const { domain, isPrimary } = await req.json();

  // @ts-ignore
  altogic.auth.setSession({
    token: getSessionCookie() as string,
  });

  try {
    const { data, errors } = await altogic.endpoint.post(
      "/add-domain",
      {
        domain,
        isPrimary,
        projectId: params.id,
      },
      undefined,
      {
        Session: getSessionCookie(),
      },
    );

    if (errors) {
      return NextResponse.json({ errors }, { status: errors.status });
    }

    const response = await fetch(
      `https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains`,
      {
        body: JSON.stringify({ name: domain }),
        headers: {
          Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
          "Content-Type": "application/json",
        },
        method: "POST",
      },
    );

    const domainData = await response.json();

    if (domainData.error?.code == "forbidden") {
      return NextResponse.json(
        {
          error: domainData.error,
        },
        { status: 403 },
      );
    } else if (domainData.error?.code == "domain_taken") {
      return NextResponse.json(
        {
          error: domainData.error,
        },
        { status: 409 },
      );
    }

    return NextResponse.json({ data }, { status: 200 });
  } catch (error) {
    return NextResponse.json({}, { status: 500 });
  }
}


================================================
FILE: app/api/project/[id]/route.ts
================================================
import { deleteProject } from "@/utils/auth";
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";

export async function DELETE(
  req: Request,
  { params }: { params: { id: string } },
) {
  try {
    const { errors } = await deleteProject(params.id);

    if (errors) {
      return NextResponse.json({ errors }, { status: 500 });
    }

    revalidatePath("/profile/projects");
    return NextResponse.json({ status: "ok" }, { status: 200 });
  } catch (error) {
    return NextResponse.json({}, { status: 500 });
  }
}


================================================
FILE: app/api/project/route.ts
================================================
import { deleteProject, updateProjectName } from "@/utils/auth";
import { NextResponse } from "next/server";

export async function PATCH(req: Request) {
  const { name, id } = await req.json();

  try {
    const { project, errors } = await updateProjectName(id, name);

    if (errors) {
      return NextResponse.json({ errors }, { status: 500 });
    }

    return NextResponse.json({ project }, { status: 200 });
  } catch (error) {
    return NextResponse.json({}, { status: 500 });
  }
}


================================================
FILE: app/api/user/route.ts
================================================
import { deleteUser, logout, updateUser } from "@/utils/auth";
import { NextResponse } from "next/server";

export async function PATCH(req: Request) {
  const { data, errors } = await updateUser(await req.json());

  if (errors) {
    return NextResponse.json({ errors }, { status: 500 });
  }

  return NextResponse.json({ user: data }, { status: 200 });
}

export async function DELETE(req: Request) {
  const { errors } = await deleteUser();

  if (errors) {
    return NextResponse.json({ errors }, { status: 500 });
  }

  return logout(req, NextResponse);
}


================================================
FILE: app/auth-redirect/route.ts
================================================
import altogic from "@/utils/altogic";
import { NextResponse } from "next/server";

export async function GET(req: Request) {
  const url = new URL(req.url);
  const accessToken = url.searchParams.get("access_token") as string;
  const status = url.searchParams.get("status");
  const isOk = status === "200";

  const destinationUrl = new URL("/", new URL(req.url).origin);
  const response = NextResponse.redirect(destinationUrl, { status: 302 });

  if (!isOk) return response;

  const { session, errors } = await altogic.auth.getAuthGrant(accessToken);

  if (errors) {
    // TODO: handle errors;
  }

  if (session) {
    response.cookies.set("sessionToken", session.token, {
      path: "/",
      httpOnly: true,
      maxAge: 60 * 60 * 24 * 7, // 1 week
    });
  }

  return response;
}


================================================
FILE: app/layout.tsx
================================================
import AuthModal from "@/components/AuthModal";
import PricesModal from "@/components/PricesModal";
import { AuthProvider } from "@/context/AuthContext";
import { Project } from "@/types";
import { fetchAuthUser, fetchProducts, getProjectByDomain } from "@/utils/auth";
import { isAipage } from "@/utils/helpers";
import { Metadata } from "next";
import { Inter } from "next/font/google";
import { headers } from "next/headers";
import { ReactNode } from "react";
import "../styles/globals.css";
const inter = Inter({ subsets: ["latin"] });

export async function generateMetadata(): Promise<Metadata> {
  const isAipageDomain = isAipage(headers().get("Host") as string);
  if (!isAipageDomain) {
    const project = await getProjectByDomain(headers().get("Host") as string);
    if (project) return {};
  }

  return {
    title: "AIPage.dev - An AI-Powered Landing Page Generator | by @zinedkaloc",
    description:
      "AI-Powered Landing Page Generator. Experience the Open Source Project that Empowers You to Build Stunning Landing Pages Instantly",
    openGraph: {
      title:
        "AIPage.dev - An AI-Powered Landing Page Generator | by @zinedkaloc",
      description:
        "AI-Powered Landing Page Generator. Experience the Open Source Project that Empowers You to Build Stunning Landing Pages Instantly",
      type: "website",
      url: "https://aipage.dev",
      images: `${process.env.NEXT_PUBLIC_DOMAIN}/api/og?text=${new Date()
        .getTime()
        .toString()}`,
    },
  };
}

export default async function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  const isAipageDomain = isAipage(headers().get("Host") as string);
  const user = await fetchAuthUser();
  const products = await fetchProducts();
  let project: Project | null = null;

  if (!isAipageDomain) {
    project = await getProjectByDomain(headers().get("Host") as string);
  }

  if (!isAipageDomain && project?.result) {
    return require("html-react-parser")(project?.result);
  }

  return (
    <AuthProvider user={user ?? null}>
      <html lang="en">
        <body className={inter.className}>
          {children}
          <AuthModal />
          <PricesModal products={products} />
        </body>
      </html>
    </AuthProvider>
  );
}


================================================
FILE: app/not-found.tsx
================================================
import Link from "next/link";
import Button from "@/components/Button";

export default async function NotFound() {
  return (
    <section className="bg-white fixed inset-0">
      <div className="container flex items-center min-h-screen px-6 py-12 mx-auto">
        <div>
          <p className="text-2xl font-medium text-blue-500">404 NOT FOUND</p>
          <h1 className="mt-3 text-3xl font-semibold text-gray-800 md:text-4xl">
            We can’t find that page
          </h1>
          <p className="mt-4 text-gray-500">
            Sorry, the page you are looking for doesn't exist or has been moved.
          </p>

          <div className="flex items-center mt-6 gap-x-3">
            <Link href="/">
              <Button variant="pill">Take me home</Button>
            </Link>
          </div>
        </div>
      </div>
    </section>
  );
}


================================================
FILE: components/AddDomainModal.tsx
================================================
"use client";
import useSearchParams from "@/hooks/useSearchParams";
import Modal from "@/components/Modal";
import { FormEvent, useState } from "react";
import Button from "@/components/Button";
import LoadingSpinner from "@/components/loadingSpinner";
import Image from "next/image";
import Switch from "@/components/Switch";
import { useParams, useRouter } from "next/navigation";
import useProject from "@/hooks/useProject";
import { APIError } from "altogic";

export default function AddDomainModal() {
  const { deleteByKey, has } = useSearchParams();
  const [loading, setLoading] = useState(false);
  const [hasError, setHasError] = useState(false);
  const [isPrimary, setIsPrimary] = useState(false);
  const [domain, setDomain] = useState("");
  const [error, setError] = useState<APIError | null>(null);
  const { id } = useParams();
  const { addDomain } = useProject();
  const { refresh } = useRouter();

  function close() {
    setIsPrimary(false);
    setDomain("");
    deleteByKey("domainModal");
    setError(null);
    setHasError(false);
  }

  async function onSubmit(e: FormEvent) {
    e.preventDefault();

    const data = {
      domain,
      isPrimary,
    };

    setLoading(true);
    setHasError(false);
    try {
      const res = await fetch(`/api/project/${id}/domain`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      });

      const { errors, data: domainFromAPI } = await res.json();

      if (errors) {
        setHasError(true);
        setError(errors);
      } else {
        close();
        addDomain(domainFromAPI);
        refresh();
      }
    } catch {
      setHasError(true);
    } finally {
      setLoading(false);
    }
  }

  return (
    <Modal
      close={close}
      isOpen={has("domainModal")}
      className="p-0 sm:w-[400px] w-[96%]"
    >
      <form onSubmit={onSubmit}>
        <div className="flex flex-col items-center justify-center space-y-3 border-gray-200 bg-white px-4 py-6 pt-8 text-center sm:px-10">
          <Image
            src="/logoa.png"
            alt="AIPage.dev logo"
            width={150}
            height={150}
            draggable={false}
            className="mx-auto h-20 w-20"
          />
          <h3 className="text-lg font-semibold">Add Domain</h3>
        </div>
        <div className="bg-gray-50 p-4 space-y-4 border-t">
          <div className="space-y-2">
            <label
              htmlFor="domain"
              className="block text-sm font-medium text-gray-700"
            >
              Domain
            </label>
            <div className="relative rounded-md shadow-sm">
              <input
                type="text"
                name="domain"
                value={domain}
                onChange={(e) => setDomain(e.target.value)}
                autoComplete="off"
                pattern="[a-z0-9]+\.[a-zA-Z]{2,}"
                className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 block w-full rounded-md focus:outline-none sm:text-sm"
                placeholder="aipage.dev"
                required
              />
            </div>
            <div className="text-[11px] text-gray-400">
              Please enter a valid domain name (e.g. aipage.dev)
            </div>
          </div>
          {/*
          <div className="flex items-center justify-between bg-gray-50 my-4">
            <p className="text-sm font-medium text-gray-900">Primary Domain</p>
            <Switch fn={setIsPrimary} checked={isPrimary} />
          </div>
          */}
          {hasError && (
            <div className="py-2">
              {error && error.items.length > 0 ? (
                error?.items.map((item) => (
                  <h2 className="text-red-500 text-sm">{item.message}</h2>
                ))
              ) : (
                <h2 className="text-red-500 text-sm">
                  There was an error adding your domain. Please try again later.
                </h2>
              )}
            </div>
          )}
          <div className="flex justify-end gap-2">
            <Button type="button" variant="light" onClick={close}>
              Cancel
            </Button>
            <Button
              className="gap-2"
              type="submit"
              disabled={loading}
              variant="default"
            >
              {loading && <LoadingSpinner className="h-4 w-4" />}
              Add Domain
            </Button>
          </div>
        </div>
      </form>
    </Modal>
  );
}


================================================
FILE: components/AlertCircleFill.tsx
================================================
export default function AlertCircleFill({ className }: { className: string }) {
  return (
    <svg
      fill="none"
      shapeRendering="geometricPrecision"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="1.5"
      viewBox="0 0 24 24"
      width="24"
      height="24"
      className={className}
    >
      <circle cx="12" cy="12" r="10" fill="currentColor" />
      <path d="M12 8v4" stroke="white" />
      <path d="M12 16h.01" stroke="white" />
    </svg>
  );
}


================================================
FILE: components/AlertDialog.tsx
================================================
"use client";

import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";

import { cn } from "@/utils/helpers";

const AlertDialog = AlertDialogPrimitive.Root;

const AlertDialogTrigger = AlertDialogPrimitive.Trigger;

const AlertDialogPortal = ({
  className,
  ...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
  <AlertDialogPrimitive.Portal className={cn(className)} {...props} />
);
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;

const AlertDialogOverlay = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
  <AlertDialogPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-white/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className,
    )}
    {...props}
    ref={ref}
  />
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
        className,
      )}
      {...props}
    />
  </AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

const AlertDialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className,
    )}
    {...props}
  />
);
AlertDialogHeader.displayName = "AlertDialogHeader";

const AlertDialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className,
    )}
    {...props}
  />
);
AlertDialogFooter.displayName = "AlertDialogFooter";

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
));
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName;

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action ref={ref} className={cn(className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn("mt-2 sm:mt-0", className)}
    {...props}
  />
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;

export {
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
};


================================================
FILE: components/AuthModal.tsx
================================================
"use client";
import { useState } from "react";
import Link from "next/link";
import altogic from "@/utils/altogic";
import useSearchParams from "@/hooks/useSearchParams";
import Image from "next/image";
import Modal from "@/components/Modal";
import LoadingSpinner from "@/components/loadingSpinner";
import Button, { ButtonVariant } from "@/components/Button";

export default function AuthModal() {
  const { deleteByKey, has } = useSearchParams();

  const [selected, setSelected] = useState<number>();

  const loginMethods = [
    {
      name: "Google",
      variant: "default",
      icon: GoogleIcon,
      handler: () => altogic.auth.signInWithProvider("google"),
    },
    {
      name: "Github",
      variant: "light",
      icon: GithubIcon,
      handler: () => altogic.auth.signInWithProvider("github"),
    },
  ];

  function close() {
    deleteByKey("authModal");
  }

  return (
    <Modal close={close} isOpen={has("authModal")} className="p-0">
      <div className="flex flex-col items-center justify-center space-y-3 border-b border-gray-200 bg-white px-4 py-6 pt-8 text-center sm:px-10">
        <Link href="/">
          <Image
            src="/logoa.png"
            alt="AIPage.dev logo"
            width={150}
            height={150}
            draggable={false}
            className="mx-auto h-24 w-24"
          />
        </Link>
        <h3 className="text-xl font-semibold">Sign in to AIPage</h3>
        <p className="text-sm text-gray-500">
          Powered by AI, Perfected for You
        </p>
      </div>
      <div className="flex flex-col space-y-3 bg-gray-50 px-4 py-8 sm:px-10">
        {loginMethods.map((method, index) => (
          <Button
            key={method.name}
            variant={method.variant as ButtonVariant}
            // this is a hack to prevent the button from being clicked twice
            disabled={selected !== undefined}
            onClick={() => {
              setSelected(index);
              method.handler();
            }}
            className="h-10"
          >
            {index === selected ? (
              <LoadingSpinner className="h-4 w-4" />
            ) : (
              <method.icon />
            )}
            <p>Continue with {method.name}</p>
          </Button>
        ))}
        <p className="text-center text-sm text-gray-500">
          Enterprise login?{" "}
          <a
            target="_blank"
            className="font-semibold text-gray-500 transition-colors hover:text-black"
            href="mailto:deniz@altogic.com"
          >
            Contact
          </a>
        </p>
      </div>
    </Modal>
  );
}

const GoogleIcon = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 488 512"
    fill="currentColor"
    className="h-4 w-4"
  >
    <path d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"></path>
  </svg>
);

const GithubIcon = () => (
  <svg aria-label="github" viewBox="0 0 14 14" className="h-4 w-4">
    <path
      d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
      fill="currentColor"
      fillRule="nonzero"
    />
  </svg>
);


================================================
FILE: components/Badge.tsx
================================================
import { cn } from "@/utils/helpers";

export type BadgeVariant = "yellow" | "gray" | "red" | "black" | "green";

export default function Badge({
  text,
  variant,
  className,
}: {
  text: string;
  variant?: BadgeVariant;
  className?: string;
}) {
  return (
    <span
      className={cn(
        "rounded-full border px-2 py-px text-xs font-medium capitalize tabular-nums",
        {
          "border-gray-400 bg-gray-400 text-white": variant === "gray",
          "border-red-500 bg-red-500 text-white": variant === "red",
          "border-green-500 bg-green-500 text-white": variant === "green",
          "border-black bg-black text-white": variant === "black",
          "border-yellow-400 bg-yellow-400 text-stone-700":
            variant === "yellow",
        },
        className,
      )}
    >
      {text}
    </span>
  );
}


================================================
FILE: components/BrowserWindow.tsx
================================================
import { ReactNode } from "react";
import { cn } from "@/utils/helpers";

interface BrowserWindowProps {
  children: ReactNode;
  className?: string;
}
export default function BrowserWindow({
  children,
  className,
}: BrowserWindowProps) {
  return (
    <div
      className={cn(
        "flex flex-col items-center w-full overflow-hidden browser-window",
        className,
      )}
    >
      <div className="border flex-1 flex flex-col rounded-xl w-full overflow-hidden">
        <div className="bg-white flex items-center justify-between px-3 py-4 border-b">
          <div className="flex items-center space-x-2">
            <div className="w-3 h-3 bg-red-500 rounded-full" />
            <div className="w-3 h-3 bg-yellow-500 rounded-full" />
            <div className="w-3 h-3 bg-green-500 rounded-full" />
          </div>
        </div>
        <div className="flex-1 overflow-hidden">{children}</div>
      </div>
    </div>
  );
}


================================================
FILE: components/Button.tsx
================================================
import { cn } from "@/utils/helpers";
import { ComponentPropsWithoutRef } from "react";
import * as React from "react";
export type ButtonVariant = "default" | "light" | "pill" | "danger";

interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
  variant?: ButtonVariant;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, children, variant, ...props }, ref) => (
    <button
      ref={ref}
      className={cn(
        "flex items-center justify-center space-x-2 rounded-md border text-sm transition-all focus:outline-none px-4 py-1.5 ",
        {
          "border-black bg-black text-white active:bg-white enabled:hover:bg-white active:text-black enabled:hover:text-black disabled:opacity-50 disabled:cursor-not-allowed":
            variant === "default",
          "border-gray-200 bg-white text-gray-500 active:border-black enabled:hover:border-black active:text-black enabled:hover:text-black disabled:opacity-50 disabled:cursor-not-allowed":
            variant === "light",
          "rounded-full border border-black bg-black text-sm text-white transition-all enabled:hover:bg-white enabled:hover:text-black !disabled:text-black !disabled:bg-white":
            variant === "pill",
          "flex items-center justify-center space-x-2 rounded-md border px-4 text-sm transition-all focus:outline-none border-red-500 bg-red-500 text-white enabled:hover:bg-white enabled:hover:text-red-500 disabled:opacity-50 disabled:cursor-not-allowed":
            variant === "danger",
        },
        className,
      )}
      {...props}
    >
      {children}
    </button>
  ),
);

export default Button;


================================================
FILE: components/Chart.tsx
================================================
export default function Chart({ className }: { className: string }) {
  return (
    <svg
      fill="none"
      shapeRendering="geometricPrecision"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="1.5"
      viewBox="0 0 24 24"
      width="14"
      height="14"
      className={className}
    >
      <path d="M12 20V10" />
      <path d="M18 20V4" />
      <path d="M6 20v-4" />
    </svg>
  );
}


================================================
FILE: components/CheckCircleFill.tsx
================================================
export default function CheckCircleFill({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      width="24"
      height="24"
      fill="none"
      shapeRendering="geometricPrecision"
    >
      <path
        d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2Z"
        fill="currentColor"
      />
      <path d="M8 11.8571L10.5 14.3572L15.8572 9" stroke="white" />
    </svg>
  );
}


================================================
FILE: components/ConfiguredSection.tsx
================================================
import { useState } from "react";
import { DomainInfo } from "@/types";

interface ConfiguredSectionProps {
  domainInfo?: DomainInfo | null;
}
const ConfiguredSection = ({ domainInfo }: ConfiguredSectionProps) => {
  const [recordType, setRecordType] = useState("A");

  if (!domainInfo?.verified) {
    const txtVerification = domainInfo?.verification?.find(
      (x: any) => x.type === "TXT",
    );
    return (
      <>
        <div className="flex items-center space-x-3 my-3 px-2 sm:px-10">
          <svg
            viewBox="0 0 24 24"
            width="24"
            height="24"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
            shapeRendering="geometricPrecision"
          >
            <circle cx="12" cy="12" r="10" fill="#EAB308" />
            <path d="M12 8v4" stroke="white" />
            <path d="M12 16h.01" stroke="white" />
          </svg>
          <p className="text-yellow-600 font-medium text-sm">
            Domain is pending verification
          </p>
        </div>

        <div className="w-full border-t border-gray-100 mt-5 mb-8" />
        <div className="px-2 sm:px-10">
          <div className="flex justify-start space-x-4">
            <div
              onClick={() => setRecordType("CNAME")}
              className={`${
                recordType == "CNAME"
                  ? "text-black border-black"
                  : "text-gray-400 border-white"
              } text-sm border-b-2 pb-1 transition-all ease duration-150`}
            >
              Verify Domain Ownership
            </div>
          </div>
          <div className="my-3 text-left">
            <p className="my-5 text-sm">
              Please set the following TXT record on {domainInfo?.apexName} to
              prove ownership of {domainInfo?.name}:
            </p>
            <div className="flex justify-start items-start space-x-10 bg-gray-50 p-2 rounded-md">
              <div>
                <p className="text-sm font-bold">Type</p>
                <p className="text-sm font-mono mt-2">
                  {txtVerification?.type}
                </p>
              </div>
              <div>
                <p className="text-sm font-bold">Name</p>
                <p className="text-sm font-mono mt-2">
                  {txtVerification?.domain &&
                    domainInfo?.apexName &&
                    txtVerification?.domain?.slice(
                      0,
                      txtVerification?.domain?.length -
                        domainInfo?.apexName?.length -
                        1,
                    )}
                </p>
              </div>
              <div>
                <p className="text-sm font-bold">Value</p>
                <p className="text-sm font-mono mt-2">
                  <span className="text-ellipsis">
                    {txtVerification?.value}
                  </span>
                </p>
              </div>
            </div>
          </div>
        </div>
      </>
    );
  }

  return (
    <>
      <div className="flex items-center space-x-3 my-3 px-2 sm:px-10">
        <svg
          viewBox="0 0 24 24"
          width="24"
          height="24"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          shapeRendering="geometricPrecision"
        >
          <circle
            cx="12"
            cy="12"
            r="10"
            fill={domainInfo.configured ? "#1976d2" : "#d32f2f"}
          />
          {domainInfo.configured ? (
            <>
              <path
                d="M8 11.8571L10.5 14.3572L15.8572 9"
                fill="none"
                stroke="white"
              />
            </>
          ) : (
            <>
              <path d="M15 9l-6 6" stroke="white" />
              <path d="M9 9l6 6" stroke="white" />
            </>
          )}
        </svg>
        <p
          className={`${
            domainInfo.configured
              ? "text-black font-normal"
              : "text-red-700 font-medium"
          } text-sm`}
        >
          {domainInfo.configured ? "Valid" : "Invalid"} Configuration
        </p>
      </div>

      {!domainInfo.configured && (
        <>
          <div className="w-full border-t border-gray-100 mt-5 mb-8" />
          <div className="px-2 sm:px-10">
            <div className="flex justify-start space-x-4">
              <button
                onClick={() => setRecordType("A")}
                className={`${
                  recordType == "A"
                    ? "text-black border-black"
                    : "text-gray-400 border-white"
                } text-sm border-b-2 pb-1 transition-all ease duration-150`}
              >
                A Record (Recommended)
              </button>
              <button
                onClick={() => setRecordType("CNAME")}
                className={`${
                  recordType == "CNAME"
                    ? "text-black border-black"
                    : "text-gray-400 border-white"
                } text-sm border-b-2 pb-1 transition-all ease duration-150`}
              >
                CNAME Record (subdomains)
              </button>
            </div>
            <div className="my-3 text-left">
              <p className="my-5 text-sm">
                Set the following record on your DNS provider to continue:
              </p>
              <div className="flex justify-start items-center space-x-10 bg-gray-50 p-2 rounded-md">
                <div>
                  <p className="text-sm font-bold">Type</p>
                  <p className="text-sm font-mono mt-2">{recordType}</p>
                </div>
                <div>
                  <p className="text-sm font-bold">Name</p>
                  <p className="text-sm font-mono mt-2">
                    {recordType == "CNAME" ? "www" : "@"}
                  </p>
                </div>
                <div>
                  <p className="text-sm font-bold">Value</p>
                  <p className="text-sm font-mono mt-2">
                    {recordType == "CNAME" ? `cname.aipage.dev` : `76.76.21.21`}
                  </p>
                </div>
              </div>
            </div>
          </div>
        </>
      )}
    </>
  );
};

export default ConfiguredSection;


================================================
FILE: components/ConfirmDialog.tsx
================================================
"use client";

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogTrigger,
} from "@/components/AlertDialog";
import Button from "@/components/Button";
import { XIcon } from "lucide-react";
import { useAuth } from "@/context/AuthContext";
import { ReactNode, useState } from "react";

interface ConfirmDialogProps {
  text: string;
  trigger: ReactNode;
  onConfirm: () => void;
  children?: ReactNode;
}

export default function ConfirmDialog({
  trigger,
  text,
  onConfirm,
  children,
}: ConfirmDialogProps) {
  const [confirmText, setConfirmText] = useState("");

  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
      <AlertDialogContent className="overflow-hidden gap-0 w-[95%] max-w-[450px] p-0 border border-gray-100 rounded-2xl shadow-xl">
        <AlertDialogCancel className="absolute top-4 z-50 right-4 text-gray-500 hover:text-black transition-colors focus:outline-none">
          <XIcon />
        </AlertDialogCancel>
        {children && (
          <div className="flex flex-col items-center justify-center relative space-y-3 border-b border-gray-200 px-4 py-4 pt-8 sm:px-8">
            {children}
          </div>
        )}
        <div className="flex flex-col space-y-6 bg-gray-50 px-4 py-8 text-left sm:px-8">
          <div>
            <label
              htmlFor="verification"
              className="block text-sm text-gray-700"
            >
              To verify, type{" "}
              <span className="font-semibold text-black">{text}</span> below
            </label>
            <div className="relative mt-1 rounded-md shadow-sm">
              <input
                type="text"
                name="verification"
                id="verification"
                pattern="confirm delete account"
                required
                autoFocus={false}
                autoComplete="off"
                value={confirmText}
                onChange={(e) => setConfirmText(e.target.value)}
                className="block w-full rounded-md border-gray-300 pr-10 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
              />
            </div>
          </div>

          <AlertDialogAction asChild>
            <Button
              disabled={confirmText !== text}
              variant="danger"
              onClick={() => {
                setConfirmText("");
                onConfirm();
              }}
            >
              {text.toUpperCase()}
            </Button>
          </AlertDialogAction>
        </div>
      </AlertDialogContent>
    </AlertDialog>
  );
}


================================================
FILE: components/DeleteAccountConfirmDialog.tsx
================================================
"use client";

import Button from "@/components/Button";
import { useState } from "react";
import ConfirmDialog from "@/components/ConfirmDialog";
import LoadingSpinner from "@/components/loadingSpinner";
import { useAuth } from "@/context/AuthContext";

export default function DeleteAccountConfirmDialog() {
  const [deleting, setDeleting] = useState(false);
  const { user } = useAuth();

  async function deleteAccountHandler() {
    setDeleting(true);
    const res = await fetch("/api/user", {
      method: "DELETE",
    });
    if (res.ok && res.redirected) {
      window.location.href = res.url;
    } else {
      setDeleting(false);
    }
  }

  return (
    <ConfirmDialog
      text="confirm delete account"
      trigger={
        <Button variant="danger" disabled={deleting} type="button">
          {deleting && <LoadingSpinner />}
          <p>Delete Account</p>
        </Button>
      }
      onConfirm={deleteAccountHandler}
    >
      <img
        alt={user?.email || "Avatar for logged in user"}
        src={
          user?.profilePicture ||
          `https://avatars.dicebear.com/api/micah/${user?.email}.svg`
        }
        className="h-20 shrink-0 w-20 rounded-full border border-gray-300 transition-all duration-75 group-focus:outline-none group-active:scale-95 sm:h-20 sm:w-20"
      />
      <h3 className="text-lg font-medium">Delete Account</h3>
      <p className="text-center text-sm text-gray-500">
        Warning: This will permanently delete your account and all your data.
      </p>
    </ConfirmDialog>
  );
}


================================================
FILE: components/DeleteProjectConfirmDialog.tsx
================================================
"use client";

import Button from "@/components/Button";
import { useState } from "react";
import ConfirmDialog from "@/components/ConfirmDialog";
import LoadingSpinner from "@/components/loadingSpinner";
import { Project } from "@/types";
import { useRouter } from "next/navigation";
import useProjectList from "@/hooks/useProjectList";

export default function DeleteProjectConfirmDialog({
  project,
}: {
  project: Project | null;
}) {
  const [deleting, setDeleting] = useState(false);
  const { push, prefetch, refresh } = useRouter();
  const { deleteProject } = useProjectList();

  async function deleteProjectHandler() {
    if (!project) return;
    setDeleting(true);
    const res = await fetch("/api/project/" + project._id, {
      method: "DELETE",
    });
    const { errors } = await res.json();

    if (!errors) {
      deleteProject(project._id);
      refresh();
      push("/profile/projects");
    } else setDeleting(false);
  }

  return (
    <ConfirmDialog
      text="confirm delete project"
      trigger={
        <Button variant="danger" disabled={deleting} type="button">
          {deleting && <LoadingSpinner />}
          <p>Delete project</p>
        </Button>
      }
      onConfirm={deleteProjectHandler}
    />
  );
}


================================================
FILE: components/Divider.tsx
================================================
export default function Divider({ className }: { className?: string }) {
  return (
    <svg
      fill="none"
      shapeRendering="geometricPrecision"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="1"
      viewBox="0 0 24 24"
      width="14"
      height="14"
      className={className}
    >
      <path d="M16.88 3.549L7.12 20.451" />
    </svg>
  );
}


================================================
FILE: components/DomainCard.tsx
================================================
"use client";

import { useEffect, useState } from "react";
import LoadingSpinner from "@/components/loadingSpinner";
import ConfiguredSection from "@/components/ConfiguredSection";
import { Domain } from "@/types";
import Button from "@/components/Button";
import { useRouter } from "next/navigation";
import useProject from "@/hooks/useProject";

interface DomainCardProps {
  domain: Domain;
}

const DomainCard = ({ domain }: DomainCardProps) => {
  const removeDomainFromState = useProject((state) => state.removeDomain);
  const [domainInfo, setDomainInfo] = useState(null);
  const [isValidating, setIsValidating] = useState(false);
  const [removing, setRemoving] = useState(false);

  const { refresh } = useRouter();

  useEffect(() => {
    checkDomain();
    const interval = setInterval(checkDomain, 10000);
    return () => clearInterval(interval);
  }, []);

  async function checkDomain() {
    setIsValidating(true);
    const res = await fetch("/api/domain/check-domain", {
      method: "POST",
      body: JSON.stringify({ domain: domain.domain }),
    });
    const data = await res.json();
    setDomainInfo(data);
    setIsValidating(false);
  }

  async function removeDomain() {
    setRemoving(true);
    try {
      await fetch("/api/domain/remove-domain", {
        method: "POST",
        body: JSON.stringify({ domain: domain.domain }),
      });
      refresh();
      removeDomainFromState(domain._id);
    } catch {
      setRemoving(false);
    }
  }

  return (
    <div className="w-full bg-white border-y sm:border border-black sm:border-gray-50 sm:rounded-lg py-10">
      <div className="flex justify-between space-x-4 px-2 sm:px-10">
        <a
          href={`http://${domain.domain}`}
          target="_blank"
          rel="noreferrer"
          className="text-xl text-left font-semibold flex items-center"
        >
          {domain.domain}
          <span className="inline-block ml-2">
            <svg
              viewBox="0 0 24 24"
              width="20"
              height="20"
              stroke="currentColor"
              strokeWidth="1.5"
              strokeLinecap="round"
              strokeLinejoin="round"
              fill="none"
              shapeRendering="geometricPrecision"
            >
              <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
              <path d="M15 3h6v6" />
              <path d="M10 14L21 3" />
            </svg>
          </span>
        </a>
        <div className="flex space-x-3">
          <Button onClick={checkDomain} disabled={isValidating}>
            {isValidating ? <LoadingSpinner /> : "Refresh"}
          </Button>
          <Button variant="danger" onClick={removeDomain} disabled={removing}>
            {removing ? <LoadingSpinner /> : "Remove"}
          </Button>
        </div>
      </div>

      <ConfiguredSection domainInfo={domainInfo} />
    </div>
  );
};

export default DomainCard;


================================================
FILE: components/DomainConfiguration.tsx
================================================
"use client";

import { useState } from "react";
import { getSubdomain } from "@/utils/helpers";
import { DomainVerificationStatusProps } from "@/types";

export const InlineSnippet = ({ children }: { children: string }) => {
  return (
    <span className="inline-block rounded-md bg-blue-100 px-1 py-0.5 font-mono text-blue-900">
      {children}
    </span>
  );
};

export default function DomainConfiguration({
  data,
}: {
  data: { status: DomainVerificationStatusProps; response: any };
}) {
  const { domainJson } = data.response;
  console.log();
  const subdomain = getSubdomain(domainJson.name, domainJson.apexName);
  const [recordType, setRecordType] = useState(!!subdomain ? "CNAME" : "A");

  if (data.status === "Pending Verification") {
    const txtVerification = domainJson.verification.find(
      (x: any) => x.type === "TXT",
    );
    return (
      <div className="border-t border-gray-200 pt-5">
        <p className="text-sm">
          Please set the following TXT record on{" "}
          <InlineSnippet>{domainJson.apexName}</InlineSnippet> to prove
          ownership of <InlineSnippet>{domainJson.name}</InlineSnippet>:
        </p>
        <div className="my-5 flex items-start justify-start space-x-10 rounded-md bg-gray-50 p-2">
          <div>
            <p className="text-sm font-bold">Type</p>
            <p className="mt-2 font-mono text-sm">{txtVerification.type}</p>
          </div>
          <div>
            <p className="text-sm font-bold">Name</p>
            <p className="mt-2 font-mono text-sm">
              {txtVerification.domain.slice(
                0,
                txtVerification.domain.length - domainJson.apexName.length - 1,
              )}
            </p>
          </div>
          <div>
            <p className="text-sm font-bold">Value</p>
            <p className="mt-2 font-mono text-sm">
              <span className="text-ellipsis">{txtVerification.value}</span>
            </p>
          </div>
        </div>
        <p className="text-sm">
          Warning: if you are using this domain for another site, setting this
          TXT record will transfer domain ownership away from that site and
          break it. Please exercise caution when setting this record.
        </p>
      </div>
    );
  }

  if (data.status === "Unknown Error") {
    return (
      <div className="border-t border-gray-200 pt-5">
        <p className="mb-5 text-sm">{data.response.domainJson.error.message}</p>
      </div>
    );
  }

  return (
    <div className="border-t border-gray-200 pt-5">
      <div className="flex justify-start space-x-4">
        <button
          onClick={() => setRecordType("A")}
          className={`${
            recordType == "A"
              ? "border-black text-black"
              : "border-white text-gray-400"
          } ease border-b-2 pb-1 text-sm transition-all duration-150`}
        >
          A Record{!subdomain && " (recommended)"}
        </button>
        <button
          onClick={() => setRecordType("CNAME")}
          className={`${
            recordType == "CNAME"
              ? "border-black text-black"
              : "border-white text-gray-400"
          } ease border-b-2 pb-1 text-sm transition-all duration-150`}
        >
          CNAME Record{subdomain && " (recommended)"}
        </button>
      </div>
      <div className="my-3 text-left">
        <p className="my-5 text-sm">
          To configure your {recordType === "A" ? "apex domain" : "subdomain"} (
          <InlineSnippet>
            {recordType === "A" ? domainJson.apexName : domainJson.name}
          </InlineSnippet>
          ), set the following {recordType} record on your DNS provider to
          continue:
        </p>
        <div className="flex items-center justify-start space-x-10 rounded-md bg-gray-50 p-2">
          <div>
            <p className="text-sm font-bold">Type</p>
            <p className="mt-2 font-mono text-sm">{recordType}</p>
          </div>
          <div>
            <p className="text-sm font-bold">Name</p>
            <p className="mt-2 font-mono text-sm">
              {recordType === "A" ? "@" : subdomain ?? "www"}
            </p>
          </div>
          <div>
            <p className="text-sm font-bold">Value</p>
            <p className="mt-2 font-mono text-sm">
              {recordType === "A" ? `76.76.21.21` : `cname.dub.co`}
            </p>
          </div>
          <div>
            <p className="text-sm font-bold">TTL</p>
            <p className="mt-2 font-mono text-sm">86400</p>
          </div>
        </div>
        <p className="mt-5 text-sm">
          Note: for TTL, if <InlineSnippet>86400</InlineSnippet> is not
          available, set the highest value possible. Also, domain propagation
          can take anywhere between 1 hour to 12 hours.
        </p>
      </div>
    </div>
  );
}


================================================
FILE: components/Drawer.tsx
================================================
"use client";

import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/helpers";
import { XIcon } from "lucide-react";

const Sheet = SheetPrimitive.Root;

const SheetTrigger = SheetPrimitive.Trigger;

const SheetClose = SheetPrimitive.Close;

const SheetPortal = ({
  className,
  ...props
}: SheetPrimitive.DialogPortalProps) => (
  <SheetPrimitive.Portal className={cn(className)} {...props} />
);
SheetPortal.displayName = SheetPrimitive.Portal.displayName;

const SheetOverlay = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-white/70 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className,
    )}
    {...props}
    ref={ref}
  />
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
  "fixed z-50 gap-4 bg-white px-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
  {
    variants: {
      side: {
        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
        bottom:
          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
        right:
          "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
      },
    },
    defaultVariants: {
      side: "right",
    },
  },
);

interface SheetContentProps
  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
    VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Content>,
  SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
  <SheetPortal>
    <SheetOverlay />
    <SheetPrimitive.Content
      ref={ref}
      className={cn(sheetVariants({ side }), className)}
      {...props}
    >
      {children}
      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-gray-200">
        <XIcon className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </SheetPrimitive.Close>
    </SheetPrimitive.Content>
  </SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;

const SheetHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 pt-6 pb-2 text-center sm:text-left",
      className,
    )}
    {...props}
  />
);
SheetHeader.displayName = "SheetHeader";

const SheetFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className,
    )}
    {...props}
  />
);
SheetFooter.displayName = "SheetFooter";

const SheetTitle = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold text-black", className)}
    {...props}
  />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;

const SheetDescription = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Description
    ref={ref}
    className={cn("text-sm text-slate-800", className)}
    {...props}
  />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;

export {
  Sheet,
  SheetTrigger,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
};


================================================
FILE: components/HTMLPreview.tsx
================================================
interface HTMLPreviewProps {
  html: string;
}

export default function HTMLPreview({ html }: HTMLPreviewProps) {
  return (
    <iframe className="w-full min-h-screen cursor-pointer" srcDoc={html} />
  );
}


================================================
FILE: components/Header.tsx
================================================
"use client";
import useSearchParams from "@/hooks/useSearchParams";
import { useAuth } from "@/context/AuthContext";
import UserDropdown from "@/components/UserDropdown";
import Logo from "@/components/Logo";
import Button from "@/components/Button";
import ProjectSelect from "@/components/ProjectSelect";
import Divider from "@/components/Divider";
import { useParams, usePathname } from "next/navigation";

export default function Header() {
  const { set } = useSearchParams();
  const { user } = useAuth();
  const pathname = usePathname();
  const { id } = useParams();

  const isProjectPage = pathname.startsWith(`/profile/projects/${id}`);

  function openAuthModal() {
    set("authModal", "true");
  }

  function openPricesModal() {
    set("pricesModal", "true");
  }

  return (
    <header className="w-full px-6 py-4 absolute top-0">
      <div className="flex justify-between items-center h-10">
        <div className="flex items-center gap-2">
          <Logo href={isProjectPage ? `/profile/projects` : "/"} />
          {isProjectPage && (
            <>
              <Divider className="h-8 w-8 text-gray-200" />
              <ProjectSelect />
            </>
          )}
        </div>
        <div
          className={`flex items-center justify-center gap-3 sm:gap-4 ${
            user ? "flex-row-reverse" : ""
          }`}
        >
          {user ? (
            <UserDropdown />
          ) : (
            <div>
              <button
                className="auth-btn rounded-full px-4 py-1.5 text-sm font-medium text-gray-500 transition-colors ease-out hover:text-black"
                onClick={openAuthModal}
              >
                Log in
              </button>
              <button
                className="auth-btn rounded-full border border-black bg-black px-4 py-1.5 text-sm text-white transition-all hover:bg-white hover:text-black"
                onClick={openAuthModal}
              >
                Sign Up
              </button>
            </div>
          )}
          <a
            href="https://twitter.com/aipagedev"
            className="flex items-center text-gray-900 hover:text-blue-500 transition-colors"
            target="_blank"
            rel="noreferrer"
          >
            <svg
              className="mr-1 h-5 w-5 fill-current"
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 24 24"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
            >
              <path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
            </svg>
          </a>
          {user && (
            <Button
              className="auth-btn"
              variant="pill"
              onClick={openPricesModal}
            >
              Buy Credits 🎉
            </Button>
          )}
        </div>
      </div>
    </header>
  );
}


================================================
FILE: components/IconMenu.tsx
================================================
import { ReactNode } from "react";

interface MenuIconProps {
  icon: ReactNode;
  text: string;
}

export default function IconMenu({ icon, text }: MenuIconProps) {
  return (
    <div className="flex items-center justify-start space-x-2">
      {icon}
      <p className="text-sm">{text}</p>
    </div>
  );
}


================================================
FILE: components/ListProjects.tsx
================================================
"use client";
import { useAuth } from "@/context/AuthContext";
import Link from "next/link";
import Badge, { BadgeVariant } from "@/components/Badge";
import { Project } from "@/types";
import ProjectIcon from "@/components/ProjectIcon";
import { useEffect } from "react";
import useProjectList from "@/hooks/useProjectList";
import useProject from "@/hooks/useProject";

const mails = [
  "ozgurozalp1999@gmail.com",
  "mail@ozgurozalp.com",
  "denizlevregi7@gmail.com",
  "umit@altogic.com",
  "umit.cakmak@altogic.com",
];

interface ListProjectsProps {
  projects?: Project[] | null;
}

export default function ListProjects({ projects }: ListProjectsProps) {
  const { user } = useAuth();
  const { setProject } = useProject();
  const setProjects = useProjectList((state) => state.setProjects);
  const _projects = useProjectList((state) => state.projects);

  const hasPermission = mails.includes(user?.email as string);
  const Component = hasPermission ? Link : "div";

  useEffect(() => {
    setProjects(projects ?? []);
  }, []);

  return (
    <>
      {_projects?.map((project) => (
        <Component
          key={project._id}
          className="flex h-full flex-col space-y-10 rounded-lg border border-gray-100 bg-white p-4 sm:p-6 transition-all"
          href={`/profile/projects/${project._id}`}
          onClick={() => setProject(project)}
        >
          <div className="flex flex-1 items-start justify-between gap-1">
            <div className="flex space-x-3 flex-1">
              <ProjectIcon className="shrink-0 self-start" />
              <div className="flex-1">
                <h2 className="text-lg leading-[1.2] font-medium text-gray-700 truncate max-w-[15ch] md:max-w-[10ch] lg:max-w-[15ch] xl:max-w-[15ch]">
                  {project?.name ?? project?.content}
                </h2>
                <div className="flex items-center">
                  <p className="text-gray-500 text-sm leading-[1]">
                    example.com
                  </p>
                </div>
              </div>
            </div>
            <Badge
              variant={badgeMap[project.status ?? "draft"]}
              text={project.status ?? "draft"}
            />
          </div>
          <div className="flex mt-auto items-center space-x-4">
            <div className="flex items-center space-x-2 text-gray-500">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width={24}
                height={24}
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth={2}
                strokeLinecap="round"
                strokeLinejoin="round"
                className="h-4 w-4"
              >
                <circle cx={12} cy={12} r={10} />
                <line x1={2} x2={22} y1={12} y2={12} />
                <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
              </svg>
              <h2 className="whitespace-nowrap text-sm">
                {project.domains ? project.domains.length : 0}{" "}
                {project.domains?.length > 1 ? "domains" : "domain"}
              </h2>
            </div>
            <div className="flex items-center space-x-2 text-gray-500">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width={24}
                height={24}
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth={2}
                strokeLinecap="round"
                strokeLinejoin="round"
                className="h-4 w-4"
              >
                <line x1={18} x2={18} y1={20} y2={10} />
                <line x1={12} x2={12} y1={20} y2={4} />
                <line x1={6} x2={6} y1={20} y2={14} />
              </svg>
              <h2 className="whitespace-nowrap text-sm">
                {project.click ?? 0}
                {project.click > 1 ? " clicks" : " click"}
              </h2>
            </div>
          </div>
        </Component>
      ))}
    </>
  );
}

const badgeMap: Record<string, BadgeVariant> = {
  draft: "black",
  live: "green",
};


================================================
FILE: components/LoadingIcon.tsx
================================================
export default function LoadingIcon({ className }: { className?: string }) {
  return (
    <svg
      version="1.1"
      id="L9"
      xmlns="http://www.w3.org/2000/svg"
      xmlnsXlink="http://www.w3.org/1999/xlink"
      x="0px"
      y="0px"
      viewBox="0 0 100 100"
      enableBackground="new 0 0 0 0"
      xmlSpace="preserve"
      className={className}
    >
      <path
        fill="currentColor"
        d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"
      >
        <animateTransform
          attributeName="transform"
          attributeType="XML"
          type="rotate"
          dur="1s"
          from="0 50 50"
          to="360 50 50"
          repeatCount="indefinite"
        />
      </path>
    </svg>
  );
}


================================================
FILE: components/Logo.tsx
================================================
import Link from "next/link";

interface LogoProps {
  href?: string;
}

export default function Logo({ href }: LogoProps) {
  const Component = href ? Link : "span";
  return (
    <Component
      href={href as string}
      className="flex items-center tracking-tight"
    >
      <strong className="font-bold text-xl">ai</strong>
      <span className="text-xl">page.dev</span>
    </Component>
  );
}


================================================
FILE: components/LogoutIcon.tsx
================================================
export default function LogoutIcon({ className }: { className: string }) {
  return (
    <svg
      fill="none"
      height="24"
      shapeRendering="geometricPrecision"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="1.5"
      viewBox="0 0 24 24"
      width="24"
      className={className}
    >
      <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" />
      <path d="M16 17l5-5-5-5" />
      <path d="M21 12H9" />
    </svg>
  );
}


================================================
FILE: components/Modal.tsx
================================================
import { XIcon } from "lucide-react";
import { MouseEvent, ReactNode, useRef } from "react";
import { cn } from "@/utils/helpers";

interface ModalProps {
  close: () => void;
  isOpen: boolean;
  className?: string;
  children: ReactNode;
}
export default function Modal({
  close,
  children,
  isOpen,
  className,
}: ModalProps) {
  const modalWrapper = useRef<HTMLDivElement>(null);
  function modalWrapperClickHandler(event: MouseEvent) {
    if (!modalWrapper.current || event.target !== modalWrapper.current) return;
    close();
  }

  if (!isOpen) return null;
  return (
    <div
      ref={modalWrapper}
      className="fixed modal inset-0 bg-white/60 backdrop-blur-sm flex items-center justify-center z-50"
      onClick={modalWrapperClickHandler}
    >
      <div
        className={cn(
          "relative overflow-auto max-h-full bg-white w-full sm:w-[400px] border border-gray-100 rounded-2xl shadow-xl p-4",
          className,
        )}
      >
        <button
          type="button"
          className="absolute top-4 right-4 text-gray-500 hover:text-black transition-colors focus:outline-none"
          onClick={close}
        >
          <XIcon />
        </button>
        {children}
      </div>
    </div>
  );
}


================================================
FILE: components/NavLink.tsx
================================================
"use client";

import { usePathname } from "next/navigation";
import Link, { LinkProps } from "next/link";
import { ReactNode } from "react";
import { cn } from "@/utils/helpers";

interface NavLinkProps extends LinkProps {
  children: ReactNode;
  className?: string;
  target?: "_blank" | "_self" | "_parent" | "_top" | undefined;
}
export default function NavLink({
  className,
  href,
  children,
  target,
  ...props
}: NavLinkProps) {
  const path = usePathname();
  return (
    <Link
      href={href}
      data-active={path === href}
      className={cn(className)}
      target={target}
      {...props}
    >
      {children}
    </Link>
  );
}


================================================
FILE: components/Popover.tsx
================================================
import * as PopoverPrimitive from "@radix-ui/react-dropdown-menu";
import { Dispatch, ReactNode, SetStateAction } from "react";

export default function Popover({
  children,
  content,
  align = "center",
}: {
  children: ReactNode;
  content: ReactNode | string;
  align?: "center" | "start" | "end";
}) {
  return (
    <PopoverPrimitive.Root>
      <PopoverPrimitive.Trigger asChild>{children}</PopoverPrimitive.Trigger>
      <PopoverPrimitive.Content
        sideOffset={8}
        align={align}
        className="z-50 animate-slide-up-fade items-center rounded-md border border-gray-200 bg-white drop-shadow-lg block"
      >
        {content}
      </PopoverPrimitive.Content>
    </PopoverPrimitive.Root>
  );
}
Popover.Item = PopoverPrimitive.Item;


================================================
FILE: components/PricesModal.tsx
================================================
"use client";
import useSearchParams from "@/hooks/useSearchParams";
import Modal from "@/components/Modal";
import { Product } from "@/types";
import Products from "@/components/Products";

export default function PricesModal({ products }: { products: Product[] }) {
  const { deleteByKey, has } = useSearchParams();

  function close() {
    deleteByKey("pricesModal");
  }

  return (
    <Modal
      className="sm:w-[850px] p-6"
      close={close}
      isOpen={has("pricesModal")}
    >
      <Products products={products} />
    </Modal>
  );
}


================================================
FILE: components/Product.tsx
================================================
"use client";
import { cn, moneyFormat, stripePrice } from "@/utils/helpers";
import { useAuth } from "@/context/AuthContext";
import useSearchParams from "@/hooks/useSearchParams";
import { useState } from "react";
import { Product as ProductType } from "@/types";
import LoadingSpinner from "@/components/loadingSpinner";
import Button from "@/components/Button";

interface ProductProps {
  product: ProductType;
  className?: string;
}
export default function Product({ product, className }: ProductProps) {
  const [loading, setLoading] = useState(false);
  const { user } = useAuth();
  const { set } = useSearchParams();

  async function getPaymentLink(priceId: string) {
    if (!user) return set("authModal", "true");

    try {
      setLoading(true);
      const res = await fetch("/api/create-checkout-session", {
        method: "POST",
        body: JSON.stringify({ priceId }),
      });

      const { url } = await res.json();
      location.href = url;
    } catch (e) {
      setLoading(false);
    }
  }

  return (
    <div
      className={cn(
        "border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200",
        className
      )}
    >
      <div className="flex h-72 gap-5 flex-col">
        <div className="px-4 pt-4">
          <div className="flex flex-col gap-2 items-center justify-center text-center">
            <p className="text-gray-600 text-xs text-center py-2">
              {product.metadata.description}
            </p>
            <span className="font-display text-4xl font-semibold text-gray-900">
              {moneyFormat(stripePrice(product.unit_amount))}
            </span>
            <span className="text-xs text-gray-500">
              {product.nickname} Credits
            </span>
          </div>
        </div>
        <div className="flex-1 flex flex-col">
          <div className="flex w-full h-14 items-center justify-center border-b border-t border-gray-200 bg-gray-50">
            <p className="text-gray-600 text-xs text-center">
              {product.metadata.info}
            </p>
          </div>
          <div className="px-4 flex-1 flex items-center w-full justify-center">
            <Button
              disabled={loading}
              className="auth-btn w-full"
              variant="pill"
              onClick={() => getPaymentLink(product.id)}
            >
              {loading ? <LoadingSpinner /> : "Buy"}
            </Button>
          </div>
        </div>
      </div>
    </div>
  );
}


================================================
FILE: components/Products.tsx
================================================
"use client";
import { Product as ProductType } from "@/types";
import Product from "@/components/Product";
import { cn } from "@/utils/helpers";

export default function Products({ products }: { products: ProductType[] }) {
  return (
    <div className="space-y-10 py-10 px-4 sm:px-6 lg:px-8">
      <div className="sm:flex sm:flex-col sm:align-center">
        <h1 className="text-4xl text-center font-extrabold text-gray-900 sm:text-center">
          Support Our Mission
        </h1>
        <p className="text-center mt-5 text-lg text-gray-500 sm:text-center">
          By choosing a plan, you’re not just building beautiful landing pages —
          you’re becoming a cherished part of our journey and mission. Fuel our
          work so we can continue empowering your digital dreams. Every credit
          counts!
        </p>
      </div>
      <div className="grid sm:grid-cols-3 gap-6">
        {products
          .sort((a, b) => a.unit_amount - b.unit_amount)
          .map((product, index) => (
            <Product
              className={cn(index === 1 && "scale-110")}
              key={product.id}
              product={product}
            />
          ))}
      </div>
    </div>
  );
}


================================================
FILE: components/ProfileLayout.tsx
================================================
"use client";
import { ReactNode } from "react";
import ProfileMenu, { MenuItem } from "@/components/ProfileMenu";
import { useParams } from "next/navigation";

export default function ProfileLayout({ children }: { children: ReactNode }) {
  const { id } = useParams();

  const hasIdMenu: MenuItem[] = [
    {
      id: 1,
      name: "Design",
      href: `/profile/projects/${id}`,
    },
    {
      id: 2,
      name: "Domains",
      href: `/profile/projects/${id}/domains`,
    },
    {
      id: 3,
      name: "Preview",
      href: `/profile/projects/${id}/preview`,
      target: "_blank",
    },
    {
      id: 4,
      name: "Integrations",
      href: `/profile/projects/${id}/integrations`,
    },
    {
      id: 5,
      name: "Settings",
      href: `/profile/projects/${id}/settings`,
    },
  ];
  const hasNoIdMenu: MenuItem[] = [
    {
      id: 1,
      name: "Projects",
      href: "/profile/projects",
    },
    {
      id: 2,
      name: "Invoices",
      href: "/profile/invoices",
    },
    {
      id: 3,
      name: "Settings",
      href: "/profile/settings",
    },
  ];

  return (
    <div className="pt-[72px] profile-page flex flex-col">
      <ProfileMenu menuItems={id ? hasIdMenu : hasNoIdMenu} />
      {children}
    </div>
  );
}


================================================
FILE: components/ProfileMenu.tsx
================================================
"use client";

import { cn } from "@/utils/helpers";
import { useAuth } from "@/context/AuthContext";
import NavLink from "@/components/NavLink";

const mails = [
  "ozgurozalp1999@gmail.com",
  "mail@ozgurozalp.com",
  "denizlevregi7@gmail.com",
];

export interface MenuItem {
  id: number;
  name: string;
  href: string;
  target?: "_blank" | "_self" | "_parent" | "_top" | undefined;
}

interface ProfileMenuProps {
  menuItems: MenuItem[];
  className?: string;
}

export default function ProfileMenu({
  menuItems,
  className,
}: ProfileMenuProps) {
  const { user } = useAuth();

  return (
    <div
      className={cn(
        "flex border-b h-12 items-center justify-start space-x-2 overflow-x-auto scrollbar-hide px-6",
        className,
      )}
    >
      {menuItems
        .filter(
          (link) =>
            mails.includes(user?.email as string) || link.name !== "Projects",
        )
        .map((link) => (
          <NavLink
            className={cn(
              "border-b-2 p-1 border-transparent text-black",
              "data-[active=true]:border-black",
            )}
            href={link.href}
            key={link.href}
            target={link.target}
          >
            <div className="rounded-md px-3 py-2 transition-all duration-75 hover:bg-gray-100 active:bg-gray-200">
              <p className="text-sm">{link.name}</p>
            </div>
          </NavLink>
        ))}
    </div>
  );
}


================================================
FILE: components/ProjectDesign.tsx
================================================
"use client";

import { Project } from "@/types";
import BrowserWindow from "@/components/BrowserWindow";
import { FormEvent, useEffect, useRef, useState } from "react";
import Button from "@/components/Button";
import LoadingSpinner from "@/components/loadingSpinner";
import { updateProject } from "@/utils/helpers";

interface ProjectDesignProps {
  project: Project;
}

const includedKeys = [
  "padding",
  "color",
  "width",
  "height",
  "margin",
  "fontSize",
  "display",
  "position",
  "top",
  "left",
  "right",
  "bottom",
  "border",
  "background",
  "transform",
];

export default function ProjectDesign(props: ProjectDesignProps) {
  const iframe = useRef<HTMLIFrameElement>(null);
  const [selected, setSelected] = useState<HTMLElement | null>(null);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    const element = iframe.current;
    if (!element) return;

    let eventElements: NodeListOf<HTMLElement> | undefined;
    let aElements: NodeListOf<HTMLAnchorElement> | undefined;

    setTimeout(() => {
      eventElements = element.contentDocument?.querySelectorAll("body *");
      eventElements?.forEach((element) => {
        element.addEventListener("mouseover", mouseoverListener);
        element.addEventListener("click", clickHandler);
        element.addEventListener("mouseout", mouseoutListener);
      });
      aElements = element.contentDocument?.querySelectorAll("a");
      aElements?.forEach((element) => {
        element.addEventListener("click", aElementClickHandler);
      });
    }, 2000);

    return () => {
      eventElements?.forEach((element) => {
        element.removeEventListener("mouseover", mouseoverListener);
        element.removeEventListener("click", clickHandler);
        element.removeEventListener("mouseout", mouseoutListener);
      });
      aElements?.forEach((element) => {
        element.removeEventListener("click", aElementClickHandler);
      });
    };
  }, []);

  function aElementClickHandler(e: Event) {
    e.preventDefault();
  }

  function mouseoverListener(e: Event) {
    setOutline(e.target as HTMLElement);
  }

  function clickHandler(e: Event) {
    removeOutline(e.target as HTMLElement);
    setSelected(e.target as HTMLElement);
  }

  function mouseoutListener(e: Event) {
    removeOutline(e.target as HTMLElement);
  }

  function setOutline(target: HTMLElement) {
    target.style.outline = "2px solid #79155B";
    target.style.outlineOffset = "3px";
  }

  function removeOutline(target: HTMLElement) {
    target.style.outline = "";
    target.style.outlineOffset = "";
  }

  async function onSubmit(e: FormEvent) {
    e.preventDefault();
    if (!selected) return;
    const formData = new FormData(e.target as HTMLFormElement);
    const values = Array.from(formData.entries()) as [string, string][];

    values
      .filter(([, value]) => !!value)
      .forEach(([key, value]) => {
        if (key === "textContent") {
          selected.innerText = value;
        } else {
          if (selected.style.getPropertyValue(key) !== value)
            selected.style.setProperty(key, value);
        }
      });

    try {
      setSaving(true);
      const html = iframe.current?.contentDocument?.documentElement
        .outerHTML as string;
      await updateProject(
        {
          result: html,
        },
        props.project._id,
      );
      setSaving(false);
    } catch {
      alert("Failed to save changes");
      setSaving(false);
    }
  }

  const items = !selected
    ? []
    : Object.entries(getComputedStyle(selected)).filter(
        ([key]) =>
          !key.match(/\d/) &&
          includedKeys.includes(key) &&
          !key.startsWith("webkit"),
      );

  const innerText = selected?.innerText;

  return (
    <div className="w-full h-full max-h-[calc(100vh-168px)] grid items-start gap-4 sm:grid-cols-[200px_1fr] xl:grid-cols-[300px_1fr]">
      <div className="border rounded-xl overflow-y-auto h-full max-h-[calc(100vh-168px)]">
        {selected && (
          <form
            onSubmit={onSubmit}
            className="grid h-full grid-rows-[1fr_60px] gap-2 overflow-y-auto"
          >
            <div className="overflow-y-auto max-h-full pt-4 px-4">
              <div>
                <label>Text Content</label>
                <input
                  name="textContent"
                  className="border-gray-200 rounded w-full"
                  type="text"
                  key={selected.innerText}
                  defaultValue={innerText}
                />
              </div>
              {items.map(([key, value]) => (
                <div key={key}>
                  <label>{key}</label>
                  <input
                    name={key}
                    className="border-gray-200 rounded w-full"
                    type="text"
                    key={value}
                    defaultValue={value}
                  />
                </div>
              ))}
            </div>
            <div className="bg-white flex items-center px-4">
              <Button className="w-full" type="submit">
                {saving ? <LoadingSpinner /> : "Save changes"}
              </Button>
            </div>
          </form>
        )}
      </div>
      <BrowserWindow className="w-full h-full max-h-[calc(100vh-168px)]">
        <iframe
          ref={iframe}
          className="w-full h-full"
          srcDoc={props.project?.result}
        />
      </BrowserWindow>
    </div>
  );
}


================================================
FILE: components/ProjectIcon.tsx
================================================
import { cn } from "@/utils/helpers";

export default function ProjectIcon({ className }: { className?: string }) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="50"
      viewBox="0 0 200 150"
      className={cn(className)}
    >
      <rect
        x="10"
        y="10"
        width="180"
        height="130"
        rx="10"
        ry="10"
        fill="#F0F0F0"
        stroke="#CCCCCC"
      />

      <rect
        x="10"
        y="10"
        width="180"
        height="30"
        rx="10"
        ry="10"
        fill="#f0f0f0"
      />

      <circle cx="25" cy="25" r="4" fill="#FF605C" />
      <circle cx="40" cy="25" r="4" fill="#FFBD44" />
      <circle cx="55" cy="25" r="4" fill="#28CA41" />

      <rect
        x="20"
        y="50"
        width="160"
        height="30"
        rx="10"
        ry="10"
        fill="#FFFFFF"
      />
      <rect
        x="25"
        y="58"
        width="120"
        rx="5"
        ry="5"
        height="14"
        fill="#F0F0F0"
      />

      <rect x="20" y="90" width="50" rx="10" ry="10" height="40" fill="#ddd" />
      <rect x="75" y="90" width="50" rx="10" ry="10" height="40" fill="#ddd" />
      <rect x="130" y="90" width="50" rx="5" ry="10" height="40" fill="#ddd" />

      <circle cx="165" cy="64" r="8" fill="#F0F0F0" />
    </svg>
  );
}


================================================
FILE: components/ProjectSelect.tsx
================================================
import { ChevronsUpDown, PlusCircle, Check } from "lucide-react";
import Link from "next/link";
import Popover from "@/components/Popover";
import ProjectIcon from "@/components/ProjectIcon";
import { usePathname } from "next/navigation";
import useProjectList from "@/hooks/useProjectList";
import useProject from "@/hooks/useProject";
import { useEffect } from "react";

export default function ProjectSelect() {
  const { project } = useProject();

  return (
    <div>
      <Popover content={<ProjectList />}>
        <button className="flex items-center justify-between rounded-lg bg-white p-1.5 text-left text-sm transition-all duration-75 hover:bg-gray-100 focus:outline-none active:bg-gray-200">
          <div className="flex items-center space-x-3 pr-2">
            <ProjectIcon className="h-7 w-7 shrink-0" />
            <div className="flex items-center space-x-3 sm:flex">
              <span className="inline-block truncate text-sm font-medium max-w-[15ch]">
                {project?.name ?? project?.content}
              </span>
            </div>
          </div>
          <ChevronsUpDown
            className="h-4 w-4 text-gray-400"
            aria-hidden="true"
          />
        </button>
      </Popover>
    </div>
  );
}

function ProjectList() {
  const { projects } = useProjectList();
  const { setProject } = useProject();
  const pathname = usePathname();

  return (
    <div className="relative mt-1 max-h-72 w-full space-y-0.5 rounded-md bg-white p-2 text-base sm:w-60 sm:text-sm sm:shadow-lg">
      <div className="p-2 text-xs text-gray-500">Projects</div>
      <div className="overflow-y-auto scrollbar-hide max-h-[200px]">
        {projects.map((project) => (
          <Popover.Item asChild key={project._id}>
            <Link
              className="relative flex w-full items-center space-x-2 rounded-md px-2 py-1.5 hover:bg-gray-100 active:bg-gray-200 transition-all duration-75 outline-none focus:outline-none"
              href={`/profile/projects/${project._id}`}
              onClick={() => setProject(project)}
              shallow={false}
            >
              <ProjectIcon className="h-7 w-7 shrink-0" />
              <span
                title={project.content}
                className="block truncate text-sm max-w-[15ch]"
              >
                {project?.name ?? project?.content}
              </span>
              {pathname.includes(`/profile/projects/${project._id}`) && (
                <span className="absolute inset-y-0 right-0 flex items-center pr-3 text-black">
                  <Check className="h-5 w-5" aria-hidden="true" />
                </span>
              )}
            </Link>
          </Popover.Item>
        ))}
      </div>
      <Link
        href="/"
        className="flex w-full cursor-pointer items-center space-x-2 rounded-md p-2 transition-all duration-75 hover:bg-gray-100"
      >
        <PlusCircle className="h-6 w-6 text-gray-500" />
        <span className="block truncate">Add a new project</span>
      </Link>
    </div>
  );
}


================================================
FILE: components/RateModal.tsx
================================================
"use client";
import useSearchParams from "@/hooks/useSearchParams";
import Modal from "@/components/Modal";
import { Rating as ReactRating } from "@smastrom/react-rating";
import { FormEvent, useState } from "react";
import Button from "@/components/Button";
import { Star } from "@smastrom/react-rating";
import LoadingSpinner from "@/components/loadingSpinner";

export default function RateModal({ show }: { show: boolean }) {
  const { deleteByKey, has } = useSearchParams();
  const [rating, setRating] = useState(0);
  const [ratingText, setRatingText] = useState("");
  const [loading, setLoading] = useState(false);
  const [hasError, setHasError] = useState(false);

  function close() {
    deleteByKey("rateModal");
    setRating(0);
    setRatingText("");
  }

  async function submitHandler(event: FormEvent) {
    event.preventDefault();
    setLoading(true);
    setHasError(false);
    try {
      const res = await fetch("/api/message", {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          rating,
          ratingText,
        }),
      });
      if (res.ok) close();
      else {
        setHasError(true);
      }
    } catch (e) {
      setHasError(true);
    } finally {
      setLoading(false);
    }
  }

  return (
    <Modal
      close={close}
      isOpen={has("rateModal")}
      className="p-0 sm:w-[500px] w-[96%]"
    >
      <form onSubmit={submitHandler}>
        <div className="p-4 space-y-4">
          <h2 className="text-2xl font-medium text-center pt-6">
            <span className="text-6xl pb-3 block">🎉</span>
            Rate your experience
          </h2>
          <div className="flex items-center justify-center">
            <ReactRating
              itemStyles={{
                itemShapes: Star,
                activeFillColor: "#ffdc17",
                activeBoxBorderColor: "#faaf00",
                itemStrokeWidth: 1,
                activeStrokeColor: "transparent",
                inactiveFillColor: "#d7d7d7",
                inactiveStrokeColor: "transparent",
              }}
              className="!w-3/5 mb-1"
              value={rating}
              onChange={setRating}
            />
          </div>
        </div>
        <div className="bg-gray-50 p-4 border-t space-y-4">
          <p className="text-sm text-gray-500">
            Your feedback matters! Let us know how your experience was while
            crafting your landing page with AI
          </p>
          <textarea
            rows={3}
            name="feedback"
            value={ratingText}
            onChange={(e) => setRatingText(e.target.value)}
            className="w-full p-2 border !border-gray-200 resize-none rounded !outline-none focus:ring-2 focus:ring-gray-500"
            placeholder="Tell us more about your experience..."
          />
          {hasError && (
            <h2 className="text-red-500 text-center text-sm">
              There was an error submitting your feedback. Please try again
              later.
            </h2>
          )}
          <div className="flex justify-end gap-2">
            <Button type="button" variant="light" onClick={close}>
              Cancel
            </Button>
            <Button
              className="gap-2"
              type="submit"
              disabled={loading}
              variant="default"
            >
              {loading && <LoadingSpinner className="h-4 w-4" />}
              Submit
            </Button>
          </div>
        </div>
      </form>
    </Modal>
  );
}


================================================
FILE: components/ShowRate.tsx
================================================
"use client";
import { Rating, Star } from "@smastrom/react-rating";

interface ShowRateProps {
  rate: number;
}
export default function ShowRate({ rate }: ShowRateProps) {
  return (
    <Rating
      itemStyles={{
        itemShapes: Star,
        activeFillColor: "#faaf00",
        activeBoxBorderColor: "#faaf00",
        itemStrokeWidth: 1,
        activeStrokeColor: "transparent",
        inactiveStrokeColor: "#faaf00",
      }}
      readOnly
      className="!w-1/3 grid grid-cols-5"
      value={rate}
    />
  );
}


================================================
FILE: components/Switch.tsx
================================================
"use client";

import { Dispatch, SetStateAction } from "react";
// @ts-ignore
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/utils/helpers";

const Switch = ({
  fn,
  checked = false,
  disabled = false,
}: {
  fn: Dispatch<SetStateAction<boolean>> | (() => void);
  checked?: boolean;
  disabled?: boolean;
}) => {
  return (
    <SwitchPrimitive.Root
      checked={checked}
      name="switch"
      onCheckedChange={(checked: boolean) => fn(checked)}
      disabled={disabled}
      className={cn(
        disabled
          ? "cursor-not-allowed data-[state=checked]:bg-gray-300"
          : "cursor-pointer focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75 data-[state=checked]:bg-blue-500 data-[state=unchecked]:bg-gray-200",
        `relative inline-flex h-4 w-8 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out`,
      )}
    >
      <SwitchPrimitive.Thumb
        className={cn(
          "data-[state=unchecked]:translate-x-0",
          `pointer-events-none h-3 w-3 translate-x-4 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`,
        )}
      />
    </SwitchPrimitive.Root>
  );
};

export default Switch;


================================================
FILE: components/UserDropdown.tsx
================================================
"use client";
import { Gem, ReceiptIcon, Settings } from "lucide-react";
import Popover from "@/components/Popover";
import Badge from "@/components/Badge";
import { useAuth } from "@/context/AuthContext";
import IconMenu from "@/components/IconMenu";
import LogoutIcon from "@/components/LogoutIcon";
import Link from "next/link";

export default function UserDropdown() {
  const { user } = useAuth();
  return (
    <Popover
      content={
        <div className="flex w-full flex-col space-y-px rounded-md bg-white p-3 sm:w-56">
          <div className="p-2">
            {user?.name && (
              <p className="truncate text-sm font-medium text-gray-900">
                {user?.name}
              </p>
            )}
            <p className="truncate text-sm text-gray-500">{user?.email}</p>
          </div>
          <div className="w-full rounded-md p-2 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200 flex justify-between">
            <IconMenu text="Credits" icon={<Gem className="h-4 w-4" />} />
            <Badge
              text={user?.credits.toString() as string}
              variant={user?.credits === 0 ? "red" : "yellow"}
            />
          </div>
          <Popover.Item asChild>
            <Link
              href="/profile/invoices"
              className="block !outline-none w-full rounded-md p-2 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200"
            >
              <IconMenu
                text="Invoices"
                icon={<ReceiptIcon className="h-4 w-4" />}
              />
            </Link>
          </Popover.Item>
          <Popover.Item asChild>
            <Link
              href="/profile/settings"
              className="block !outline-none w-full rounded-md p-2 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200"
            >
              <IconMenu
                text="Settings"
                icon={<Settings className="h-4 w-4" />}
              />
            </Link>
          </Popover.Item>
          <Popover.Item asChild>
            <a
              href="/api/logout"
              className="block w-full !outline-none rounded-md p-2 text-sm transition-all duration-75 hover:bg-gray-100 active:bg-gray-200"
            >
              <IconMenu
                text="Logout"
                icon={<LogoutIcon className="h-4 w-4" />}
              />
            </a>
          </Popover.Item>
        </div>
      }
      align="end"
    >
      <button className="group relative shrink-0 !outline-none">
        {user ? (
          <img
            alt={user?.email || "Avatar for logged in user"}
            src={
              user.profilePicture ||
              `https://avatars.dicebear.com/api/micah/${user?.email}.svg`
            }
            className="h-9 shrink-0 w-9 rounded-full border border-gray-300 transition-all duration-75 group-focus:outline-none group-active:scale-95 sm:h-10 sm:w-10"
          />
        ) : (
          <div className="h-9 w-9 animate-pulse rounded-full border border-gray-300 bg-gray-100 sm:h-10 sm:w-10" />
        )}
        {user?.credits === 0 && (
          <div className="absolute -bottom-0.5 -right-0.5 h-4 w-4 rounded-full border-2 border-white bg-red-500" />
        )}
      </button>
    </Popover>
  );
}


================================================
FILE: components/XCircleFill.tsx
================================================
export default function XCircleFill({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      width="24"
      height="24"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      fill="none"
      shapeRendering="geometricPrecision"
    >
      <circle cx="12" cy="12" r="10" fill="currentColor" />
      <path d="M15 9l-6 6" stroke="white" />
      <path d="M9 9l6 6" stroke="white" />
    </svg>
  );
}


================================================
FILE: components/customDropdown.tsx
================================================
import Image from "next/image";
import { useState } from "react";

type TechIcon = {
  name: string;
  url: string;
};

type Stack = {
  tech: TechIcon[];
  eta: string;
};

type CustomDropdownProps = {
  stacks: Stack[];
  onSelect: (stack: Stack) => void;
};

export const CustomDropdown: React.FC<CustomDropdownProps> = ({
  stacks,
  onSelect,
}) => {
  const [isLoading, setIsLoading] = useState(false);

  const handleSelect = (stack: Stack) => {
    setIsLoading(true);
    onSelect(stack);
    setTimeout(() => {
      setIsLoading(false);
    }, parseDuration(stack.eta));
  };

  const parseDuration = (duration: string) => {
    const [value, unit] = duration.split(" ");
    return parseInt(value) * (unit === "min" ? 60000 : 1000);
  };

  return (
    <div className="origin-bottom-left relative right-16 mt-8 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
      <div
        className="py-1"
        role="menu"
        aria-orientation="vertical"
        aria-labelledby="options-menu"
      >
        {stacks.map((stack, index) => (
          <a
            href="#"
            key={index}
            className="block px-4 py-2 text-xs text-gray-700 hover:bg-gray-100 hover:text-gray-900"
            role="menuitem"
            onClick={() => handleSelect(stack)}
          >
            {stack.tech.map((tech, idx, arr) => (
              <span key={idx}>
                <Image
                  src={tech.url}
                  alt={tech.name}
                  width={16}
                  height={16}
                  className="inline-block h-4 w-4 mr-1"
                />
                {idx < arr.length - 1 && <span className="mx-1">+</span>}
              </span>
            ))}
            / {stack.eta}
          </a>
        ))}
        {isLoading && (
          <div className="flex justify-center items-center py-2"></div>
        )}
      </div>
    </div>
  );
};


================================================
FILE: components/loadingSpinner.module.css
================================================
.spinner {
  --spinner-color: currentColor;
  position: relative;
  top: 50%;
  left: 50%;
}
.spinner div {
  animation: spinner 1.2s linear infinite;
  background: var(--spinner-color, gray);
  position: absolute;
  border-radius: 1rem;
  width: 30%;
  height: 8%;
  left: -10%;
  top: -4%;
}

.spinner div:nth-child(1) {
  animation-delay: -1.2s;
  transform: rotate(1deg) translate(120%);
}
.spinner div:nth-child(2) {
  animation-delay: -1.1s;
  transform: rotate(30deg) translate(120%);
}
.spinner div:nth-child(3) {
  animation-delay: -1s;
  transform: rotate(60deg) translate(120%);
}
.spinner div:nth-child(4) {
  animation-delay: -0.9s;
  transform: rotate(90deg) translate(120%);
}
.spinner div:nth-child(5) {
  animation-delay: -0.8s;
  transform: rotate(120deg) translate(120%);
}
.spinner div:nth-child(6) {
  animation-delay: -0.7s;
  transform: rotate(150deg) translate(120%);
}
.spinner div:nth-child(7) {
  animation-delay: -0.6s;
  transform: rotate(180deg) translate(120%);
}
.spinner div:nth-child(8) {
  animation-delay: -0.5s;
  transform: rotate(210deg) translate(120%);
}
.spinner div:nth-child(9) {
  animation-delay: -0.4s;
  transform: rotate(240deg) translate(120%);
}
.spinner div:nth-child(10) {
  animation-delay: -0.3s;
  transform: rotate(270deg) translate(120%);
}
.spinner div:nth-child(11) {
  animation-delay: -0.2s;
  transform: rotate(300deg) translate(120%);
}
.spinner div:nth-child(12) {
  animation-delay: -0.1s;
  transform: rotate(330deg) translate(120%);
}

@keyframes spinner {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}


================================================
FILE: components/loadingSpinner.tsx
================================================
import { cn } from "@/utils/helpers";
import styles from "./loadingSpinner.module.css";

export default function LoadingSpinner({
  className,
  style,
}: {
  className?: string;
  style?: Record<string, any>;
}) {
  return (
    <div className={cn("h-5 w-5", className)}>
      <div className={cn(styles.spinner, "h-5 w-5", className)} style={style}>
        {[...Array(12)].map((_, i) => (
          <div key={i} />
        ))}
      </div>
    </div>
  );
}


================================================
FILE: components/tweetButton.tsx
================================================
import { useState } from "react";
import Head from "next/head";

const TweetButton = () => {
  // Array of possible tweet texts
  const tweetIntents = [
    "Just used AI to craft an EPIC landing page in minutes with AIpage.dev ! 🤖 This is the future of web design! Check it out 👉 @aipagedev",
    "Creating a stunning webpage has never been easier thanks to AIpage.dev! 🚀 Give it a try 👉 @aipagedev",
    "Web design will never be the same after you try AIpage.dev! 🛠️ A whole new level of creativity unleashed! Check it out 👉 @aipagedev",
    "Revolutionize your web design process with AIpage.dev. The future is here! 👉 @aipagedev",
    "I just built an amazing webpage with AIpage.dev in minutes! 🌟 You have to try this 👉 @aipagedev",
    "AIpage.dev is a game-changer for web design! Say hello to efficiency 👋 @aipagedev",
    "Why spend hours on web design when AIpage.dev can do it in minutes? 🕒 Check it out! 👉 @aipagedev",
    "Impressed by the power of AI in web design with AIpage.dev! This is incredible 👀 @aipagedev",
    "I used AIpage.dev and it completely transformed how I approach web design. You need to try this! 🎉 @aipagedev",
    "Just when I thought web design couldn’t get any easier, I found AIpage.dev! 🎊 Try it now 👉 @aipagedev",
    "Unleashing my inner designer with the help of AIpage.dev. This is next level! 🚀 Check it out 👉 @aipagedev",
    "With AIpage.dev, I can focus on creativity while AI handles the coding. It’s amazing! 💥 @aipagedev",
  ];

  // Function to generate a random index for selecting a tweet text
  const getRandomIndex = () => {
    return Math.floor(Math.random() * tweetIntents.length);
  };

  // Initial tweet text state
  const [tweet, setTweet] = useState(tweetIntents[getRandomIndex()]);

  // Function to capture iframe content (assuming you want this function)
  const handleClick = () => {
    // Randomize the tweet text after capturing the iframe content
    setTweet(tweetIntents[getRandomIndex()]);
  };

  // Randomly select a tweet text from the tweetIntents array

  return (
    <>
      <a
        onClick={handleClick}
        href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
          tweet
        )}`}
        target="_blank"
        rel="noreferrer"
        className="text-2xl animate-blink"
      >
        🐦
      </a>
    </>
  );
};

export default TweetButton;


================================================
FILE: context/AuthContext.tsx
================================================
"use client";

import { createContext, ReactNode, useContext, useState } from "react";
import { User } from "@/types";

interface AuthContext {
  user: User | null;
  setUser: (user: User | null) => void;
}

export const AuthContext = createContext<AuthContext>({
  user: null,
  setUser: () => {},
});

interface AuthProviderProps {
  children: ReactNode;
  user: User | null;
}

export const AuthProvider = ({ user, children }: AuthProviderProps) => {
  const [_user, setUser] = useState<User | null>(user);

  function setAuthUser(user: User | null) {
    setUser(user);
  }

  return (
    <AuthContext.Provider value={{ user: _user, setUser: setAuthUser }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within a AuthProvider");
  }
  return context;
};


================================================
FILE: custom.css
================================================
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes blink {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 1;
  }
}


================================================
FILE: hooks/useProject.tsx
================================================
"use client";

import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { Domain, Project } from "@/types";
import { useEffect } from "react";
import useProjectList from "@/hooks/useProjectList";

interface ProjectStore {
  project: Project | null;
  setProject: (project: Project | null) => void;
  addDomain: (domain: Domain) => void;
  removeDomain: (id: string) => void;
}

const useProject = create<ProjectStore>()(
  devtools(
    (set) => ({
      project: null,
      setProject: (project) => {
        set({ project });
        useProjectList.setState((prev) => {
          const projects = prev.projects.map((p) =>
            p._id === project?._id ? project : p,
          );
          return { projects };
        });
      },
      addDomain: (domain) => {
        set((prev) => {
          if (!prev.project) return prev;
          const project = {
            ...prev.project,
            domains: [...prev.project.domains, domain],
          };
          return { project };
        });
      },
      removeDomain: (id) => {
        set((prev) => {
          if (!prev.project) return prev;
          const project = {
            ...prev.project,
            domains: prev.project.domains.filter((d) => d._id !== id),
          };
          return { project };
        });
      },
    }),
    {
      name: "project-storage",
    },
  ),
);

export function SetProject({ project }: { project: Project | null }) {
  useEffect(() => {
    useProject.setState({ project });
  }, []);

  return <></>;
}

export default useProject;


================================================
FILE: hooks/useProjectList.tsx
================================================
"use client";

import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { Project } from "@/types";
import { useEffect } from "react";

interface ProjectList {
  projects: Project[];
  setProjects: (projects: Project[]) => void;
  updateProject: (id: string, project: Project) => void;
  deleteProject: (id: string) => void;
}

const useProjectList = create<ProjectList>()(
  devtools(
    (set) => ({
      projects: [],
      setProjects: (projects) => set({ projects }),
      updateProject: (id, project) =>
        set((state) => ({
          projects: state.projects.map((p) => (p._id === id ? project : p)),
        })),
      deleteProject: (id) =>
        set((state) => ({
          projects: state.projects.filter((p) => p._id !== id),
        })),
    }),
    {
      name: "project-list-storage",
    },
  ),
);

export function SetProjects({ projects }: { projects: Project[] }) {
  useEffect(() => {
    useProjectList.setState({ projects });
  }, []);

  return <></>;
}

export default useProjectList;


================================================
FILE: hooks/useSearchParams.ts
================================================
"use client";
import { useSearchParams as useNextSearchParams } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";

export default function useSearchParams() {
  const searchParams = useNextSearchParams();
  const router = useRouter();
  const path = usePathname();

  function getURL() {
    return new URL(path, window.location.origin);
  }

  function get(key: string) {
    return searchParams.get(key);
  }

  function set(key: string, value: string) {
    const params = getURL();
    params.searchParams.set(key, value);
    router.push(params.toString());
  }

  function deleteByKey(key: string) {
    const params = getURL();
    params.searchParams.delete(key);
    router.push(params.toString());
  }

  function has(key: string) {
    return searchParams.has(key);
  }

  return {
    set,
    has,
    get,
    deleteByKey,
  };
}


================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig


================================================
FILE: package.json
================================================
{
  "name": "next-openai",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -H 0.0.0.0",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-alert-dialog": "^1.0.4",
    "@radix-ui/react-dialog": "^1.0.4",
    "@radix-ui/react-dropdown-menu": "^2.0.5",
    "@radix-ui/react-popover": "^1.0.6",
    "@radix-ui/react-switch": "^1.0.3",
    "@smastrom/react-rating": "^1.3.2",
    "@upstash/ratelimit": "^0.4.3",
    "@upstash/redis": "^1.22.0",
    "ai": "^2.1.34",
    "altogic": "^2.3.9",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.0.0",
    "html-react-parser": "^4.2.2",
    "html2canvas": "^1.4.1",
    "lucide-react": "^0.268.0",
    "monaco-editor": "^0.40.0",
    "next": "13.4.19",
    "openai-edge": "^1.2.2",
    "re-resizable": "^6.9.9",
    "react": "18.2.0",
    "react-dom": "^18.2.0",
    "react-frame-component": "^5.2.6",
    "react-html-parser": "^2.0.2",
    "react-resizable": "^3.0.5",
    "react-select": "^5.7.3",
    "tailwind-merge": "^1.14.0",
    "tailwind-scrollbar-hide": "^1.1.7",
    "tailwindcss-animate": "^1.0.7",
    "zustand": "^4.4.1"
  },
  "devDependencies": {
    "@tailwindcss/forms": "^0.5.4",
    "@types/node": "^17.0.12",
    "@types/react": "18.2.7",
    "@types/react-dom": "18.2.4",
    "@types/react-html-parser": "^2.0.2",
    "@types/react-resizable": "^3.0.4",
    "autoprefixer": "^10.4.14",
    "eslint": "^7.32.0",
    "eslint-config-next": "13.4.4-canary.11",
    "postcss": "^8.4.23",
    "prettier": "^3.0.1",
    "tailwindcss": "^3.3.2",
    "typescript": "5.0.4"
  }
}


================================================
FILE: postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
}


================================================
FILE: public/site.webmanifest
================================================
{
  "name": "",
  "short_name": "",
  "icons": [
    {
      "src": "assets/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "assets/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "standalone"
}


================================================
FILE: styles/custom.css
================================================
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes blink {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 1;
  }
}

.blinking {
  animation: blink 1s infinite;
}


================================================
FILE: styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body,
.profile-page {
  min-height: 100dvh;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes blink {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 1;
  }
}

.blinking {
  animation: blink 1s infinite;
}

body:has(.modal) {
  overflow: hidden;
}

.auth-btn {
  @apply relative overflow-hidden;
}

.auth-btn::before {
  -webkit-animation: authBtnAnimation 3.5s;
  animation: authBtnAnimation 3.5s;
  -webkit-animation-delay: 1.8s;
  animation-delay: 1.8s;
  -webkit-animation-iteration-count: infinite;
  animation-iteration-count: infinite;
  -webkit-animation-timing-function: ease-out;
  animation-timing-function: ease-out;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0) 0,
    rgba(255, 255, 255, 0.18) 25%,
    rgba(255, 255, 255, 0.3) 50%,
    rgba(255, 255, 255, 0.18) 75%,
    rgba(255, 255, 255, 0)
  );
  content: "";
  display: block;
  height: 90px;
  left: -60%;
  position: absolute;
  top: -150px;
  transform: rotate(-25deg);
  width: 200px;
}

@keyframes authBtnAnimation {
  0% {
    left: -100%;
    top: -150px;
  }

  50% {
    left: 100%;
    top: 150px;
  }

  to {
    left: 100%;
    top: 150px;
  }
}


================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
        "gradient-conic":
          "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
      },

      outline: {
        blue: "2px solid #007BFF",
      },
      animation: {
        blink: "blink 1s infinite",
      },
    },
  },
  plugins: [
    require("@tailwindcss/forms"),
    require("tailwind-scrollbar-hide"),
    require("tailwindcss-animate"),
  ],
};


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}


================================================
FILE: types/index.ts
================================================
import type { User as AltogicUser } from "altogic";

export interface User extends AltogicUser {
  credits: number;
}

export interface Product {
  id: string;
  object: string;
  active: boolean;
  billing_scheme: string;
  created: number;
  currency: "usd" | string;
  custom_unit_amount: null;
  livemode: boolean;
  lookup_key: string | null;
  metadata: {
    description: string;
    info: string;
  };
  nickname: string;
  product: string;
  recurring: string | null;
  tax_behavior: string;
  tiers_mode: string | null;
  transform_quantity: string | null;
  type: string;
  unit_amount: number;
  unit_amount_decimal: string;
}

export interface Project {
  _id: string;
  content: string;
  name: string;
  deletedAt: string;
  result: string;
  rating: number;
  role: string;
  ratingText: string;
  user: string | User;
  createdAt: string;
  status: "draft" | "live";
  updatedAt: string;
  click: number;
  domains: Domain[];
}

export interface Domain {
  _id: string;
  updatedAt: string;
  createdAt: string;
  _parent: string;
  isPrimary: boolean;
  clickCount: number;
  status: DomainVerificationStatusProps;
  domain: string;
}

export interface Invoice {
  id: string;
  object: string;
  account_country: string;
  account_name: string;
  account_tax_ids: null | [];
  created: number;
  currency: string;
  livemode: boolean;
  paid: boolean;
  status: string;
  total: number;
  lines: {
    object: "list";
    data: {
      price: {
        id: "price_1NfeLpFctreK8fHPFa5RIeKt";
        object: "price";
        active: true;
        billing_scheme: "per_unit";
        created: number;
        currency: "usd" | string;
        livemode: false;
        lookup_key: null;
        metadata: {
          description: string;
          info: string;
        };
        nickname: "100";
      };
      quantity: 1;
    }[];
  };
  hosted_invoice_url: string;
  invoice_pdf: string;
}

export type DomainVerificationStatusProps =
  | "Valid Configuration"
  | "Invalid Configuration"
  | "Pending Verification"
  | "Domain Not Found"
  | "Unknown Error";

export interface DomainResponse {
  name: string;
  apexName: string;
  projectId: string;
  redirect?: string | null;
  redirectStatusCode?: (307 | 301 | 302 | 308) | null;
  gitBranch?: string | null;
  updatedAt?: number;
  createdAt?: number;
  verified: boolean;
  verification?: {
    type: string;
    domain: string;
    value: string;
    reason: string;
  }[];
}

export interface DomainInfo {
  configured: boolean;
  name: string;
  apexName: string;
  projectId: string;
  redirect?: string | null;
  redirectStatusCode?: (307 | 301 | 302 | 308) | null;
  gitBranch?: string | null;
  updatedAt?: number;
  createdAt?: number;
  verified: boolean;
  verification?: {
    type: string;
    domain: string;
    value: string;
    reason: string;
  }[];
}


================================================
FILE: utils/altogic.ts
================================================
import { createClient } from "altogic";

const clientKey = process.env.NEXT_PUBLIC_ALTOGIC_CLIENT_KEY;
const apiBaseURL = process.env.NEXT_PUBLIC_ALTOGIC_API_BASE_URL;

if (!clientKey || !apiBaseURL) {
  throw new Error(
    "Please define the NEXT_PUBLIC_ALTOGIC_CLIENT_KEY and NEXT_PUBLIC_ALTOGIC_API_BASE_URL environment variables inside .env file",
  );
}

const altogic = createClient(apiBaseURL, clientKey, {
  signInRedirect: "/",
});

export default altogic;


================================================
FILE: utils/auth.ts
================================================
import { cookies, headers } from "next/headers";
import { Invoice, Product, Project, User } from "@/types";
import altogic from "@/utils/altogic";
import { NextResponse } from "next/server";

const isDev = process.env.NODE_ENV === "development";

export function getSessionCookie() {
  const cookieStore = cookies();
  const token = cookieStore.get("sessionToken");
  return token?.value;
}

export async function fetchAuthUser() {
  const token = getSessionCookie();
  if (!token) return;

  // @ts-ignore
  altogic.auth.setSession({
    token,
  });

  const { user } = await altogic.auth.getUserFromDB();
  return user as User;
}

export async function fetchProducts(): Promise<Product[]> {
  const path = process.env.NEXT_PUBLIC_GET_PRICES_PATH as string;
  const { data, errors } = await altogic.endpoint.get(path);
  if (errors) throw new Error("Failed to fetch Products");
  return data.data;
}

export async function fetchInvoices(): Promise<Invoice[]> {
  const path = process.env.NEXT_PUBLIC_GET_INVOICES_PATH as string;
  const { data } = await altogic.endpoint.get(path, undefined, {
    Session: getSessionCookie(),
  });
  return data?.data ?? [];
}

export async function updateUser(data: Partial<User>) {
  const token = getSessionCookie();
  if (!token) throw new Error("No token found");

  // @ts-ignore
  altogic.auth.setSession({
    token,
  });

  const { user, errors } = await altogic.auth.getUserFromDB();

  if (!user || errors) throw new Error("Failed to fetch User");

  return await altogic.db
    .model("users")
    .object(user?._id)
    .update(data);
}

export async function deleteUser() {
  const token = getSessionCookie();
  if (!token) throw new Error("No token found");

  // @ts-ignore
  altogic.auth.setSession({
    token,
  });

  const { user, errors } = await altogic.auth.getUserFromDB();

  if (!user || errors) throw new Error("Failed to fetch User");

  return await altogic.db
    .model("users")
    .object(user?._id)
    .delete();
}

export function logout(req: Request, nextResponse: typeof NextResponse) {
  /*altogic.auth
    .signOut(token?.value)
    .then(console.log)
    .catch(console.error);*/

  const destinationUrl = new URL("/", new URL(req.url).origin);
  const response = nextResponse.redirect(destinationUrl);
  response.cookies.delete("sessionToken");
  return response;
}

export async function fetchProjects(): Promise<Project[] | null> {
  const { data, errors } = await altogic.endpoint.get("/projects", undefined, {
    Session: getSessionCookie(),
  });
  if (errors) throw new Error("Failed to fetch Projects");
  return data.result;
}

export async function fetchProjectById(id: string): Promise<Project | null> {
  const regex = /^[a-fA-F0-9]{24}$/g; // mongo id regex
  if (!regex.test(id)) return null;

  const { data, errors } = await altogic.endpoint.get(
    "/project/" + id,
    undefined,
    {
      Session: getSessionCookie(),
    },
  );
  if (errors) {
    console.log(JSON.stringify(errors, null, 4));
    return null;
  }
  return data as Project;
}

export async function updateProjectName(id: string, name: string) {
  const token = getSessionCookie();

  const { data: project, errors } = await altogic.endpoint.put(
    `/project/name/${id}`,
    {
      name,
    },
    undefined,
    {
      Session: getSessionCookie(),
    },
  );

  return { project, errors };
}

export async function deleteProject(id: string) {
  const { errors } = await altogic.endpoint.delete(
    `/project`,
    {
      id,
    },
    undefined,
    {
      Session: getSessionCookie(),
    },
  );

  return { errors };
}

export async function getProjectByDomain(
  domain: string,
): Promise<Project | null> {
  const res = await fetch(
    `${process.env.NEXT_PUBLIC_ALTOGIC_API_BASE_URL}/project/domain`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        domain: domain.replace("www.", ""),
      }),
    },
  );

  try {
    const data = await res.json();
    if (data.errors) return null;
    return data;
  } catch {
    return null;
  }
}


================================================
FILE: utils/helpers.ts
================================================
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { Project } from "@/types";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export function stripePrice(price: number) {
  return price / 100;
}

export function moneyFormat(price: number) {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(price);
}

export const getSubdomain = (name: string, apexName: string) => {
  if (name === apexName) return null;
  return name.slice(0, name.length - apexName.length - 1);
};

export function capitalize(str: string) {
  if (!str || typeof str !== "string") return str;
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export const truncate = (str: string | null, length: number) => {
  if (!str || str.length <= length) return str;
  return `${str.slice(0, length - 3)}...`;
};

export function toReversed<T>(arr: T[]) {
  const array = [...arr];
  return array.reverse();
}

export async function updateProject(
  data: Omit<Partial<Project>, "_id">,
  id?: string,
) {
  const body = {
    ...data,
    ...(id && { _id: id }),
  };
  const res = await fetch("/api/message", {
    headers: {
      "Content-Type": "application/json",
    },
    method: "PUT",
    body: JSON.stringify(body),
  });

  return res.json();
}

export function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export function isAipage(host: string) {
  const hosts = [
    "localhost",
    "localhost:3000",
    "localhost:3000",
    "ozgurozalp.test",
    "aipage-dev.vercel.app",
  ];
  return hosts.includes(host);
}


================================================
FILE: utils/redis.ts
================================================
import { Redis } from "@upstash/redis";

const redis =
  !!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN
    ? new Redis({
        url: process.env.UPSTASH_REDIS_REST_URL,
        token: process.env.UPSTASH_REDIS_REST_TOKEN,
      })
    : undefined;

export default redis;
Download .txt
gitextract_yf13pds3/

├── .github/
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app/
│   ├── (aipage)/
│   │   ├── layout.tsx
│   │   ├── loading.tsx
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   └── profile/
│   │       ├── invoices/
│   │       │   ├── loading.tsx
│   │       │   └── page.tsx
│   │       ├── layout.tsx
│   │       ├── projects/
│   │       │   ├── [id]/
│   │       │   │   ├── domains/
│   │       │   │   │   ├── layout.tsx
│   │       │   │   │   └── page.tsx
│   │       │   │   ├── integrations/
│   │       │   │   │   └── page.tsx
│   │       │   │   ├── layout.tsx
│   │       │   │   ├── loading.tsx
│   │       │   │   ├── page.tsx
│   │       │   │   └── settings/
│   │       │   │       └── page.tsx
│   │       │   ├── loading.tsx
│   │       │   └── page.tsx
│   │       └── settings/
│   │           ├── loading.tsx
│   │           └── page.tsx
│   ├── (preview)/
│   │   ├── not-found.tsx
│   │   └── profile/
│   │       └── projects/
│   │           └── [id]/
│   │               └── preview/
│   │                   ├── loading.tsx
│   │                   └── page.tsx
│   ├── api/
│   │   ├── chat/
│   │   │   └── route.ts
│   │   ├── create-checkout-session/
│   │   │   └── route.ts
│   │   ├── domain/
│   │   │   ├── check-domain/
│   │   │   │   └── route.ts
│   │   │   ├── remove-domain/
│   │   │   │   └── route.ts
│   │   │   └── verify-domain/
│   │   │       └── route.ts
│   │   ├── logout/
│   │   │   └── route.ts
│   │   ├── message/
│   │   │   └── route.ts
│   │   ├── og/
│   │   │   └── route.tsx
│   │   ├── project/
│   │   │   ├── [id]/
│   │   │   │   ├── domain/
│   │   │   │   │   └── route.ts
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   └── user/
│   │       └── route.ts
│   ├── auth-redirect/
│   │   └── route.ts
│   ├── layout.tsx
│   └── not-found.tsx
├── components/
│   ├── AddDomainModal.tsx
│   ├── AlertCircleFill.tsx
│   ├── AlertDialog.tsx
│   ├── AuthModal.tsx
│   ├── Badge.tsx
│   ├── BrowserWindow.tsx
│   ├── Button.tsx
│   ├── Chart.tsx
│   ├── CheckCircleFill.tsx
│   ├── ConfiguredSection.tsx
│   ├── ConfirmDialog.tsx
│   ├── DeleteAccountConfirmDialog.tsx
│   ├── DeleteProjectConfirmDialog.tsx
│   ├── Divider.tsx
│   ├── DomainCard.tsx
│   ├── DomainConfiguration.tsx
│   ├── Drawer.tsx
│   ├── HTMLPreview.tsx
│   ├── Header.tsx
│   ├── IconMenu.tsx
│   ├── ListProjects.tsx
│   ├── LoadingIcon.tsx
│   ├── Logo.tsx
│   ├── LogoutIcon.tsx
│   ├── Modal.tsx
│   ├── NavLink.tsx
│   ├── Popover.tsx
│   ├── PricesModal.tsx
│   ├── Product.tsx
│   ├── Products.tsx
│   ├── ProfileLayout.tsx
│   ├── ProfileMenu.tsx
│   ├── ProjectDesign.tsx
│   ├── ProjectIcon.tsx
│   ├── ProjectSelect.tsx
│   ├── RateModal.tsx
│   ├── ShowRate.tsx
│   ├── Switch.tsx
│   ├── UserDropdown.tsx
│   ├── XCircleFill.tsx
│   ├── customDropdown.tsx
│   ├── loadingSpinner.module.css
│   ├── loadingSpinner.tsx
│   └── tweetButton.tsx
├── context/
│   └── AuthContext.tsx
├── custom.css
├── hooks/
│   ├── useProject.tsx
│   ├── useProjectList.tsx
│   └── useSearchParams.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public/
│   └── site.webmanifest
├── styles/
│   ├── custom.css
│   └── globals.css
├── tailwind.config.js
├── tsconfig.json
├── types/
│   └── index.ts
└── utils/
    ├── altogic.ts
    ├── auth.ts
    ├── helpers.ts
    └── redis.ts
Download .txt
SYMBOL INDEX (139 symbols across 83 files)

FILE: app/(aipage)/layout.tsx
  function AipageLayout (line 5) | async function AipageLayout({

FILE: app/(aipage)/loading.tsx
  function RootLoading (line 3) | function RootLoading() {

FILE: app/(aipage)/not-found.tsx
  function NotFound (line 4) | async function NotFound() {

FILE: app/(aipage)/page.tsx
  type DeviceSize (line 14) | enum DeviceSize {
  function Chat (line 20) | function Chat() {

FILE: app/(aipage)/profile/invoices/loading.tsx
  function InvoicesLoading (line 3) | function InvoicesLoading() {

FILE: app/(aipage)/profile/invoices/page.tsx
  function InvoicesPage (line 7) | async function InvoicesPage() {
  function DesktopTable (line 53) | function DesktopTable({ invoices }: { invoices: Invoice[] }) {
  function MobileTable (line 136) | function MobileTable({ invoices }: { invoices: Invoice[] }) {

FILE: app/(aipage)/profile/layout.tsx
  function ProfileBaseLayout (line 4) | async function ProfileBaseLayout({

FILE: app/(aipage)/profile/projects/[id]/domains/layout.tsx
  function DomainLayout (line 8) | function DomainLayout({ children }: { children: ReactNode }) {

FILE: app/(aipage)/profile/projects/[id]/domains/page.tsx
  function ProjectDomains (line 6) | async function ProjectDomains({

FILE: app/(aipage)/profile/projects/[id]/integrations/page.tsx
  function ProjectIntegrations (line 1) | function ProjectIntegrations({

FILE: app/(aipage)/profile/projects/[id]/layout.tsx
  function ProjectLayout (line 6) | async function ProjectLayout({

FILE: app/(aipage)/profile/projects/[id]/loading.tsx
  function ProjectByIdLoading (line 3) | function ProjectByIdLoading() {

FILE: app/(aipage)/profile/projects/[id]/page.tsx
  function ProjectDetail (line 18) | async function ProjectDetail({
  type PanelProps (line 45) | interface PanelProps {
  function Panel (line 51) | function Panel(props: PanelProps) {

FILE: app/(aipage)/profile/projects/[id]/settings/page.tsx
  function ProjectSettingsPage (line 12) | function ProjectSettingsPage() {

FILE: app/(aipage)/profile/projects/loading.tsx
  function ProjectsLoading (line 3) | function ProjectsLoading() {

FILE: app/(aipage)/profile/projects/page.tsx
  function ProfileProjects (line 9) | async function ProfileProjects() {

FILE: app/(aipage)/profile/settings/loading.tsx
  function SettingsLoading (line 3) | function SettingsLoading() {

FILE: app/(aipage)/profile/settings/page.tsx
  function ProfileSettings (line 10) | function ProfileSettings() {

FILE: app/(preview)/not-found.tsx
  function NotFound (line 4) | async function NotFound() {

FILE: app/(preview)/profile/projects/[id]/preview/loading.tsx
  function PreviewLoading (line 3) | function PreviewLoading() {

FILE: app/(preview)/profile/projects/[id]/preview/page.tsx
  function projectPreview (line 5) | async function projectPreview({

FILE: app/api/chat/route.ts
  function POST (line 28) | async function POST(req: Request) {

FILE: app/api/create-checkout-session/route.ts
  function POST (line 5) | async function POST(req: Request) {

FILE: app/api/domain/check-domain/route.ts
  function POST (line 3) | async function POST(req: Request) {

FILE: app/api/domain/remove-domain/route.ts
  function POST (line 5) | async function POST(req: Request) {

FILE: app/api/domain/verify-domain/route.ts
  function POST (line 3) | async function POST(req: Request) {

FILE: app/api/logout/route.ts
  function GET (line 4) | async function GET(req: Request) {

FILE: app/api/message/route.ts
  function PUT (line 6) | async function PUT(req: Request) {

FILE: app/api/og/route.tsx
  function GET (line 8) | async function GET(request: Request) {

FILE: app/api/project/[id]/domain/route.ts
  function POST (line 5) | async function POST(

FILE: app/api/project/[id]/route.ts
  function DELETE (line 5) | async function DELETE(

FILE: app/api/project/route.ts
  function PATCH (line 4) | async function PATCH(req: Request) {

FILE: app/api/user/route.ts
  function PATCH (line 4) | async function PATCH(req: Request) {
  function DELETE (line 14) | async function DELETE(req: Request) {

FILE: app/auth-redirect/route.ts
  function GET (line 4) | async function GET(req: Request) {

FILE: app/layout.tsx
  function generateMetadata (line 14) | async function generateMetadata(): Promise<Metadata> {
  function RootLayout (line 39) | async function RootLayout({

FILE: app/not-found.tsx
  function NotFound (line 4) | async function NotFound() {

FILE: components/AddDomainModal.tsx
  function AddDomainModal (line 13) | function AddDomainModal() {

FILE: components/AlertCircleFill.tsx
  function AlertCircleFill (line 1) | function AlertCircleFill({ className }: { className: string }) {

FILE: components/AuthModal.tsx
  function AuthModal (line 11) | function AuthModal() {

FILE: components/Badge.tsx
  type BadgeVariant (line 3) | type BadgeVariant = "yellow" | "gray" | "red" | "black" | "green";
  function Badge (line 5) | function Badge({

FILE: components/BrowserWindow.tsx
  type BrowserWindowProps (line 4) | interface BrowserWindowProps {
  function BrowserWindow (line 8) | function BrowserWindow({

FILE: components/Button.tsx
  type ButtonVariant (line 4) | type ButtonVariant = "default" | "light" | "pill" | "danger";
  type ButtonProps (line 6) | interface ButtonProps extends ComponentPropsWithoutRef<"button"> {

FILE: components/Chart.tsx
  function Chart (line 1) | function Chart({ className }: { className: string }) {

FILE: components/CheckCircleFill.tsx
  function CheckCircleFill (line 1) | function CheckCircleFill({ className }: { className?: string }) {

FILE: components/ConfiguredSection.tsx
  type ConfiguredSectionProps (line 4) | interface ConfiguredSectionProps {

FILE: components/ConfirmDialog.tsx
  type ConfirmDialogProps (line 15) | interface ConfirmDialogProps {
  function ConfirmDialog (line 22) | function ConfirmDialog({

FILE: components/DeleteAccountConfirmDialog.tsx
  function DeleteAccountConfirmDialog (line 9) | function DeleteAccountConfirmDialog() {

FILE: components/DeleteProjectConfirmDialog.tsx
  function DeleteProjectConfirmDialog (line 11) | function DeleteProjectConfirmDialog({

FILE: components/Divider.tsx
  function Divider (line 1) | function Divider({ className }: { className?: string }) {

FILE: components/DomainCard.tsx
  type DomainCardProps (line 11) | interface DomainCardProps {
  function checkDomain (line 29) | async function checkDomain() {
  function removeDomain (line 40) | async function removeDomain() {

FILE: components/DomainConfiguration.tsx
  function DomainConfiguration (line 15) | function DomainConfiguration({

FILE: components/Drawer.tsx
  type SheetContentProps (line 57) | interface SheetContentProps

FILE: components/HTMLPreview.tsx
  type HTMLPreviewProps (line 1) | interface HTMLPreviewProps {
  function HTMLPreview (line 5) | function HTMLPreview({ html }: HTMLPreviewProps) {

FILE: components/Header.tsx
  function Header (line 11) | function Header() {

FILE: components/IconMenu.tsx
  type MenuIconProps (line 3) | interface MenuIconProps {
  function IconMenu (line 8) | function IconMenu({ icon, text }: MenuIconProps) {

FILE: components/ListProjects.tsx
  type ListProjectsProps (line 19) | interface ListProjectsProps {
  function ListProjects (line 23) | function ListProjects({ projects }: ListProjectsProps) {

FILE: components/LoadingIcon.tsx
  function LoadingIcon (line 1) | function LoadingIcon({ className }: { className?: string }) {

FILE: components/Logo.tsx
  type LogoProps (line 3) | interface LogoProps {
  function Logo (line 7) | function Logo({ href }: LogoProps) {

FILE: components/LogoutIcon.tsx
  function LogoutIcon (line 1) | function LogoutIcon({ className }: { className: string }) {

FILE: components/Modal.tsx
  type ModalProps (line 5) | interface ModalProps {
  function Modal (line 11) | function Modal({

FILE: components/NavLink.tsx
  type NavLinkProps (line 8) | interface NavLinkProps extends LinkProps {
  function NavLink (line 13) | function NavLink({

FILE: components/Popover.tsx
  function Popover (line 4) | function Popover({

FILE: components/PricesModal.tsx
  function PricesModal (line 7) | function PricesModal({ products }: { products: Product[] }) {

FILE: components/Product.tsx
  type ProductProps (line 10) | interface ProductProps {
  function Product (line 14) | function Product({ product, className }: ProductProps) {

FILE: components/Products.tsx
  function Products (line 6) | function Products({ products }: { products: ProductType[] }) {

FILE: components/ProfileLayout.tsx
  function ProfileLayout (line 6) | function ProfileLayout({ children }: { children: ReactNode }) {

FILE: components/ProfileMenu.tsx
  type MenuItem (line 13) | interface MenuItem {
  type ProfileMenuProps (line 20) | interface ProfileMenuProps {
  function ProfileMenu (line 25) | function ProfileMenu({

FILE: components/ProjectDesign.tsx
  type ProjectDesignProps (line 10) | interface ProjectDesignProps {
  function ProjectDesign (line 32) | function ProjectDesign(props: ProjectDesignProps) {

FILE: components/ProjectIcon.tsx
  function ProjectIcon (line 3) | function ProjectIcon({ className }: { className?: string }) {

FILE: components/ProjectSelect.tsx
  function ProjectSelect (line 10) | function ProjectSelect() {
  function ProjectList (line 35) | function ProjectList() {

FILE: components/RateModal.tsx
  function RateModal (line 10) | function RateModal({ show }: { show: boolean }) {

FILE: components/ShowRate.tsx
  type ShowRateProps (line 4) | interface ShowRateProps {
  function ShowRate (line 7) | function ShowRate({ rate }: ShowRateProps) {

FILE: components/UserDropdown.tsx
  function UserDropdown (line 10) | function UserDropdown() {

FILE: components/XCircleFill.tsx
  function XCircleFill (line 1) | function XCircleFill({ className }: { className?: string }) {

FILE: components/customDropdown.tsx
  type TechIcon (line 4) | type TechIcon = {
  type Stack (line 9) | type Stack = {
  type CustomDropdownProps (line 14) | type CustomDropdownProps = {

FILE: components/loadingSpinner.tsx
  function LoadingSpinner (line 4) | function LoadingSpinner({

FILE: context/AuthContext.tsx
  type AuthContext (line 6) | interface AuthContext {
  type AuthProviderProps (line 16) | interface AuthProviderProps {
  function setAuthUser (line 24) | function setAuthUser(user: User | null) {

FILE: hooks/useProject.tsx
  type ProjectStore (line 9) | interface ProjectStore {
  function SetProject (line 56) | function SetProject({ project }: { project: Project | null }) {

FILE: hooks/useProjectList.tsx
  type ProjectList (line 8) | interface ProjectList {
  function SetProjects (line 35) | function SetProjects({ projects }: { projects: Project[] }) {

FILE: hooks/useSearchParams.ts
  function useSearchParams (line 5) | function useSearchParams() {

FILE: types/index.ts
  type User (line 3) | interface User extends AltogicUser {
  type Product (line 7) | interface Product {
  type Project (line 32) | interface Project {
  type Domain (line 49) | interface Domain {
  type Invoice (line 60) | interface Invoice {
  type DomainVerificationStatusProps (line 97) | type DomainVerificationStatusProps =
  type DomainResponse (line 104) | interface DomainResponse {
  type DomainInfo (line 122) | interface DomainInfo {

FILE: utils/auth.ts
  function getSessionCookie (line 8) | function getSessionCookie() {
  function fetchAuthUser (line 14) | async function fetchAuthUser() {
  function fetchProducts (line 27) | async function fetchProducts(): Promise<Product[]> {
  function fetchInvoices (line 34) | async function fetchInvoices(): Promise<Invoice[]> {
  function updateUser (line 42) | async function updateUser(data: Partial<User>) {
  function deleteUser (line 61) | async function deleteUser() {
  function logout (line 80) | function logout(req: Request, nextResponse: typeof NextResponse) {
  function fetchProjects (line 92) | async function fetchProjects(): Promise<Project[] | null> {
  function fetchProjectById (line 100) | async function fetchProjectById(id: string): Promise<Project | null> {
  function updateProjectName (line 118) | async function updateProjectName(id: string, name: string) {
  function deleteProject (line 135) | async function deleteProject(id: string) {
  function getProjectByDomain (line 150) | async function getProjectByDomain(

FILE: utils/helpers.ts
  function cn (line 5) | function cn(...inputs: ClassValue[]) {
  function stripePrice (line 9) | function stripePrice(price: number) {
  function moneyFormat (line 13) | function moneyFormat(price: number) {
  function capitalize (line 25) | function capitalize(str: string) {
  function toReversed (line 35) | function toReversed<T>(arr: T[]) {
  function updateProject (line 40) | async function updateProject(
  function wait (line 59) | function wait(ms: number) {
  function isAipage (line 63) | function isAipage(host: string) {
Condensed preview — 105 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (200K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 534,
    "preview": "---\nname: Bug report\nabout: Create a bug report for AI landing page generator\ntitle: \"[BUG]\"\nlabels: ''\nassignees: ''\n\n-"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 504,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n# Feature Req"
  },
  {
    "path": ".gitignore",
    "chars": 380,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n.env\n/node_modules"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 2085,
    "preview": "# Code of Conduct\n\nAs contributors and maintainers of the AI Landing Page Generator project, we pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2259,
    "preview": "# Contributing to AI Landing Page Generator\n\nWe welcome and appreciate contributions from the community! By contributing"
  },
  {
    "path": "LICENSE",
    "chars": 1036,
    "preview": "MIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associate"
  },
  {
    "path": "README.md",
    "chars": 3995,
    "preview": "# AI Landing Page Generator\n\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n## Description\n\n!"
  },
  {
    "path": "app/(aipage)/layout.tsx",
    "chars": 278,
    "preview": "import \"@smastrom/react-rating/style.css\";\nimport { ReactNode } from \"react\";\nimport Header from \"@/components/Header\";\n"
  },
  {
    "path": "app/(aipage)/loading.tsx",
    "chars": 218,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function RootLoading() {\n  return (\n    <div c"
  },
  {
    "path": "app/(aipage)/not-found.tsx",
    "chars": 878,
    "preview": "import Link from \"next/link\";\nimport Button from \"@/components/Button\";\n\nexport default async function NotFound() {\n  re"
  },
  {
    "path": "app/(aipage)/page.tsx",
    "chars": 16313,
    "preview": "\"use client\";\nimport { useChat } from \"ai/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport Frame from"
  },
  {
    "path": "app/(aipage)/profile/invoices/loading.tsx",
    "chars": 255,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function InvoicesLoading() {\n  return (\n    <d"
  },
  {
    "path": "app/(aipage)/profile/invoices/page.tsx",
    "chars": 7284,
    "preview": "import { fetchInvoices } from \"@/utils/auth\";\nimport { moneyFormat, stripePrice } from \"@/utils/helpers\";\nimport Button "
  },
  {
    "path": "app/(aipage)/profile/layout.tsx",
    "chars": 241,
    "preview": "import { ReactNode } from \"react\";\nimport ProfileLayout from \"@/components/ProfileLayout\";\n\nexport default async functio"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/domains/layout.tsx",
    "chars": 1203,
    "preview": "\"use client\";\nimport AddDomainModal from \"@/components/AddDomainModal\";\nimport Button from \"@/components/Button\";\nimport"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/domains/page.tsx",
    "chars": 1129,
    "preview": "import DomainCard from \"@/components/DomainCard\";\nimport { toReversed } from \"@/utils/helpers\";\nimport { fetchProjectByI"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/integrations/page.tsx",
    "chars": 123,
    "preview": "export default function ProjectIntegrations({\n  params,\n}: {\n  params: {\n    id: string;\n  };\n}) {\n  return <div></div>;"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/layout.tsx",
    "chars": 512,
    "preview": "import { ReactNode } from \"react\";\nimport { fetchProjectById } from \"@/utils/auth\";\nimport { notFound } from \"next/navig"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/loading.tsx",
    "chars": 257,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function ProjectByIdLoading() {\n  return (\n   "
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/page.tsx",
    "chars": 3694,
    "preview": "import { fetchProjectById, fetchProjects } from \"@/utils/auth\";\nimport { SetProjects } from \"@/hooks/useProjectList\";\nim"
  },
  {
    "path": "app/(aipage)/profile/projects/[id]/settings/page.tsx",
    "chars": 3592,
    "preview": "\"use client\";\n\nimport useProject from \"@/hooks/useProject\";\nimport { FormEvent, useEffect, useState } from \"react\";\nimpo"
  },
  {
    "path": "app/(aipage)/profile/projects/loading.tsx",
    "chars": 255,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function ProjectsLoading() {\n  return (\n    <d"
  },
  {
    "path": "app/(aipage)/profile/projects/page.tsx",
    "chars": 1626,
    "preview": "import { cn } from \"@/utils/helpers\";\nimport Link from \"next/link\";\nimport Button from \"@/components/Button\";\nimport Lis"
  },
  {
    "path": "app/(aipage)/profile/settings/loading.tsx",
    "chars": 255,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function SettingsLoading() {\n  return (\n    <d"
  },
  {
    "path": "app/(aipage)/profile/settings/page.tsx",
    "chars": 3712,
    "preview": "\"use client\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport { useState, FormEvent } from \"react\";\nimport Loadi"
  },
  {
    "path": "app/(preview)/not-found.tsx",
    "chars": 878,
    "preview": "import Link from \"next/link\";\nimport Button from \"@/components/Button\";\n\nexport default async function NotFound() {\n  re"
  },
  {
    "path": "app/(preview)/profile/projects/[id]/preview/loading.tsx",
    "chars": 229,
    "preview": "import LoadingSpinner from \"@/components/loadingSpinner\";\n\nexport default function PreviewLoading() {\n  return (\n    <di"
  },
  {
    "path": "app/(preview)/profile/projects/[id]/preview/page.tsx",
    "chars": 396,
    "preview": "import { fetchProjectById } from \"@/utils/auth\";\nimport { notFound } from \"next/navigation\";\nimport HTMLPreview from \"@/"
  },
  {
    "path": "app/api/chat/route.ts",
    "chars": 5673,
    "preview": "import { Configuration, OpenAIApi } from \"openai-edge\";\nimport { Ratelimit } from \"@upstash/ratelimit\";\nimport redis fro"
  },
  {
    "path": "app/api/create-checkout-session/route.ts",
    "chars": 811,
    "preview": "import { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers\";\nimport altogic from \"@/utils/altogic"
  },
  {
    "path": "app/api/domain/check-domain/route.ts",
    "chars": 1968,
    "preview": "import { NextResponse } from \"next/server\";\n\nexport async function POST(req: Request) {\n  const { domain } = await req.j"
  },
  {
    "path": "app/api/domain/remove-domain/route.ts",
    "chars": 999,
    "preview": "import { NextResponse } from \"next/server\";\nimport altogic from \"@/utils/altogic\";\nimport { getSessionCookie } from \"@/u"
  },
  {
    "path": "app/api/domain/verify-domain/route.ts",
    "chars": 554,
    "preview": "import { NextResponse } from \"next/server\";\n\nexport async function POST(req: Request) {\n  const { domain } = await req.j"
  },
  {
    "path": "app/api/logout/route.ts",
    "chars": 164,
    "preview": "import { NextResponse } from \"next/server\";\nimport { logout } from \"@/utils/auth\";\n\nexport async function GET(req: Reque"
  },
  {
    "path": "app/api/message/route.ts",
    "chars": 823,
    "preview": "import altogic from \"@/utils/altogic\";\nimport { NextResponse } from \"next/server\";\nimport { cookies } from \"next/headers"
  },
  {
    "path": "app/api/og/route.tsx",
    "chars": 5512,
    "preview": "import { ImageResponse } from \"next/server\";\nimport { useState } from \"react\";\n// App router includes @vercel/og.\n// No "
  },
  {
    "path": "app/api/project/[id]/domain/route.ts",
    "chars": 1617,
    "preview": "import { NextResponse } from \"next/server\";\nimport altogic from \"@/utils/altogic\";\nimport { getSessionCookie } from \"@/u"
  },
  {
    "path": "app/api/project/[id]/route.ts",
    "chars": 561,
    "preview": "import { deleteProject } from \"@/utils/auth\";\nimport { NextResponse } from \"next/server\";\nimport { revalidatePath } from"
  },
  {
    "path": "app/api/project/route.ts",
    "chars": 495,
    "preview": "import { deleteProject, updateProjectName } from \"@/utils/auth\";\nimport { NextResponse } from \"next/server\";\n\nexport asy"
  },
  {
    "path": "app/api/user/route.ts",
    "chars": 565,
    "preview": "import { deleteUser, logout, updateUser } from \"@/utils/auth\";\nimport { NextResponse } from \"next/server\";\n\nexport async"
  },
  {
    "path": "app/auth-redirect/route.ts",
    "chars": 798,
    "preview": "import altogic from \"@/utils/altogic\";\nimport { NextResponse } from \"next/server\";\n\nexport async function GET(req: Reque"
  },
  {
    "path": "app/layout.tsx",
    "chars": 2261,
    "preview": "import AuthModal from \"@/components/AuthModal\";\nimport PricesModal from \"@/components/PricesModal\";\nimport { AuthProvide"
  },
  {
    "path": "app/not-found.tsx",
    "chars": 860,
    "preview": "import Link from \"next/link\";\nimport Button from \"@/components/Button\";\n\nexport default async function NotFound() {\n  re"
  },
  {
    "path": "components/AddDomainModal.tsx",
    "chars": 4626,
    "preview": "\"use client\";\nimport useSearchParams from \"@/hooks/useSearchParams\";\nimport Modal from \"@/components/Modal\";\nimport { Fo"
  },
  {
    "path": "components/AlertCircleFill.tsx",
    "chars": 529,
    "preview": "export default function AlertCircleFill({ className }: { className: string }) {\n  return (\n    <svg\n      fill=\"none\"\n  "
  },
  {
    "path": "components/AlertDialog.tsx",
    "chars": 4500,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nim"
  },
  {
    "path": "components/AuthModal.tsx",
    "chars": 3912,
    "preview": "\"use client\";\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport altogic from \"@/utils/altogic\";\nimp"
  },
  {
    "path": "components/Badge.tsx",
    "chars": 844,
    "preview": "import { cn } from \"@/utils/helpers\";\n\nexport type BadgeVariant = \"yellow\" | \"gray\" | \"red\" | \"black\" | \"green\";\n\nexport"
  },
  {
    "path": "components/BrowserWindow.tsx",
    "chars": 948,
    "preview": "import { ReactNode } from \"react\";\nimport { cn } from \"@/utils/helpers\";\n\ninterface BrowserWindowProps {\n  children: Rea"
  },
  {
    "path": "components/Button.tsx",
    "chars": 1653,
    "preview": "import { cn } from \"@/utils/helpers\";\nimport { ComponentPropsWithoutRef } from \"react\";\nimport * as React from \"react\";\n"
  },
  {
    "path": "components/Chart.tsx",
    "chars": 457,
    "preview": "export default function Chart({ className }: { className: string }) {\n  return (\n    <svg\n      fill=\"none\"\n      shapeR"
  },
  {
    "path": "components/CheckCircleFill.tsx",
    "chars": 516,
    "preview": "export default function CheckCircleFill({ className }: { className?: string }) {\n  return (\n    <svg\n      className={cl"
  },
  {
    "path": "components/ConfiguredSection.tsx",
    "chars": 6347,
    "preview": "import { useState } from \"react\";\nimport { DomainInfo } from \"@/types\";\n\ninterface ConfiguredSectionProps {\n  domainInfo"
  },
  {
    "path": "components/ConfirmDialog.tsx",
    "chars": 2686,
    "preview": "\"use client\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogTrig"
  },
  {
    "path": "components/DeleteAccountConfirmDialog.tsx",
    "chars": 1557,
    "preview": "\"use client\";\n\nimport Button from \"@/components/Button\";\nimport { useState } from \"react\";\nimport ConfirmDialog from \"@/"
  },
  {
    "path": "components/DeleteProjectConfirmDialog.tsx",
    "chars": 1258,
    "preview": "\"use client\";\n\nimport Button from \"@/components/Button\";\nimport { useState } from \"react\";\nimport ConfirmDialog from \"@/"
  },
  {
    "path": "components/Divider.tsx",
    "chars": 417,
    "preview": "export default function Divider({ className }: { className?: string }) {\n  return (\n    <svg\n      fill=\"none\"\n      sha"
  },
  {
    "path": "components/DomainCard.tsx",
    "chars": 2943,
    "preview": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport LoadingSpinner from \"@/components/loadingSpinner\";\nim"
  },
  {
    "path": "components/DomainConfiguration.tsx",
    "chars": 4878,
    "preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { getSubdomain } from \"@/utils/helpers\";\nimport { DomainVerifica"
  },
  {
    "path": "components/Drawer.tsx",
    "chars": 4400,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, t"
  },
  {
    "path": "components/HTMLPreview.tsx",
    "chars": 208,
    "preview": "interface HTMLPreviewProps {\n  html: string;\n}\n\nexport default function HTMLPreview({ html }: HTMLPreviewProps) {\n  retu"
  },
  {
    "path": "components/Header.tsx",
    "chars": 3068,
    "preview": "\"use client\";\nimport useSearchParams from \"@/hooks/useSearchParams\";\nimport { useAuth } from \"@/context/AuthContext\";\nim"
  },
  {
    "path": "components/IconMenu.tsx",
    "chars": 312,
    "preview": "import { ReactNode } from \"react\";\n\ninterface MenuIconProps {\n  icon: ReactNode;\n  text: string;\n}\n\nexport default funct"
  },
  {
    "path": "components/ListProjects.tsx",
    "chars": 4221,
    "preview": "\"use client\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport Link from \"next/link\";\nimport Badge, { BadgeVarian"
  },
  {
    "path": "components/LoadingIcon.tsx",
    "chars": 791,
    "preview": "export default function LoadingIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      version=\"1.1\"\n   "
  },
  {
    "path": "components/Logo.tsx",
    "chars": 406,
    "preview": "import Link from \"next/link\";\n\ninterface LogoProps {\n  href?: string;\n}\n\nexport default function Logo({ href }: LogoProp"
  },
  {
    "path": "components/LogoutIcon.tsx",
    "chars": 495,
    "preview": "export default function LogoutIcon({ className }: { className: string }) {\n  return (\n    <svg\n      fill=\"none\"\n      h"
  },
  {
    "path": "components/Modal.tsx",
    "chars": 1244,
    "preview": "import { XIcon } from \"lucide-react\";\nimport { MouseEvent, ReactNode, useRef } from \"react\";\nimport { cn } from \"@/utils"
  },
  {
    "path": "components/NavLink.tsx",
    "chars": 658,
    "preview": "\"use client\";\n\nimport { usePathname } from \"next/navigation\";\nimport Link, { LinkProps } from \"next/link\";\nimport { Reac"
  },
  {
    "path": "components/Popover.tsx",
    "chars": 760,
    "preview": "import * as PopoverPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Dispatch, ReactNode, SetStateAction } from \""
  },
  {
    "path": "components/PricesModal.tsx",
    "chars": 553,
    "preview": "\"use client\";\nimport useSearchParams from \"@/hooks/useSearchParams\";\nimport Modal from \"@/components/Modal\";\nimport { Pr"
  },
  {
    "path": "components/Product.tsx",
    "chars": 2503,
    "preview": "\"use client\";\nimport { cn, moneyFormat, stripePrice } from \"@/utils/helpers\";\nimport { useAuth } from \"@/context/AuthCon"
  },
  {
    "path": "components/Products.tsx",
    "chars": 1215,
    "preview": "\"use client\";\nimport { Product as ProductType } from \"@/types\";\nimport Product from \"@/components/Product\";\nimport { cn "
  },
  {
    "path": "components/ProfileLayout.tsx",
    "chars": 1276,
    "preview": "\"use client\";\nimport { ReactNode } from \"react\";\nimport ProfileMenu, { MenuItem } from \"@/components/ProfileMenu\";\nimpor"
  },
  {
    "path": "components/ProfileMenu.tsx",
    "chars": 1447,
    "preview": "\"use client\";\n\nimport { cn } from \"@/utils/helpers\";\nimport { useAuth } from \"@/context/AuthContext\";\nimport NavLink fro"
  },
  {
    "path": "components/ProjectDesign.tsx",
    "chars": 5501,
    "preview": "\"use client\";\n\nimport { Project } from \"@/types\";\nimport BrowserWindow from \"@/components/BrowserWindow\";\nimport { FormE"
  },
  {
    "path": "components/ProjectIcon.tsx",
    "chars": 1348,
    "preview": "import { cn } from \"@/utils/helpers\";\n\nexport default function ProjectIcon({ className }: { className?: string }) {\n  re"
  },
  {
    "path": "components/ProjectSelect.tsx",
    "chars": 3060,
    "preview": "import { ChevronsUpDown, PlusCircle, Check } from \"lucide-react\";\nimport Link from \"next/link\";\nimport Popover from \"@/c"
  },
  {
    "path": "components/RateModal.tsx",
    "chars": 3604,
    "preview": "\"use client\";\nimport useSearchParams from \"@/hooks/useSearchParams\";\nimport Modal from \"@/components/Modal\";\nimport { Ra"
  },
  {
    "path": "components/ShowRate.tsx",
    "chars": 529,
    "preview": "\"use client\";\nimport { Rating, Star } from \"@smastrom/react-rating\";\n\ninterface ShowRateProps {\n  rate: number;\n}\nexport"
  },
  {
    "path": "components/Switch.tsx",
    "chars": 1290,
    "preview": "\"use client\";\n\nimport { Dispatch, SetStateAction } from \"react\";\n// @ts-ignore\nimport * as SwitchPrimitive from \"@radix-"
  },
  {
    "path": "components/UserDropdown.tsx",
    "chars": 3329,
    "preview": "\"use client\";\nimport { Gem, ReceiptIcon, Settings } from \"lucide-react\";\nimport Popover from \"@/components/Popover\";\nimp"
  },
  {
    "path": "components/XCircleFill.tsx",
    "chars": 525,
    "preview": "export default function XCircleFill({ className }: { className?: string }) {\n  return (\n    <svg\n      className={classN"
  },
  {
    "path": "components/customDropdown.tsx",
    "chars": 1928,
    "preview": "import Image from \"next/image\";\nimport { useState } from \"react\";\n\ntype TechIcon = {\n  name: string;\n  url: string;\n};\n\n"
  },
  {
    "path": "components/loadingSpinner.module.css",
    "chars": 1583,
    "preview": ".spinner {\n  --spinner-color: currentColor;\n  position: relative;\n  top: 50%;\n  left: 50%;\n}\n.spinner div {\n  animation:"
  },
  {
    "path": "components/loadingSpinner.tsx",
    "chars": 461,
    "preview": "import { cn } from \"@/utils/helpers\";\nimport styles from \"./loadingSpinner.module.css\";\n\nexport default function Loading"
  },
  {
    "path": "components/tweetButton.tsx",
    "chars": 2363,
    "preview": "import { useState } from \"react\";\nimport Head from \"next/head\";\n\nconst TweetButton = () => {\n  // Array of possible twee"
  },
  {
    "path": "context/AuthContext.tsx",
    "chars": 914,
    "preview": "\"use client\";\n\nimport { createContext, ReactNode, useContext, useState } from \"react\";\nimport { User } from \"@/types\";\n\n"
  },
  {
    "path": "custom.css",
    "chars": 212,
    "preview": "@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes blin"
  },
  {
    "path": "hooks/useProject.tsx",
    "chars": 1580,
    "preview": "\"use client\";\n\nimport { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { Domain, Project"
  },
  {
    "path": "hooks/useProjectList.tsx",
    "chars": 1050,
    "preview": "\"use client\";\n\nimport { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { Project } from "
  },
  {
    "path": "hooks/useSearchParams.ts",
    "chars": 876,
    "preview": "\"use client\";\nimport { useSearchParams as useNextSearchParams } from \"next/navigation\";\nimport { usePathname, useRouter "
  },
  {
    "path": "next.config.js",
    "chars": 92,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "chars": 1643,
    "preview": "{\n  \"name\": \"next-openai\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev -H 0.0.0.0\",\n   "
  },
  {
    "path": "postcss.config.js",
    "chars": 80,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n}\n"
  },
  {
    "path": "public/site.webmanifest",
    "chars": 372,
    "preview": "{\n  \"name\": \"\",\n  \"short_name\": \"\",\n  \"icons\": [\n    {\n      \"src\": \"assets/android-chrome-192x192.png\",\n      \"sizes\": "
  },
  {
    "path": "styles/custom.css",
    "chars": 259,
    "preview": "@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes blin"
  },
  {
    "path": "styles/globals.css",
    "chars": 1318,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n.profile-page {\n  min-height: 100dvh;\n}\n\n@keyfra"
  },
  {
    "path": "tailwind.config.js",
    "chars": 709,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    "
  },
  {
    "path": "tsconfig.json",
    "chars": 638,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  },
  {
    "path": "types/index.ts",
    "chars": 2848,
    "preview": "import type { User as AltogicUser } from \"altogic\";\n\nexport interface User extends AltogicUser {\n  credits: number;\n}\n\ne"
  },
  {
    "path": "utils/altogic.ts",
    "chars": 467,
    "preview": "import { createClient } from \"altogic\";\n\nconst clientKey = process.env.NEXT_PUBLIC_ALTOGIC_CLIENT_KEY;\nconst apiBaseURL "
  },
  {
    "path": "utils/auth.ts",
    "chars": 4116,
    "preview": "import { cookies, headers } from \"next/headers\";\nimport { Invoice, Product, Project, User } from \"@/types\";\nimport altog"
  },
  {
    "path": "utils/helpers.ts",
    "chars": 1659,
    "preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\nimport { Project } from \"@/types"
  },
  {
    "path": "utils/redis.ts",
    "chars": 305,
    "preview": "import { Redis } from \"@upstash/redis\";\n\nconst redis =\n  !!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_R"
  }
]

About this extraction

This page contains the full source code of the zinedkaloc/aipage.dev GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 105 files (179.8 KB), approximately 47.9k tokens, and a symbol index with 139 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!