├── .env.example ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_zh.md ├── SECURITY.md ├── analysis.jpg ├── apps ├── .gitignore └── nextjs │ ├── .eslintignore │ ├── .prettierignore │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── favicon-16x16.ico │ ├── favicon.ico │ └── logo.svg │ ├── src │ ├── app │ │ ├── (index) │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── robots.ts │ ├── components │ │ ├── main-nav.tsx │ │ ├── mobile-nav.tsx │ │ ├── mode-toggle.tsx │ │ ├── navbar.tsx │ │ ├── site-footer.tsx │ │ ├── tailwind-indicator.tsx │ │ └── theme-provider.tsx │ ├── config │ │ ├── site.ts │ │ └── ui │ │ │ └── marketing.ts │ ├── env.mjs │ ├── hooks │ │ ├── use-lock-body.ts │ │ └── use-scroll.ts │ ├── styles │ │ ├── calsans.ttf │ │ ├── fonts │ │ │ ├── CalSans-SemiBold.ttf │ │ │ ├── CalSans-SemiBold.woff │ │ │ ├── CalSans-SemiBold.woff2 │ │ │ ├── Inter-Bold.ttf │ │ │ └── Inter-Regular.ttf │ │ ├── globals.css │ │ └── theme │ │ │ └── default.css │ └── types │ │ └── index.d.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── deploy1.png ├── deploy2.png ├── img.png ├── img_1.png ├── package.json ├── packages ├── common │ ├── .eslintignore │ ├── package.json │ ├── src │ │ ├── config │ │ │ └── site.ts │ │ ├── email.ts │ │ ├── emails │ │ │ └── magic-link-email.tsx │ │ ├── env.mjs │ │ ├── index.ts │ │ └── subscriptions.ts │ └── tsconfig.json └── ui │ ├── .eslintignore │ ├── package.json │ ├── src │ ├── 3d-card.tsx │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── animated-tooltip.tsx │ ├── avatar.tsx │ ├── background-beams.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── callout.tsx │ ├── card-hover-effect.tsx │ ├── card-skeleton.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── data-table.tsx │ ├── data │ │ └── globe.json │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── globe.tsx │ ├── icons.tsx │ ├── index.ts │ ├── infinite-moving-cards.tsx │ ├── input.tsx │ ├── label.tsx │ ├── meteors.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── sparkles.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── text-generate-effect.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── typewriter-effect.tsx │ ├── use-toast.tsx │ └── utils │ │ └── cn.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── saasfly-logo.svg ├── tooling ├── eslint-config │ ├── base.js │ ├── nextjs.js │ ├── package.json │ ├── react.js │ └── tsconfig.json ├── prettier-config │ ├── index.mjs │ ├── package.json │ └── tsconfig.json ├── tailwind-config │ ├── .eslintignore │ ├── index.ts │ ├── package.json │ ├── postcss.js │ └── tsconfig.json └── typescript-config │ ├── base.json │ └── package.json └── turbo.json /.env.example: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # App 3 | # ----------------------------------------------------------------------------- 4 | NEXT_PUBLIC_APP_URL='localhost:3000' 5 | 6 | NEXT_PUBLIC_GOOGLE_FORM_URL='' 7 | NEXT_PUBLIC_GOOGLE_FORM_EMAIL='' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | next-env.d.ts 15 | 16 | # expo 17 | .expo/ 18 | dist/ 19 | expo-env.d.ts 20 | apps/expo/.gitignore 21 | 22 | # production 23 | build 24 | 25 | # misc 26 | .DS_Store 27 | *.pem 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | .pnpm-debug.log* 34 | 35 | # local env files 36 | .env.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | 44 | # turbo 45 | .turbo 46 | 47 | yarn.lock 48 | package-lock.json 49 | bun.lockb 50 | 51 | .idea/ 52 | 53 | .env 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contract@nextify.ltd. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Can I create a pull request for saasfly? 2 | 3 | Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know if it will be merged or not. 4 | 5 | Here are some references: 6 | 7 | ### ✅ Usually accepted 8 | 9 | - Bug fix 10 | - Security fix 11 | - Adding notification providers 12 | - Adding new language keys 13 | 14 | ### ⚠️ Discussion required 15 | 16 | - Large pull requests 17 | - New features 18 | 19 | ### ❌ Won't be merged 20 | 21 | - Do not pass the auto-test(we dont have auto-test now) 22 | - Any breaking changes 23 | - Duplicated pull requests 24 | - Buggy 25 | - UI/UX is not close to saasfly 26 | - Modifications or deletions of existing logic without a valid reason. 27 | - Adding functions that is completely out of scope 28 | - Converting existing code into other programming languages 29 | - Unnecessarily large code changes that are hard to review and cause conflicts with other PRs. 30 | 31 | The above cases may not cover all possible situations. 32 | 33 | If your pull request does not meet my expectations, I will reject it, no matter how much time you spent on it. Therefore, it is essential to have a discussion beforehand. 34 | 35 | I will assign your pull request to a [milestone](https://github.com/saasfly/saasfly/milestones), if I plan to review and merge it. 36 | 37 | Also, please don't rush or ask for an ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests. 38 | 39 | ### Recommended Pull Request Guideline 40 | 41 | Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended. 42 | 43 | 1. Fork the project 44 | 2. Clone your fork repo to local 45 | 3. Create a new branch 46 | 4. Create an empty commit: `git commit -m "" --allow-empty` 47 | 5. Push to your fork repo 48 | 6. Prepare a pull request: https://github.com/saasfly/saasfly/compare 49 | 7. Write a proper description. You can mention @tianzx in it, so @tianzx will get the notification. 50 | 8. Create your pull request as a Draft 51 | 9. Wait for the discussion -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 saasfly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Empower Your AI Startup with a Waitlist 2 | [![Chinese](https://img.shields.io/badge/-Chinese-red.svg)](README_zh.md) 3 | 4 | Kickstart your AI startup journey with a waitlist instead of a SaaS template. Once you've completed your waitlist, check out our AI-powered Next.js SaaS template [Saasfly](https://github.com/saasfly/saasfly). 5 | 6 | ## Why Choose Saasfly Waitlist? 7 | 8 | 1. **High-Performance Static Pages**: Leveraging Next.js's static site generation (SSG) capabilities, deliver blazing-fast static pages while enjoying Next.js's unparalleled development experience. 9 | 2. **Zero Hosting Costs**: Deploy static pages on Cloudflare completely free of charge, eliminating hosting costs and concerns. 10 | 3. **Dynamic Data Collection**: Harness the power of Google Forms to collect form data, empowering static pages with dynamic capabilities for seamless data collection and user interaction. 11 | 4. **Data Ownership and Privacy**: Maintain full control and ownership of your data without relying on third-party SaaS providers, ensuring data privacy and autonomy. 12 | 5. **Powerful Data Analysis**: Utilize a simple yet effective local-first analysis [service](https://excel.saasfly.io/) to gain valuable insights from your collected data. This lightweight tool empowers you to make data-driven decisions without the need for complex and costly BI systems, while keeping your data secure and under your control. 13 | 14 | > **Nextify** offers complete enterprise-grade SaaS solutions. If you're interested in discussing your project or just want to connect with us, feel free to reach out at contact@nextify.ltd. 15 | 16 | > ❤️ We **provide free technical support and deployment services for non-profit organizations**. 17 | 18 | > 🙌 **All profits we gain from open-source projects will be entirely used to support open-source initiatives and charitable causes**. 19 | 20 | ## Introduction 21 | 22 | Our goal is to leverage Next.js's static site generation (SSG) capabilities to build a high-performance waiting list page, and use Google Forms as the backend to receive user-submitted email information. With this approach, we can quickly and cost-effectively create a waiting list page without having to write any backend code for data collection. 23 | 24 | ## ⚡ Live Demo 25 | 26 | Try it out for yourself! 27 | 28 | Demo URL: https://waitlist.saasfly.io 29 | 30 | ## ⚠️ Attention 31 | For security reasons, we forcefully require users to log in before submitting the Google Form. You can turn off this requirement in the test environment or if your use case doesn't need this level of security. 32 | 33 | ![Google Form Login](img.png) 34 | 35 | ## 🚀 Quick Start 36 | 37 | ### 📋 Prerequisites 38 | 39 | Before getting started, make sure you have the following installed: 40 | 41 | 1. Bun & Node.js & Git 42 | 43 | - Linux 44 | 45 | ```bash 46 | curl -sL https://gist.github.com/tianzx/874662fb204d32390bc2f2e9e4d2df0a/raw -o ~/downloaded_script.sh && chmod +x ~/downloaded_script.sh && source ~/downloaded_script.sh 47 | ``` 48 | 49 | - MacOS 50 | 51 | ```bash 52 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 53 | brew install git 54 | brew install oven-sh/bun/bun 55 | brew install nvm 56 | ``` 57 | 58 | ### Installation 59 | 60 | To start using this template, follow these steps: 61 | 62 | 1. Clone the repository: 63 | 64 | ```bash 65 | git clone https://github.com/saasfly/waitlist.git 66 | cd waitlist 67 | bun install 68 | ``` 69 | 70 | 2. Set up environment variables: 71 | 72 | ```bash 73 | cp .env.example .env.local 74 | ``` 75 | See our [documentation](https://document.saasfly.io/usage/waitlist/google-forms/) to learn how to get your own variables. 76 | 77 | 3. Run the development server: 78 | 79 | ```bash 80 | bun run dev 81 | ``` 82 | 83 | 4. Open http://localhost:3000 in your browser to see the result. 84 | 85 | ## ⭐ Features 86 | 87 | - **🐭 Framework**: Next.js - The React Framework for the Web (using App Directory) 88 | - **🐮 Platform**: Cloudflare 89 | - **🐯 Backend**: Google Forms API 90 | 91 | ## 🚀 Deployment 92 | To deploy your waitlist page, create a Cloudflare Pages application: 93 | 94 | 1. Add your own variables 95 | 96 | ![Cloudflare Pages Setup](deploy1.png) 97 | 98 | 2. Deploy config 99 | 100 | ![Cloudflare Pages Deployment](deploy2.png) 101 | 102 | ## 🤔 Analysis 103 | You don't need a very complex BI system. I build a simple local-first excel analysis [service](https://excel.saasfly.io) to help you. 104 | ![analysis.jpg](analysis.jpg) 105 | 106 | ## 📜 License 107 | This project is licensed under the MIT License. For more information, see the [LICENSE](LICENSE) file. 108 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 用Waitlist开启你的 AI 创业 2 | 3 | 用Waitlist开启你的而不是 SaaS 模板开启你的 AI 创业之旅。一旦你完成了Waitlist,可以查看我们的 AI 驱动的 Next.js SaaS 模板 [Saasfly](https://github.com/saasfly/saasfly)。 4 | 5 | ## 为什么选择 Saasfly Waitlist? 6 | 7 | 1. **高性能静态页面**: 利用 Next.js 的静态站点生成(SSG)能力,提供极快的静态页面,同时享受 Next.js 无与伦比的开发体验。 8 | 2. **零托管成本**: 完全免费地在 Cloudflare 上部署静态页面,消除托管成本和烦恼。 9 | 3. **动态数据收集**: 利用 Google Forms 的能力收集表单数据,为静态页面赋予动态功能,实现无缝的数据收集和用户交互。 10 | 4. **数据所有权和隐私**: 完全控制和拥有你的数据,无需依赖第三方 SaaS 提供商,确保数据隐私和自主权。 11 | 5. **强大的数据分析**:利用简单而有效的本地优先分析[服务](https://excel.saasfly.io/),从您收集的数据中获得有价值的见解。这个轻量级工具使您能够在无需复杂昂贵的BI系统的情况下做出数据驱动的决策,同时保证您的数据安全并在您的控制之下。 12 | 13 | > **Nextify** 提供完整的企业级 SaaS 解决方案。如果你有兴趣讨论你的项目或者只是想与我们联系,欢迎随时通过 contact@nextify.ltd 与我们联系。 14 | 15 | > ❤️ 我们**为非营利组织提供免费的技术支持和部署服务**。 16 | 17 | > 🙌 我们从开源项目中获得的**所有利润将全部用于支持开源计划和慈善事业**。 18 | 19 | ## 简介 20 | 21 | 我们的目标是利用 Next.js 的静态站点生成(SSG)功能构建高性能的等候页面,并使用 Google Forms 作为后端接收用户提交的电子邮件信息。 22 | 通过这种方法,我们可以快速、经济高效地创建等候页面,而无需编写任何后端代码进行数据收集。 23 | 24 | ## ⚡ 在线演示 25 | 26 | 试试吧! 27 | 28 | 演示网址: https://waitlist.saasfly.io 29 | 30 | ## ⚠️ 注意 31 | 出于安全原因,我们强制要求用户在提交 Google 表单之前登录。如果你的使用场景不需要这种级别的安全性,可以关闭此要求。 32 | 33 | ![Google 表单登录](img.png) 34 | 35 | ## 🚀 快速开始 36 | 37 | ### 📋 先决条件 38 | 39 | 在开始之前,请确保你已经安装了以下内容: 40 | 41 | 1. Bun & Node.js & Git 42 | 43 | - Linux 44 | 45 | ```bash 46 | curl -sL https://gist.github.com/tianzx/874662fb204d32390bc2f2e9e4d2df0a/raw -o ~/downloaded_script.sh && chmod +x ~/downloaded_script.sh && source ~/downloaded_script.sh 47 | ``` 48 | 49 | - MacOS 50 | 51 | ```bash 52 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 53 | brew install git 54 | brew install oven-sh/bun/bun 55 | brew install nvm 56 | ``` 57 | 58 | ### 安装 59 | 60 | 要开始使用此模板,请按照以下步骤操作: 61 | 62 | 1. 克隆仓库: 63 | 64 | ```bash 65 | git clone https://github.com/saasfly/waitlist.git 66 | cd waitlist 67 | bun install 68 | ``` 69 | 70 | 2. 设置环境变量: 71 | 72 | ```bash 73 | cp .env.example .env.local 74 | ``` 75 | 请参阅我们的[文档](https://document.saasfly.io/usage/waitlist/google-forms/)以了解如何获取你自己的变量。 76 | 77 | 3. 运行开发服务器: 78 | 79 | ```bash 80 | bun run dev 81 | ``` 82 | 83 | 4. 在浏览器中打开 http://localhost:3000 查看结果。 84 | 85 | ## ⭐ 特性 86 | 87 | - **🐭 框架**: Next.js - 用于 Web 的 React 框架(使用 App 目录) 88 | - **🐮 平台**: Cloudflare 89 | - **🐯 后端**: Google Forms API 90 | 91 | ## 🚀 部署 92 | 要部署你的Waitlist,请创建一个 Cloudflare Pages 应用程序: 93 | 94 | 1. 添加你自己的变量 95 | 96 | ![Cloudflare Pages 设置](deploy1.png) 97 | 98 | 2. 部署配置 99 | 100 | ![Cloudflare Pages 部署](deploy2.png) 101 | 102 | ## 🤔 数据分析 103 | 你不需要一个非常复杂的BI系统。我构建了一个简单的本地优先的Excel分析[服务](https://excel.saasfly.io) 来帮助你。![analysis.jpg](analysis.jpg) 104 | 105 | ## 📜 许可证 106 | 本项目采用 MIT 许可证。有关更多信息,请参阅[许可证](LICENSE)文件。 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | We greatly value the security community's efforts in helping keep our project safe. If you've discovered a security vulnerability, your responsible disclosure is crucial to us. Here's how you can report it: 6 | 7 | 1. **Contact Method**: Please send an email to [contact@nextify.ltd](mailto:contact@nextify.ltd). 8 | 2. **Email Subject**: Please use a concise yet descriptive subject, such as "Security Vulnerability Discovered". 9 | 3. **Vulnerability Details**: Provide a comprehensive description of the vulnerability. Include reproduction steps and any other information that might help us effectively understand and resolve the issue. 10 | 4. **Proof of Concept**: If possible, please attach any proof of concept or sample code. Please ensure that your research does not involve destructive testing or violate any laws. 11 | 5. **Response Time**: We will acknowledge receipt of your report within [e.g., 24 hours] and will keep you informed of our progress. 12 | 6. **Investigation and Remediation**: Our team will promptly investigate and work on resolving the issue. We will maintain communication with you throughout the process. 13 | 7. **Disclosure Policy**: Please refrain from public disclosure until we have mitigated the vulnerability. We will collaborate with you to determine an appropriate disclosure timeline based on the severity of the issue. 14 | 15 | We appreciate your contributions to the security of our project. Contributors who help improve our security may be publicly acknowledged (with consent). 16 | 17 | Note: Our security policy may be updated periodically. -------------------------------------------------------------------------------- /analysis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/analysis.jpg -------------------------------------------------------------------------------- /apps/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/.gitignore -------------------------------------------------------------------------------- /apps/nextjs/.eslintignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | src/env.mjs -------------------------------------------------------------------------------- /apps/nextjs/.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | -------------------------------------------------------------------------------- /apps/nextjs/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import "./src/env.mjs"; 3 | 4 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs")); 5 | 6 | /** @type {import("next").NextConfig} */ 7 | const config = { 8 | reactStrictMode: true, 9 | /** Enables hot reloading for local packages without a build step */ 10 | transpilePackages: ["@saasfly/common", "@saasfly/ui"], 11 | pageExtensions: ["ts", "tsx"], 12 | /** We already do linting and typechecking as separate tasks in CI */ 13 | eslint: { ignoreDuringBuilds: true }, 14 | typescript: { ignoreBuildErrors: true }, 15 | output: "export", 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /apps/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saasfly/nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "bun next build", 7 | "build-local": "bun with-env next build", 8 | "clean": "git clean -xdf .next .turbo node_modules", 9 | "dev": "bun with-env next dev", 10 | "lint": " bun with-env next lint --quiet", 11 | "format": "prettier --write '**/*.{js,cjs,mjs,ts,tsx,md,json}' --ignore-path .prettierignore", 12 | "start": "bun with-env next start", 13 | "with-env": "dotenv -e ../../.env.local --" 14 | }, 15 | "dependencies": { 16 | "@hookform/resolvers": "3.3.4", 17 | "@t3-oss/env-nextjs": "0.9.2", 18 | "date-fns": "3.6.0", 19 | "framer-motion": "11.0.25", 20 | "negotiator": "0.6.3", 21 | "next": "14.2.2", 22 | "next-themes": "0.3.0", 23 | "react": "18.2.0", 24 | "react-day-picker": "8.10.0", 25 | "react-dom": "18.2.0", 26 | "react-image-crop": "11.0.5", 27 | "react-wrap-balancer": "1.1.0", 28 | "recharts": "2.12.4", 29 | "superjson": "2.2.1", 30 | "tailwindcss-animate": "1.0.7", 31 | "zod": "3.22.4", 32 | "zustand": "4.5.2", 33 | "vaul": "0.9.0", 34 | "sharp": "0.33.3" 35 | }, 36 | "devDependencies": { 37 | "vercel": "34.1.1", 38 | "@saasfly/eslint-config": "workspace:*", 39 | "@saasfly/prettier-config": "workspace:*", 40 | "@saasfly/tailwind-config": "workspace:*", 41 | "@saasfly/typescript-config": "workspace:*", 42 | "@types/negotiator": "0.6.3", 43 | "@types/node": "20.12.7", 44 | "@types/react": "18.2.77", 45 | "@types/react-dom": "18.2.25", 46 | "autoprefixer": "10.4.19", 47 | "dotenv-cli": "7.4.1", 48 | "eslint": "8.57.0", 49 | "prettier": "3.2.5", 50 | "tailwindcss": "3.4.3", 51 | "typescript": "5.4.5" 52 | }, 53 | "eslintConfig": { 54 | "root": true, 55 | "extends": [ 56 | "@saasfly/eslint-config/base", 57 | "@saasfly/eslint-config/nextjs", 58 | "@saasfly/eslint-config/react" 59 | ] 60 | }, 61 | "prettier": "@saasfly/prettier-config" 62 | } 63 | -------------------------------------------------------------------------------- /apps/nextjs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@saasfly/tailwind-config/postcss"); 2 | -------------------------------------------------------------------------------- /apps/nextjs/public/favicon-16x16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/public/favicon-16x16.ico -------------------------------------------------------------------------------- /apps/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /apps/nextjs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 82 | 89 | 96 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(index)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { NavBar } from "~/components/navbar"; 4 | import { SiteFooter } from "~/components/site-footer"; 5 | import { getMarketingConfig } from "~/config/ui/marketing"; 6 | 7 | export default function MarketingLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 |
14 | 15 | 20 | 21 |
{children}
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/(index)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { BackgroundBeams } from "@saasfly/ui/background-beams"; 3 | import { useState } from "react"; 4 | import { env } from "~/env.mjs"; 5 | 6 | export default function IndexPage() { 7 | const [email, setEmail] = useState(""); 8 | const [emailError, setEmailError] = useState(""); 9 | 10 | const handleEmailChange = (e) => { 11 | const newEmail = e.target.value; 12 | setEmail(newEmail); 13 | setEmailError(validateEmail(newEmail) ? "" : "Invalid email"); 14 | }; 15 | 16 | const validateEmail = (email) => { 17 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 18 | return emailRegex.test(email); 19 | }; 20 | 21 | return ( 22 |
23 |
24 |
25 |
26 |

27 | 🔥 Amazing SaaS Resources & Services! 28 |

29 |

30 | Join The Waitlist for{" "} 31 | 32 | Saasfly 33 | {" "} 34 | Today! 35 |

36 |

37 | We are exploring paths to open-source commercialization. If you want 38 | to promote your open-source or SaaS service, please contact us. 39 |

40 |
45 |
46 |
47 | 57 | {emailError && ( 58 |

{emailError}

59 | )} 60 |
61 | 67 |
68 |
69 |
70 | Please make sure your Google account is logged in. 71 |
72 |
73 | 77 | More Details → 78 | 79 |
80 |
81 |
82 |
83 | 84 |
85 | ); 86 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter as FontSans } from "next/font/google"; 2 | import localFont from "next/font/local"; 3 | 4 | import "~/styles/globals.css"; 5 | 6 | import { cn } from "@saasfly/ui"; 7 | import { Toaster } from "@saasfly/ui/toaster"; 8 | 9 | import { TailwindIndicator } from "~/components/tailwind-indicator"; 10 | import { ThemeProvider } from "~/components/theme-provider"; 11 | import { siteConfig } from "~/config/site"; 12 | 13 | const fontSans = FontSans({ 14 | subsets: ["latin"], 15 | variable: "--font-sans", 16 | }); 17 | 18 | // Font files can be colocated inside of `pages` 19 | const fontHeading = localFont({ 20 | src: "../styles/fonts/CalSans-SemiBold.woff2", 21 | variable: "--font-heading", 22 | }); 23 | 24 | export const metadata = { 25 | title: { 26 | default: siteConfig.name, 27 | template: `%s | ${siteConfig.name}`, 28 | }, 29 | description: siteConfig.description, 30 | keywords: [ 31 | "Next.js", 32 | "Shadcn ui", 33 | "Sass", 34 | "Fast ", 35 | "Simple ", 36 | "Easy", 37 | "Cloud Native", 38 | ], 39 | authors: [ 40 | { 41 | name: "Saasfly", 42 | }, 43 | ], 44 | creator: "Saasfly", 45 | openGraph: { 46 | type: "website", 47 | locale: "en_US", 48 | url: siteConfig.url, 49 | title: siteConfig.name, 50 | description: siteConfig.description, 51 | siteName: siteConfig.name, 52 | }, 53 | icons: { 54 | icon: "/favicon.ico", 55 | shortcut: "/favicon-16x16.ico", 56 | // apple: "/apple-touch-icon.png", 57 | }, 58 | metadataBase: new URL("https://waitlist.saasfly.io/"), 59 | // manifest: `${siteConfig.url}/site.webmanifest`, 60 | }; 61 | 62 | export default function RootLayout({ 63 | children, 64 | }: { 65 | children: React.ReactNode; 66 | }) { 67 | return ( 68 | 69 | 70 | 77 | 82 | {children} 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | import { useSelectedLayoutSegment } from "next/navigation"; 6 | import { useTheme } from "next-themes"; 7 | 8 | import { cn } from "@saasfly/ui"; 9 | import * as Icons from "@saasfly/ui/icons"; 10 | 11 | import { MobileNav } from "~/components/mobile-nav"; 12 | import type { MainNavItem } from "~/types"; 13 | 14 | interface MainNavProps { 15 | items?: MainNavItem[]; 16 | children?: React.ReactNode; 17 | } 18 | 19 | export function MainNav({ items, children }: MainNavProps) { 20 | const segment = useSelectedLayoutSegment(); 21 | const [showMobileMenu, setShowMobileMenu] = React.useState(false); 22 | const { theme } = useTheme(); 23 | const [mounted, setMounted] = React.useState(false); 24 | React.useEffect(() => { 25 | setMounted(true); 26 | }, []); 27 | const isDarkMode = theme === "dark"; 28 | const logoColor = isDarkMode ? "#FFFFFF" : "#000000"; 29 | if (!mounted) return null; 30 | 31 | return ( 32 |
33 |
34 | 35 | 36 | 37 | {items?.length ? ( 38 | 55 | ) : null} 56 |
57 | 67 | {showMobileMenu && items && ( 68 | {children} 69 | )} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { useTheme } from "next-themes"; 4 | 5 | import { cn } from "@saasfly/ui"; 6 | import * as Icons from "@saasfly/ui/icons"; 7 | 8 | import { siteConfig } from "~/config/site"; 9 | import { useLockBody } from "~/hooks/use-lock-body"; 10 | import type { MainNavItem } from "~/types"; 11 | 12 | interface MobileNavProps { 13 | items: MainNavItem[]; 14 | children?: React.ReactNode; 15 | } 16 | 17 | export function MobileNav({ items, children }: MobileNavProps) { 18 | useLockBody(); 19 | const { theme } = useTheme(); 20 | const [mounted, setMounted] = React.useState(false); 21 | React.useEffect(() => { 22 | setMounted(true); 23 | }, []); 24 | const isDarkMode = theme === "dark"; 25 | const logoColor = isDarkMode ? "#FFFFFF" : "#000000"; 26 | if (!mounted) return null; 27 | return ( 28 |
33 |
34 | 35 | 36 | {siteConfig.name} 37 | 38 | 52 | {children} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@saasfly/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@saasfly/ui/dropdown-menu"; 13 | import * as Icons from "@saasfly/ui/icons"; 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | 30 | Light 31 | 32 | setTheme("dark")}> 33 | 34 | Dark 35 | 36 | setTheme("system")}> 37 | 38 | System 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MainNav } from "~/components/main-nav"; 4 | import useScroll from "~/hooks/use-scroll"; 5 | import type { MainNavItem } from "~/types"; 6 | 7 | interface NavBarProps { 8 | items?: MainNavItem[]; 9 | children?: React.ReactNode; 10 | rightElements?: React.ReactNode; 11 | scroll?: boolean; 12 | dropdown: Record; 13 | } 14 | 15 | export function NavBar({ 16 | items, 17 | children, 18 | rightElements, 19 | scroll = false, 20 | }: NavBarProps) { 21 | const scrolled = useScroll(50); 22 | return ( 23 |
28 |
29 | {children} 30 | 31 |
{rightElements}
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@saasfly/ui"; 4 | import * as Icons from "@saasfly/ui/icons"; 5 | 6 | import { ModeToggle } from "~/components/mode-toggle"; 7 | 8 | function getCopyrightText() { 9 | const currentYear = new Date().getFullYear(); 10 | const copyrightTemplate = String( 11 | "Copyright © ${currentYear} Nextify Limited. All rights reserved.", 12 | ); 13 | return copyrightTemplate?.replace("${currentYear}", String(currentYear)); 14 | } 15 | 16 | export function SiteFooter({ className }: { className?: string }) { 17 | return ( 18 |
19 |
20 |
21 | 22 |

23 | {getCopyrightText()} 24 |

25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
8 | sm 9 |
10 |
md
11 |
lg
12 |
xl
13 |
2xl
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/nextjs/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemeProvider } from "next-themes"; 4 | 5 | export const ThemeProvider = NextThemeProvider; 6 | -------------------------------------------------------------------------------- /apps/nextjs/src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Saasfly", 3 | description: "We provide an easier way to build saas service in production", 4 | url: "https://github.com/saasfly/saasfly", 5 | ogImage: "", 6 | links: { 7 | github: "https://github.com/saasfly/saasfly", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/nextjs/src/config/ui/marketing.ts: -------------------------------------------------------------------------------- 1 | export const getMarketingConfig = () => { 2 | return { 3 | mainNav: [ 4 | { 5 | title: "Github", 6 | href: `https://github.com/saasfly/saasfly`, 7 | }, 8 | ], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/nextjs/src/env.mjs: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { createEnv } from "@t3-oss/env-nextjs"; 3 | import { z } from "zod"; 4 | 5 | export const env = createEnv({ 6 | server: {}, 7 | client: { 8 | NEXT_PUBLIC_APP_URL: z.string().optional(), 9 | NEXT_PUBLIC_GOOGLE_FORM_URL: z.string(), 10 | NEXT_PUBLIC_GOOGLE_FORM_EMAIL: z.string(), 11 | }, 12 | runtimeEnv: { 13 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 14 | NEXT_PUBLIC_GOOGLE_FORM_URL: process.env.NEXT_PUBLIC_GOOGLE_FORM_URL, 15 | NEXT_PUBLIC_GOOGLE_FORM_EMAIL: process.env.NEXT_PUBLIC_GOOGLE_FORM_EMAIL, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /apps/nextjs/src/hooks/use-lock-body.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // @see https://usehooks.com/useLockBodyScroll. 4 | export function useLockBody() { 5 | React.useLayoutEffect((): (() => void) => { 6 | const originalStyle: string = window.getComputedStyle( 7 | document.body, 8 | ).overflow; 9 | document.body.style.overflow = "hidden"; 10 | return () => (document.body.style.overflow = originalStyle); 11 | }, []); 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs/src/hooks/use-scroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export default function useScroll(threshold: number) { 4 | const [scrolled, setScrolled] = useState(false); 5 | 6 | const onScroll = useCallback(() => { 7 | setScrolled(window.pageYOffset > threshold); 8 | }, [threshold]); 9 | 10 | useEffect(() => { 11 | window.addEventListener("scroll", onScroll); 12 | return () => window.removeEventListener("scroll", onScroll); 13 | }, [onScroll]); 14 | 15 | return scrolled; 16 | } 17 | -------------------------------------------------------------------------------- /apps/nextjs/src/styles/calsans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/calsans.ttf -------------------------------------------------------------------------------- /apps/nextjs/src/styles/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff -------------------------------------------------------------------------------- /apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /apps/nextjs/src/styles/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /apps/nextjs/src/styles/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/apps/nextjs/src/styles/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /apps/nextjs/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0deg 0% 100%; 8 | --foreground: 222.2deg 47.4% 11.2%; 9 | 10 | --muted: 210deg 40% 96.1%; 11 | --muted-foreground: 215.4deg 16.3% 46.9%; 12 | 13 | --popover: 0deg 0% 100%; 14 | --popover-foreground: 222.2deg 47.4% 11.2%; 15 | 16 | --border: 214.3deg 31.8% 91.4%; 17 | --input: 214.3deg 31.8% 91.4%; 18 | 19 | --card: 0deg 0% 100%; 20 | --card-foreground: 222.2deg 47.4% 11.2%; 21 | 22 | --primary: 222.2deg 47.4% 11.2%; 23 | --primary-foreground: 210deg 40% 98%; 24 | 25 | --secondary: 210deg 40% 96.1%; 26 | --secondary-foreground: 222.2deg 47.4% 11.2%; 27 | 28 | --accent: 210deg 40% 96.1%; 29 | --accent-foreground: 222.2deg 47.4% 11.2%; 30 | 31 | --destructive: 0deg 100% 50%; 32 | --destructive-foreground: 210deg 40% 98%; 33 | 34 | --ring: 215deg 20.2% 65.1%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71% 4%; 41 | --foreground: 213 31% 91%; 42 | 43 | --muted: 223 47% 11%; 44 | --muted-foreground: 215.4 16.3% 56.9%; 45 | 46 | --accent: 216 34% 17%; 47 | --accent-foreground: 210 40% 98%; 48 | 49 | --popover: 224 71% 4%; 50 | --popover-foreground: 215 20.2% 65.1%; 51 | 52 | --border: 216 34% 17%; 53 | --input: 216 34% 17%; 54 | 55 | --card: 224 71% 4%; 56 | --card-foreground: 213 31% 91%; 57 | 58 | --primary: 210 40% 98%; 59 | --primary-foreground: 222.2 47.4% 1.2%; 60 | 61 | --secondary: 222.2 47.4% 11.2%; 62 | --secondary-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 63% 31%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 216 34% 17%; 68 | 69 | --radius: 0.5rem; 70 | } 71 | } 72 | 73 | @layer base { 74 | body { 75 | @apply bg-background text-foreground; 76 | font-feature-settings: 77 | "rlig" 1, 78 | "calt" 1; 79 | } 80 | 81 | .container { 82 | @apply max-sm:px-4; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /apps/nextjs/src/styles/theme/default.css: -------------------------------------------------------------------------------- 1 | .theme-dracula.light { 2 | --background: 231, 15%, 100%; 3 | --foreground: 60, 30%, 10%; 4 | 5 | --muted: 232, 14%, 98%; 6 | --muted-foreground: 60, 30%, 20%; 7 | 8 | --popover: 231, 15%, 94%; 9 | --popover-foreground: 60, 30%, 20%; 10 | 11 | --border: 232, 14%, 31%; 12 | --input: 225, 27%, 51%; 13 | 14 | --card: 232, 14%, 98%; 15 | --card-foreground: 60, 30%, 5%; 16 | 17 | --primary: 265, 89%, 78%; 18 | --primary-foreground: 60, 30%, 96%; 19 | 20 | --secondary: 326, 100%, 74%; 21 | --secondary-foreground: 60, 30%, 96%; 22 | 23 | --accent: 225, 27%, 70%; 24 | --accent-foreground: 60, 30%, 10%; 25 | 26 | --destructive: 0, 100%, 67%; 27 | --destructive-foreground: 60, 30%, 96%; 28 | 29 | --ring: 225, 27%, 51%; 30 | } 31 | 32 | .theme-dracula.dark { 33 | --background: 231, 15%, 18%; 34 | --foreground: 60, 30%, 96%; 35 | 36 | --muted: 232, 14%, 31%; 37 | --muted-foreground: 60, 30%, 96%; 38 | 39 | --popover: 231, 15%, 18%; 40 | --popover-foreground: 60, 30%, 96%; 41 | 42 | --border: 232, 14%, 31%; 43 | --input: 225, 27%, 51%; 44 | 45 | --card: 232, 14%, 31%; 46 | --card-foreground: 60, 30%, 96%; 47 | 48 | --primary: 265, 89%, 78%; 49 | --primary-foreground: 60, 30%, 96%; 50 | 51 | --secondary: 326, 100%, 74%; 52 | --secondary-foreground: 60, 30%, 96%; 53 | 54 | --accent: 225, 27%, 51%; 55 | --accent-foreground: 60, 30%, 96%; 56 | 57 | --destructive: 0, 100%, 67%; 58 | --destructive-foreground: 60, 30%, 96%; 59 | 60 | --ring: 225, 27%, 51%; 61 | } 62 | -------------------------------------------------------------------------------- /apps/nextjs/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type * as Lucide from "lucide-react"; 2 | 3 | export interface NavItem { 4 | title: string; 5 | href: string; 6 | disabled?: boolean; 7 | } 8 | 9 | export type MainNavItem = NavItem; 10 | 11 | export interface DocsConfig { 12 | mainNav: MainNavItem[]; 13 | sidebarNav: SidebarNavItem[]; 14 | } 15 | 16 | export type SidebarNavItem = { 17 | id: string; 18 | title: string; 19 | disabled?: boolean; 20 | external?: boolean; 21 | icon?: Lucide.LucideIcon; 22 | } & ( 23 | | { 24 | href: string; 25 | items?: never; 26 | } 27 | | { 28 | href?: string; 29 | items: NavLink[]; 30 | } 31 | ); 32 | 33 | export interface SiteConfig { 34 | name: string; 35 | description: string; 36 | url: string; 37 | ogImage: string; 38 | links: { 39 | github: string; 40 | }; 41 | } 42 | 43 | export interface DocsConfig { 44 | mainNav: MainNavItem[]; 45 | sidebarNav: SidebarNavItem[]; 46 | } 47 | 48 | export interface MarketingConfig { 49 | mainNav: MainNavItem[]; 50 | } 51 | 52 | export interface DashboardConfig { 53 | mainNav: MainNavItem[]; 54 | sidebarNav: SidebarNavItem[]; 55 | } 56 | -------------------------------------------------------------------------------- /apps/nextjs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | import baseConfig from "@saasfly/tailwind-config"; 4 | 5 | export default { 6 | content: [...baseConfig.content, "../../packages/ui/src/**/*.{ts,tsx}"], 7 | presets: [baseConfig], 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saasfly/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./src/*"] 7 | }, 8 | "plugins": [ 9 | { 10 | "name": "next" 11 | } 12 | ], 13 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 14 | }, 15 | "include": [ 16 | "next-env.d.ts", 17 | ".next/types/**/*.ts", 18 | "*.ts", 19 | "*.tsx", 20 | "*.mjs", 21 | "src" 22 | ], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /deploy1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/deploy1.png -------------------------------------------------------------------------------- /deploy2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/deploy2.png -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/img.png -------------------------------------------------------------------------------- /img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saasfly/waitlist/feed45bbc4a2287e7681784e5aa8900cc084cf25/img_1.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-template", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo build ", 6 | "clean": "git clean -xdf node_modules", 7 | "clean:workspaces": "turbo clean", 8 | "dev": "turbo dev --parallel", 9 | "format": "turbo format --continue -- --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'", 10 | "format:fix": "turbo format --continue -- --write --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'", 11 | "lint": "turbo lint -- --quiet -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check", 12 | "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache' ", 13 | "typecheck": "turbo typecheck", 14 | "postinstall": "bun run check-deps", 15 | "check-deps": "check-dependency-version-consistency .", 16 | "gen": "turbo gen --config 'turbo/generators/config.ts'" 17 | }, 18 | "devDependencies": { 19 | "@turbo/gen": "1.13.2", 20 | "prettier": "3.2.5", 21 | "turbo": "1.13.2", 22 | "typescript": "5.4.5", 23 | "check-dependency-version-consistency": "4.1.0" 24 | }, 25 | "engines": { 26 | "node": ">=18" 27 | }, 28 | "prettier": "@saasfly/prettier-config", 29 | "workspaces": [ 30 | "apps/*", 31 | "packages/*", 32 | "tooling/*" 33 | ], 34 | "packageManager": "bun@1.1.2" 35 | } 36 | -------------------------------------------------------------------------------- /packages/common/.eslintignore: -------------------------------------------------------------------------------- 1 | src/subscriptions.ts -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saasfly/common", 3 | "version": "0.1.0", 4 | "private": true, 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./resend": "./src/email.ts", 8 | "./MagicLinkEmail": "./src/emails/magic-link-email.tsx", 9 | "./subscriptions": "./src/subscriptions.ts", 10 | "./env": "./src/env.mjs" 11 | }, 12 | "typesVersions": { 13 | "*": { 14 | "*": [ 15 | "src/*" 16 | ] 17 | } 18 | }, 19 | "scripts": { 20 | "clean": "rm -rf .turbo node_modules", 21 | "format": "prettier --check '**/*.{mjs,ts,json}' " 22 | }, 23 | "dependencies": { 24 | "@saasfly/ui": "workspace:*", 25 | "resend": "2.1.0" 26 | }, 27 | "devDependencies": { 28 | "@saasfly/eslint-config": "workspace:*", 29 | "@saasfly/prettier-config": "workspace:*", 30 | "@saasfly/typescript-config": "workspace:*", 31 | "eslint": "8.57.0", 32 | "prettier": "3.2.5", 33 | "typescript": "5.4.5" 34 | }, 35 | "eslintConfig": { 36 | "root": true, 37 | "extends": [ 38 | "@saasfly/eslint-config/base" 39 | ] 40 | }, 41 | "prettier": "@saasfly/prettier-config" 42 | } 43 | -------------------------------------------------------------------------------- /packages/common/src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Saasfly", 3 | description: "We provide an easier way to build saas service in production", 4 | url: "https://github.com/saaslfy/saasfly", 5 | ogImage: "", 6 | links: { 7 | github: "https://github.com/saaslfy", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/common/src/email.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | 3 | import { env } from "./env.mjs"; 4 | 5 | export const resend = new Resend(env.RESEND_API_KEY); 6 | -------------------------------------------------------------------------------- /packages/common/src/emails/magic-link-email.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Hr, 7 | Html, 8 | Preview, 9 | Section, 10 | Tailwind, 11 | Text, 12 | } from "@react-email/components"; 13 | 14 | import * as Icons from "@saasfly/ui/icons"; 15 | 16 | interface MagicLinkEmailProps { 17 | actionUrl: string; 18 | firstName: string; 19 | mailType: "login" | "register"; 20 | siteName: string; 21 | } 22 | 23 | export const MagicLinkEmail = ({ 24 | firstName = "", 25 | actionUrl, 26 | mailType, 27 | siteName, 28 | }: MagicLinkEmailProps) => ( 29 | 30 | 31 | 32 | Click to {mailType === "login" ? "sign in" : "activate"} your {siteName}{" "} 33 | account. 34 | 35 | 36 | 37 | 38 | 39 | Hi {firstName}, 40 | 41 | Welcome to {siteName} ! Click the link below to{" "} 42 | {mailType === "login" ? "sign in to" : "activate"} your account. 43 | 44 |
45 | 51 |
52 | 53 | This link expires in 24 hours and can only be used once. 54 | 55 | {mailType === "login" ? ( 56 | 57 | If you did not try to log into your account, you can safely ignore 58 | it. 59 | 60 | ) : null} 61 |
62 | saasfly.io 63 |
64 | 65 |
66 | 67 | ); 68 | 69 | export default MagicLinkEmail; 70 | -------------------------------------------------------------------------------- /packages/common/src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import * as z from "zod"; 3 | 4 | export const env = createEnv({ 5 | shared: { 6 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string().optional(), 7 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string().optional(), 8 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID: z.string().optional(), 9 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID: z.string().optional(), 10 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID: z.string().optional(), 11 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID: z.string().optional(), 12 | }, 13 | server: { 14 | NEXTAUTH_SECRET: z.string().min(1), 15 | RESEND_API_KEY: z.string().optional(), 16 | }, 17 | // Client side variables gets destructured here due to Next.js static analysis 18 | // Shared ones are also included here for good measure since the behavior has been inconsistent 19 | runtimeEnv: { 20 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, 21 | RESEND_API_KEY: process.env.RESEND_API_KEY, 22 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: 23 | process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID, 24 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: 25 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID, 26 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID: 27 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID, 28 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID: 29 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID, 30 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID: 31 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, 32 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID: 33 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID, 34 | }, 35 | skipValidation: 36 | !!process.env.SKIP_ENV_VALIDATION || 37 | process.env.npm_lifecycle_event === "lint", 38 | }); 39 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export { resend } from "./email"; 2 | 3 | export { MagicLinkEmail } from "./emails/magic-link-email"; 4 | 5 | export { siteConfig } from "./config/site"; 6 | -------------------------------------------------------------------------------- /packages/common/src/subscriptions.ts: -------------------------------------------------------------------------------- 1 | import { env } from "./env.mjs"; 2 | 3 | export interface SubscriptionPlan { 4 | title: string; 5 | description: string; 6 | benefits: string[]; 7 | limitations: string[]; 8 | prices: { 9 | monthly: number; 10 | yearly: number; 11 | }; 12 | stripeIds: { 13 | monthly: string | null; 14 | yearly: string | null; 15 | }; 16 | } 17 | 18 | export const pricingData: SubscriptionPlan[] = [ 19 | { 20 | title: "Starter", 21 | description: "For Beginners", 22 | benefits: [ 23 | "Up to 100 monthly posts", 24 | "Basic analytics and reporting", 25 | "Access to standard templates", 26 | ], 27 | limitations: [ 28 | "No priority access to new features.", 29 | "Limited customer support", 30 | "No custom branding", 31 | "Limited access to business resources.", 32 | ], 33 | prices: { 34 | monthly: 0, 35 | yearly: 0, 36 | }, 37 | stripeIds: { 38 | monthly: null, 39 | yearly: null, 40 | }, 41 | }, 42 | { 43 | title: "Pro", 44 | description: "Unlock Advanced Features", 45 | benefits: [ 46 | "Up to 500 monthly posts", 47 | "Advanced analytics and reporting", 48 | "Access to business templates", 49 | "Priority customer support", 50 | "Exclusive webinars and training.", 51 | ], 52 | limitations: [ 53 | "No custom branding", 54 | "Limited access to business resources.", 55 | ], 56 | prices: { 57 | monthly: 15, 58 | yearly: 144, 59 | }, 60 | stripeIds: { 61 | // @ts-ignore 62 | monthly: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID, 63 | // @ts-ignore 64 | yearly: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, 65 | }, 66 | }, 67 | { 68 | title: "Business", 69 | description: "For Power Users", 70 | benefits: [ 71 | "Unlimited posts", 72 | "Real-time analytics and reporting", 73 | "Access to all templates, including custom branding", 74 | "24/7 business customer support", 75 | "Personalized onboarding and account management.", 76 | ], 77 | limitations: [], 78 | prices: { 79 | monthly: 30, 80 | yearly: 300, 81 | }, 82 | stripeIds: { 83 | // @ts-ignore 84 | monthly: env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID, 85 | // @ts-ignore 86 | yearly: env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID, 87 | }, 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saasfly/typescript-config/base.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 5 | }, 6 | "include": ["*.ts", "src"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/.eslintignore: -------------------------------------------------------------------------------- 1 | src/button.tsx 2 | src/alert.tsx 3 | src/data-table.tsx 4 | src/form.tsx 5 | src/label.tsx 6 | src/sheet.tsx 7 | src/toast.tsx 8 | src/utils/ 9 | src/table.tsx 10 | src/sparkles.tsx 11 | src/globe.tsx -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saasfly/ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "typesVersions": { 6 | "*": { 7 | "*": [ 8 | "src/*" 9 | ] 10 | } 11 | }, 12 | "scripts": { 13 | "clean": "git clean -xdf .turbo node_modules", 14 | "format": "prettier --check '**/*.{ts,tsx}' ", 15 | "lint": "eslint ." 16 | }, 17 | "dependencies": { 18 | "@radix-ui/react-accessible-icon": "1.0.3", 19 | "@radix-ui/react-accordion": "1.1.2", 20 | "@radix-ui/react-alert-dialog": "1.0.5", 21 | "@radix-ui/react-aspect-ratio": "1.0.3", 22 | "@radix-ui/react-avatar": "1.0.4", 23 | "@radix-ui/react-checkbox": "1.0.4", 24 | "@radix-ui/react-collapsible": "^1.0.3", 25 | "@radix-ui/react-context-menu": "2.1.5", 26 | "@radix-ui/react-dialog": "1.0.5", 27 | "@radix-ui/react-dropdown-menu": "2.0.6", 28 | "@radix-ui/react-hover-card": "1.0.7", 29 | "@radix-ui/react-label": "2.0.2", 30 | "@radix-ui/react-menubar": "1.0.4", 31 | "@radix-ui/react-navigation-menu": "1.1.4", 32 | "@radix-ui/react-popover": "1.0.7", 33 | "@radix-ui/react-progress": "1.0.3", 34 | "@radix-ui/react-radio-group": "1.1.3", 35 | "@radix-ui/react-scroll-area": "1.0.5", 36 | "@radix-ui/react-select": "1.2.2", 37 | "@radix-ui/react-separator": "1.0.3", 38 | "@radix-ui/react-slider": "1.1.2", 39 | "@radix-ui/react-slot": "1.0.2", 40 | "@radix-ui/react-switch": "1.0.3", 41 | "@radix-ui/react-tabs": "1.0.4", 42 | "@radix-ui/react-toast": "1.1.5", 43 | "@radix-ui/react-toggle": "1.0.3", 44 | "@radix-ui/react-toggle-group": "1.0.4", 45 | "@radix-ui/react-tooltip": "1.0.7", 46 | "@react-email/button": "0.0.14", 47 | "@react-email/components": "0.0.16", 48 | "@react-email/html": "0.0.7", 49 | "class-variance-authority": "0.7.0", 50 | "clsx": "2.1.0", 51 | "cmdk": "0.2.1", 52 | "lucide-react": "0.365.0", 53 | "tailwind-merge": "1.14.0", 54 | "zod": "3.22.4", 55 | "@tsparticles/react" : "3.0.0", 56 | "@tsparticles/engine": "3.3.0", 57 | "@tsparticles/slim": "3.3.0", 58 | "@react-three/fiber": "8.16.1", 59 | "@react-three/drei": "9.105.1", 60 | "three": "0.163.0", 61 | "three-globe": "2.31.0" 62 | }, 63 | "devDependencies": { 64 | "@saasfly/eslint-config": "workspace:*", 65 | "@saasfly/prettier-config": "workspace:*", 66 | "@saasfly/typescript-config": "workspace:*", 67 | "@saasfly/tailwind-config": "workspace:*", 68 | "@types/react": "18.2.77", 69 | "@tanstack/react-table": "8.15.3", 70 | "date-fns": "3.6.0", 71 | "eslint": "8.57.0", 72 | "prettier": "3.2.5", 73 | "react": "18.2.0", 74 | "react-day-picker": "8.10.0", 75 | "react-dom": "18.2.0", 76 | "react-hook-form": "7.51.2", 77 | "tailwindcss": "3.4.3", 78 | "tailwindcss-animate": "1.0.7", 79 | "typescript": "5.4.5" 80 | }, 81 | "prettier": "@saasfly/prettier-config", 82 | "eslintConfig": { 83 | "root": true, 84 | "extends": [ 85 | "@saasfly/eslint-config/base", 86 | "@saasfly/eslint-config/react" 87 | ] 88 | }, 89 | "exports": { 90 | ".": "./src/index.ts", 91 | "./avatar": "./src/avatar.tsx", 92 | "./button": "./src/button.tsx", 93 | "./calendar": "./src/calendar.tsx", 94 | "./card": "./src/card.tsx", 95 | "./checkbox": "./src/checkbox.tsx", 96 | "./command": "./src/command.tsx", 97 | "./data-table": "./src/data-table.tsx", 98 | "./dialog": "./src/dialog.tsx", 99 | "./dropdown-menu": "./src/dropdown-menu.tsx", 100 | "./form": "./src/form.tsx", 101 | "./icons": "./src/icons.tsx", 102 | "./input": "./src/input.tsx", 103 | "./label": "./src/label.tsx", 104 | "./popover": "./src/popover.tsx", 105 | "./scroll-area": "./src/scroll-area.tsx", 106 | "./select": "./src/select.tsx", 107 | "./sheet": "./src/sheet.tsx", 108 | "./table": "./src/table.tsx", 109 | "./tabs": "./src/tabs.tsx", 110 | "./toaster": "./src/toaster.tsx", 111 | "./use-toast": "./src/use-toast.tsx", 112 | "./skeleton": "./src/skeleton.tsx", 113 | "./alert": "./src/alert.tsx", 114 | "./alert-dialog": "./src/alert-dialog.tsx", 115 | "./callout": "./src/callout.tsx", 116 | "./card-skeleton": "./src/card-skeleton.tsx", 117 | "./switch": "./src/switch.tsx", 118 | "./accordion": "./src/accordion.tsx", 119 | "./3d-card": "./src/3d-card.tsx", 120 | "./animated-tooltip": "./src/animated-tooltip.tsx", 121 | "./text-generate-effect": "./src/text-generate-effect.tsx", 122 | "./typewriter-effect": "./src/typewriter-effect.tsx", 123 | "./meteors": "./src/meteors.tsx", 124 | "./infinite-moving-cards": "./src/infinite-moving-cards.tsx", 125 | "./card-hover-effect": "./src/card-hover-effect.tsx", 126 | "./sparkles": "./src/sparkles.tsx", 127 | "./globe": "./src/globe.tsx", 128 | "./background-beams": "./src/background-beams.tsx" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /packages/ui/src/3d-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | createContext, 5 | useContext, 6 | useEffect, 7 | useRef, 8 | useState, 9 | } from "react"; 10 | 11 | import { cn } from "@saasfly/ui"; 12 | 13 | const MouseEnterContext = createContext< 14 | [boolean, React.Dispatch>] | undefined 15 | >(undefined); 16 | 17 | export const CardContainer = ({ 18 | children, 19 | className, 20 | containerClassName, 21 | }: { 22 | children?: React.ReactNode; 23 | className?: string; 24 | containerClassName?: string; 25 | }) => { 26 | const containerRef = useRef(null); 27 | const [isMouseEntered, setIsMouseEntered] = useState(false); 28 | 29 | const handleMouseMove = (e: React.MouseEvent) => { 30 | if (!containerRef.current) return; 31 | const { left, top, width, height } = 32 | containerRef.current.getBoundingClientRect(); 33 | const x = (e.clientX - left - width / 2) / 25; 34 | const y = (e.clientY - top - height / 2) / 25; 35 | containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`; 36 | }; 37 | 38 | const handleMouseEnter = () => { 39 | setIsMouseEntered(true); 40 | if (!containerRef.current) return; 41 | }; 42 | 43 | const handleMouseLeave = () => { 44 | if (!containerRef.current) return; 45 | setIsMouseEntered(false); 46 | containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`; 47 | }; 48 | return ( 49 | 50 |
59 |
72 | {children} 73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export const CardBody = ({ 80 | children, 81 | className, 82 | }: { 83 | children: React.ReactNode; 84 | className?: string; 85 | }) => { 86 | return ( 87 |
*]:[transform-style:preserve-3d]", 90 | className, 91 | )} 92 | > 93 | {children} 94 |
95 | ); 96 | }; 97 | 98 | export const CardItem = ({ 99 | as: Tag = "div", 100 | children, 101 | className, 102 | translateX = 0, 103 | translateY = 0, 104 | translateZ = 0, 105 | rotateX = 0, 106 | rotateY = 0, 107 | rotateZ = 0, 108 | ...rest 109 | }: { 110 | as?: React.ElementType; 111 | children: React.ReactNode; 112 | className?: string; 113 | translateX?: number | string; 114 | translateY?: number | string; 115 | translateZ?: number | string; 116 | rotateX?: number | string; 117 | rotateY?: number | string; 118 | rotateZ?: number | string; 119 | }) => { 120 | const ref = useRef(null); 121 | const [isMouseEntered] = useMouseEnter(); 122 | 123 | useEffect(() => { 124 | handleAnimations(); 125 | }, [isMouseEntered]); 126 | 127 | const handleAnimations = () => { 128 | if (!ref.current) return; 129 | if (isMouseEntered) { 130 | ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`; 131 | } else { 132 | ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`; 133 | } 134 | }; 135 | 136 | return ( 137 | 142 | {children} 143 | 144 | ); 145 | }; 146 | 147 | // Create a hook to use the context 148 | export const useMouseEnter = () => { 149 | const context = useContext(MouseEnterContext); 150 | if (context === undefined) { 151 | throw new Error("useMouseEnter must be used within a MouseEnterProvider"); 152 | } 153 | return context; 154 | }; 155 | -------------------------------------------------------------------------------- /packages/ui/src/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDown } from "lucide-react"; 6 | 7 | import { cn } from "@saasfly/ui"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 55 |
{children}
56 |
57 | )); 58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 59 | 60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 61 | -------------------------------------------------------------------------------- /packages/ui/src/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { buttonVariants } from "./button"; 7 | import { cn } from "./utils/cn"; 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root; 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 12 | 13 | const AlertDialogPortal = ({ 14 | children, 15 | ...props 16 | }: AlertDialogPrimitive.AlertDialogPortalProps) => ( 17 | 18 |
19 | {children} 20 |
21 |
22 | ); 23 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; 24 | 25 | const AlertDialogOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 39 | 40 | const AlertDialogContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 45 | 46 | 54 | 55 | )); 56 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 57 | 58 | const AlertDialogHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
69 | ); 70 | AlertDialogHeader.displayName = "AlertDialogHeader"; 71 | 72 | const AlertDialogFooter = ({ 73 | className, 74 | ...props 75 | }: React.HTMLAttributes) => ( 76 |
83 | ); 84 | AlertDialogFooter.displayName = "AlertDialogFooter"; 85 | 86 | const AlertDialogTitle = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )); 96 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 97 | 98 | const AlertDialogDescription = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )); 108 | AlertDialogDescription.displayName = 109 | AlertDialogPrimitive.Description.displayName; 110 | 111 | const AlertDialogAction = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 120 | )); 121 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 122 | 123 | const AlertDialogCancel = React.forwardRef< 124 | React.ElementRef, 125 | React.ComponentPropsWithoutRef 126 | >(({ className, ...props }, ref) => ( 127 | 136 | )); 137 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 138 | 139 | export { 140 | AlertDialog, 141 | AlertDialogTrigger, 142 | AlertDialogContent, 143 | AlertDialogHeader, 144 | AlertDialogFooter, 145 | AlertDialogTitle, 146 | AlertDialogDescription, 147 | AlertDialogAction, 148 | AlertDialogCancel, 149 | }; 150 | -------------------------------------------------------------------------------- /packages/ui/src/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "./utils/cn"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | {props.children} 45 |
46 | )); 47 | AlertTitle.displayName = "AlertTitle"; 48 | 49 | const AlertDescription = React.forwardRef< 50 | HTMLParagraphElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => ( 53 |
58 | )); 59 | AlertDescription.displayName = "AlertDescription"; 60 | 61 | export { Alert, AlertTitle, AlertDescription }; 62 | -------------------------------------------------------------------------------- /packages/ui/src/animated-tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from "react"; 4 | import Image from "next/image"; 5 | import { motion } from "framer-motion"; 6 | 7 | export const AnimatedTooltip = ({ 8 | items, 9 | }: { 10 | items: { 11 | id: number; 12 | name: string; 13 | designation: string; 14 | image: string; 15 | }[]; 16 | }) => { 17 | const [hoveredIndex, setHoveredIndex] = useState(null); 18 | const springConfig = { stiffness: 100, damping: 5 }; 19 | 20 | return ( 21 | <> 22 | {items.map((item, index) => ( 23 |
setHoveredIndex(index)} 27 | onMouseLeave={() => setHoveredIndex(null)} 28 | > 29 | {hoveredIndex === index && ( 30 | 41 |
{item.name}
42 |
{item.designation}
43 |
44 | )} 45 | {item.name} 52 |
53 | ))} 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/ui/src/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/background-beams.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | 6 | import { cn } from "./utils/cn"; 7 | 8 | export const BackgroundBeams = React.memo( 9 | ({ className }: { className?: string }) => { 10 | const paths = [ 11 | "M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875", 12 | "M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867", 13 | "M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859", 14 | "M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851", 15 | "M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843", 16 | "M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835", 17 | "M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827", 18 | "M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819", 19 | "M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811", 20 | "M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803", 21 | "M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795", 22 | "M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787", 23 | "M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779", 24 | "M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771", 25 | "M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763", 26 | "M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755", 27 | "M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747", 28 | "M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739", 29 | "M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731", 30 | "M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723", 31 | "M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715", 32 | "M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707", 33 | "M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699", 34 | "M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691", 35 | "M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683", 36 | "M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675", 37 | "M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667", 38 | "M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659", 39 | "M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651", 40 | "M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643", 41 | "M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635", 42 | "M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627", 43 | "M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619", 44 | "M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611", 45 | "M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603", 46 | "M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595", 47 | "M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587", 48 | "M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579", 49 | "M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571", 50 | "M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563", 51 | "M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555", 52 | "M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547", 53 | "M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539", 54 | "M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531", 55 | "M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523", 56 | "M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515", 57 | "M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507", 58 | "M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499", 59 | "M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491", 60 | "M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483", 61 | ]; 62 | return ( 63 |
69 | 77 | 83 | 84 | {paths.map((path, index) => ( 85 | 92 | ))} 93 | 94 | {paths.map((path, index) => ( 95 | 117 | 118 | 119 | 120 | 121 | 122 | ))} 123 | 124 | 132 | 133 | 134 | 135 | 136 | 137 | 138 |
139 | ); 140 | }, 141 | ); 142 | 143 | BackgroundBeams.displayName = "BackgroundBeams"; 144 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@saasfly/ui"; 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 12 | destructive: 13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: 15 | "border border-input hover:bg-accent hover:text-accent-foreground", 16 | secondary: 17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 18 | ghost: "hover:bg-accent hover:text-accent-foreground", 19 | link: "underline-offset-4 hover:underline text-primary", 20 | }, 21 | size: { 22 | default: "h-10 py-2 px-4", 23 | sm: "h-9 px-3 rounded-md", 24 | lg: "h-11 px-8 rounded-md", 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: "default", 29 | size: "default", 30 | }, 31 | }, 32 | ); 33 | 34 | export interface ButtonProps 35 | extends React.ButtonHTMLAttributes, 36 | VariantProps {} 37 | 38 | const Button = React.forwardRef( 39 | ({ className, variant, size, ...props }, ref) => { 40 | return ( 41 |