├── .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 | [](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 | 
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 | 
97 |
98 | 2. Deploy config
99 |
100 | 
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 | 
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 | 
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 | 
97 |
98 | 2. 部署配置
99 |
100 | 
101 |
102 | ## 🤔 数据分析
103 | 你不需要一个非常复杂的BI系统。我构建了一个简单的本地优先的Excel分析[服务](https://excel.saasfly.io) 来帮助你。
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 |
69 |
70 | Please make sure your Google account is logged in.
71 |
72 |
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 |
39 | {items?.map((item, index) => (
40 |
51 | {item.title}
52 |
53 | ))}
54 |
55 | ) : null}
56 |
57 |
setShowMobileMenu(!showMobileMenu)}
60 | >
61 | {showMobileMenu ? (
62 |
63 | ) : (
64 |
65 | )}
66 |
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 |
39 | {items.map((item, index) => (
40 |
48 | {item.title}
49 |
50 | ))}
51 |
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 |
22 |
23 |
24 | Toggle theme
25 |
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 |
49 | {mailType === "login" ? "Sign in" : "Activate Account"}
50 |
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 |
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 |
46 | );
47 | },
48 | );
49 | Button.displayName = "Button";
50 |
51 | export { Button, buttonVariants };
52 |
--------------------------------------------------------------------------------
/packages/ui/src/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ChevronLeft, ChevronRight } from "lucide-react";
5 | import { DayPicker } from "react-day-picker";
6 |
7 | import { buttonVariants } from "./button";
8 | import { cn } from "./utils/cn";
9 |
10 | export type { DateRange } from "react-day-picker";
11 | export type CalendarProps = React.ComponentProps;
12 |
13 | function Calendar({
14 | className,
15 | classNames,
16 | showOutsideDays = true,
17 | ...props
18 | }: CalendarProps) {
19 | return (
20 | (
57 |
58 | ),
59 | IconRight: ({ ...props }) => (
60 |
61 | ),
62 | }}
63 | {...props}
64 | />
65 | );
66 | }
67 |
68 | Calendar.displayName = "Calendar";
69 |
70 | export { Calendar };
71 |
--------------------------------------------------------------------------------
/packages/ui/src/callout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@saasfly/ui";
2 |
3 | interface CalloutProps {
4 | icon?: string;
5 | children?: React.ReactNode;
6 | type?: "default" | "warning" | "danger" | "info";
7 | }
8 |
9 | // ✅💡⚠️🚫🚨
10 | export function Callout({
11 | children,
12 | icon,
13 | type = "default",
14 | ...props
15 | }: CalloutProps) {
16 | return (
17 |
28 | {icon &&
{icon} }
29 |
{children}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/ui/src/card-hover-effect.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Link from "next/link";
3 | import { AnimatePresence, motion } from "framer-motion";
4 |
5 | import { cn } from "./utils/cn";
6 |
7 | export const HoverEffect = ({
8 | items,
9 | className,
10 | }: {
11 | items: {
12 | title: string;
13 | description: string;
14 | link: string;
15 | }[];
16 | className?: string;
17 | }) => {
18 | const [hoveredIndex, setHoveredIndex] = useState(null);
19 |
20 | return (
21 |
27 | {items.map((item, idx) => (
28 |
setHoveredIndex(idx)}
33 | onMouseLeave={() => setHoveredIndex(null)}
34 | >
35 |
36 | {hoveredIndex === idx && (
37 |
50 | )}
51 |
52 |
53 | {item.title}
54 | {item.description}
55 |
56 |
57 | ))}
58 |
59 | );
60 | };
61 |
62 | export const Card = ({
63 | className,
64 | children,
65 | }: {
66 | className?: string;
67 | children: React.ReactNode;
68 | }) => {
69 | return (
70 |
80 | );
81 | };
82 | export const CardTitle = ({
83 | className,
84 | children,
85 | }: {
86 | className?: string;
87 | children: React.ReactNode;
88 | }) => {
89 | return (
90 |
91 | {children}
92 |
93 | );
94 | };
95 | export const CardDescription = ({
96 | className,
97 | children,
98 | }: {
99 | className?: string;
100 | children: React.ReactNode;
101 | }) => {
102 | return (
103 |
109 | {children}
110 |
111 | );
112 | };
113 |
--------------------------------------------------------------------------------
/packages/ui/src/card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardFooter, CardHeader } from "@saasfly/ui/card";
2 | import { Skeleton } from "@saasfly/ui/skeleton";
3 |
4 | export function CardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "./utils/cn";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | {props.children}
45 |
46 | ));
47 | CardTitle.displayName = "CardTitle";
48 |
49 | const CardDescription = React.forwardRef<
50 | HTMLParagraphElement,
51 | React.HTMLAttributes
52 | >(({ className, ...props }, ref) => (
53 |
58 | ));
59 | CardDescription.displayName = "CardDescription";
60 |
61 | const CardContent = React.forwardRef<
62 | HTMLDivElement,
63 | React.HTMLAttributes
64 | >(({ className, ...props }, ref) => (
65 |
66 | ));
67 | CardContent.displayName = "CardContent";
68 |
69 | const CardFooter = React.forwardRef<
70 | HTMLDivElement,
71 | React.HTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
78 | ));
79 | CardFooter.displayName = "CardFooter";
80 |
81 | export {
82 | Card,
83 | CardHeader,
84 | CardFooter,
85 | CardTitle,
86 | CardDescription,
87 | CardContent,
88 | };
89 |
--------------------------------------------------------------------------------
/packages/ui/src/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { Check } from "lucide-react";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/packages/ui/src/command.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import type { DialogProps } from "@radix-ui/react-dialog";
5 | import { Command as CommandPrimitive } from "cmdk";
6 | import { Search } from "lucide-react";
7 |
8 | import { Dialog, DialogContent } from "./dialog";
9 | import { cn } from "./utils/cn";
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ));
24 | Command.displayName = CommandPrimitive.displayName;
25 |
26 | type CommandDialogProps = DialogProps;
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 | // eslint-disable-next-line react/no-unknown-property
45 |
46 |
47 |
55 |
56 | ));
57 |
58 | CommandInput.displayName = CommandPrimitive.Input.displayName;
59 |
60 | const CommandList = React.forwardRef<
61 | React.ElementRef,
62 | React.ComponentPropsWithoutRef
63 | >(({ className, ...props }, ref) => (
64 |
69 | ));
70 |
71 | CommandList.displayName = CommandPrimitive.List.displayName;
72 |
73 | const CommandEmpty = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >((props, ref) => (
77 |
82 | ));
83 |
84 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
85 |
86 | const CommandGroup = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
98 | ));
99 |
100 | CommandGroup.displayName = CommandPrimitive.Group.displayName;
101 |
102 | const CommandSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
113 |
114 | const CommandItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, ...props }, ref) => (
118 |
126 | ));
127 |
128 | CommandItem.displayName = CommandPrimitive.Item.displayName;
129 |
130 | const CommandShortcut = ({
131 | className,
132 | ...props
133 | }: React.HTMLAttributes) => {
134 | return (
135 |
142 | );
143 | };
144 | CommandShortcut.displayName = "CommandShortcut";
145 |
146 | export {
147 | Command,
148 | CommandDialog,
149 | CommandInput,
150 | CommandList,
151 | CommandEmpty,
152 | CommandGroup,
153 | CommandItem,
154 | CommandShortcut,
155 | CommandSeparator,
156 | };
157 |
--------------------------------------------------------------------------------
/packages/ui/src/data-table.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | flexRender,
5 | getCoreRowModel,
6 | useReactTable,
7 | type ColumnDef,
8 | } from "@tanstack/react-table";
9 |
10 | import {
11 | Table,
12 | TableBody,
13 | TableCell,
14 | TableHead,
15 | TableHeader,
16 | TableRow,
17 | } from "./table";
18 |
19 | interface DataTableProps {
20 | columns: ColumnDef[];
21 | data: TData[];
22 | }
23 |
24 | export function DataTable({
25 | columns,
26 | data,
27 | }: DataTableProps) {
28 | const table = useReactTable({
29 | data,
30 | columns,
31 | getCoreRowModel: getCoreRowModel(),
32 | });
33 |
34 | return (
35 |
36 |
37 |
38 | {table.getHeaderGroups().map((headerGroup) => (
39 |
40 | {headerGroup.headers.map((header) => {
41 | return (
42 |
43 | {header.isPlaceholder
44 | ? null
45 | : flexRender(
46 | header.column.columnDef.header,
47 | header.getContext(),
48 | )}
49 |
50 | );
51 | })}
52 |
53 | ))}
54 |
55 |
56 | {table.getRowModel().rows?.length ? (
57 | table.getRowModel().rows.map((row) => (
58 |
62 | {row.getVisibleCells().map((cell) => (
63 |
64 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
65 |
66 | ))}
67 |
68 | ))
69 | ) : (
70 |
71 |
72 | No results.
73 |
74 |
75 | )}
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/packages/ui/src/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({
14 | children,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 |
19 | {children}
20 |
21 |
22 | );
23 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
24 |
25 | const DialogOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
39 |
40 | const DialogContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, children, ...props }, ref) => (
44 |
45 |
46 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ));
63 | DialogContent.displayName = DialogPrimitive.Content.displayName;
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
70 | );
71 | DialogHeader.displayName = "DialogHeader";
72 |
73 | const DialogFooter = ({
74 | className,
75 | ...props
76 | }: React.HTMLAttributes) => (
77 |
84 | );
85 | DialogFooter.displayName = "DialogFooter";
86 |
87 | const DialogTitle = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => (
91 |
99 | ));
100 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
101 |
102 | const DialogDescription = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
113 |
114 | const DialogClose = DialogPrimitive.Close;
115 |
116 | export {
117 | Dialog,
118 | DialogClose,
119 | DialogTrigger,
120 | DialogContent,
121 | DialogHeader,
122 | DialogFooter,
123 | DialogTitle,
124 | DialogDescription,
125 | };
126 |
--------------------------------------------------------------------------------
/packages/ui/src/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { Check, ChevronRight, Circle } from "lucide-react";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------
/packages/ui/src/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import type * as LabelPrimitive from "@radix-ui/react-label";
5 | import { Slot } from "@radix-ui/react-slot";
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | type ControllerProps,
11 | type FieldPath,
12 | type FieldValues,
13 | } from "react-hook-form";
14 |
15 | import { Label } from "./label";
16 | import { cn } from "./utils/cn";
17 |
18 | const Form = FormProvider;
19 |
20 | interface FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath,
23 | > {
24 | name: TName;
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue,
29 | );
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath,
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext);
46 | const itemContext = React.useContext(FormItemContext);
47 | const { getFieldState, formState } = useFormContext();
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState);
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ");
53 | }
54 |
55 | const { id } = itemContext;
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | };
65 | };
66 |
67 | interface FormItemContextValue {
68 | id: string;
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue,
73 | );
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId();
80 |
81 | return (
82 |
83 |
84 |
85 | );
86 | });
87 | FormItem.displayName = "FormItem";
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField();
94 |
95 | return (
96 |
102 | );
103 | });
104 | FormLabel.displayName = "FormLabel";
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } =
111 | useFormField();
112 |
113 | return (
114 |
125 | );
126 | });
127 | FormControl.displayName = "FormControl";
128 |
129 | const FormDescription = React.forwardRef<
130 | HTMLParagraphElement,
131 | React.HTMLAttributes
132 | >(({ className, ...props }, ref) => {
133 | const { formDescriptionId } = useFormField();
134 |
135 | return (
136 |
142 | );
143 | });
144 | FormDescription.displayName = "FormDescription";
145 |
146 | const FormMessage = React.forwardRef<
147 | HTMLParagraphElement,
148 | React.HTMLAttributes
149 | >(({ className, children, ...props }, ref) => {
150 | const { error, formMessageId } = useFormField();
151 | const body = error ? String(error?.message) : children;
152 |
153 | if (!body) {
154 | return null;
155 | }
156 |
157 | return (
158 |
164 | {body}
165 |
166 | );
167 | });
168 | FormMessage.displayName = "FormMessage";
169 |
170 | export {
171 | useFormField,
172 | Form,
173 | FormItem,
174 | FormLabel,
175 | FormControl,
176 | FormDescription,
177 | FormMessage,
178 | FormField,
179 | };
180 |
--------------------------------------------------------------------------------
/packages/ui/src/globe.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef, useState } from "react";
4 | import dynamic from "next/dynamic";
5 | import { OrbitControls } from "@react-three/drei";
6 | import { Canvas, extend, Object3DNode, useThree } from "@react-three/fiber";
7 | import { Color, Fog, PerspectiveCamera, Scene, Vector3 } from "three";
8 | import ThreeGlobe from "three-globe";
9 |
10 | import countries from "./data/globe.json";
11 |
12 | declare module "@react-three/fiber" {
13 | interface ThreeElements {
14 | threeGlobe: Object3DNode;
15 | }
16 | }
17 |
18 | extend({ ThreeGlobe });
19 |
20 | const RING_PROPAGATION_SPEED = 3;
21 | const aspect = 1.2;
22 | const cameraZ = 300;
23 |
24 | type Position = {
25 | order: number;
26 | startLat: number;
27 | startLng: number;
28 | endLat: number;
29 | endLng: number;
30 | arcAlt: number;
31 | color: string | undefined;
32 | };
33 |
34 | export interface GlobeConfig {
35 | pointSize?: number;
36 | globeColor?: string;
37 | showAtmosphere?: boolean;
38 | atmosphereColor?: string;
39 | atmosphereAltitude?: number;
40 | emissive?: string;
41 | emissiveIntensity?: number;
42 | shininess?: number;
43 | polygonColor?: string;
44 | ambientLight?: string;
45 | directionalLeftLight?: string;
46 | directionalTopLight?: string;
47 | pointLight?: string;
48 | arcTime?: number;
49 | arcLength?: number;
50 | rings?: number;
51 | maxRings?: number;
52 | initialPosition?: {
53 | lat: number;
54 | lng: number;
55 | };
56 | autoRotate?: boolean;
57 | autoRotateSpeed?: number;
58 | }
59 |
60 | interface WorldProps {
61 | globeConfig: GlobeConfig;
62 | data: Position[];
63 | }
64 |
65 | let numbersOfRings = [0];
66 |
67 | export function Globe({ globeConfig, data }: WorldProps) {
68 | const [globeData, setGlobeData] = useState<
69 | | {
70 | size: number;
71 | order: number;
72 | color: (t: number) => string;
73 | lat: number;
74 | lng: number;
75 | }[]
76 | | null
77 | >(null);
78 |
79 | const globeRef = useRef(null);
80 |
81 | const defaultProps = {
82 | pointSize: 1,
83 | atmosphereColor: "#ffffff",
84 | showAtmosphere: true,
85 | atmosphereAltitude: 0.1,
86 | polygonColor: "rgba(255,255,255,0.7)",
87 | globeColor: "#1d072e",
88 | emissive: "#000000",
89 | emissiveIntensity: 0.1,
90 | shininess: 0.9,
91 | arcTime: 2000,
92 | arcLength: 0.9,
93 | rings: 1,
94 | maxRings: 3,
95 | ...globeConfig,
96 | };
97 |
98 | useEffect(() => {
99 | if (globeRef.current) {
100 | _buildData();
101 | _buildMaterial();
102 | }
103 | }, [globeRef.current]);
104 |
105 | const _buildMaterial = () => {
106 | if (!globeRef.current) return;
107 |
108 | const globeMaterial = globeRef.current.globeMaterial() as unknown as {
109 | color: Color;
110 | emissive: Color;
111 | emissiveIntensity: number;
112 | shininess: number;
113 | };
114 | globeMaterial.color = new Color(globeConfig.globeColor);
115 | globeMaterial.emissive = new Color(globeConfig.emissive);
116 | globeMaterial.emissiveIntensity = globeConfig.emissiveIntensity || 0.1;
117 | globeMaterial.shininess = globeConfig.shininess || 0.9;
118 | };
119 | const _buildData = () => {
120 | const arcs = data;
121 | let points = [];
122 | for (let i = 0; i < arcs.length; i++) {
123 | const arc = arcs[i];
124 |
125 | // @ts-ignore
126 | const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
127 | points.push({
128 | size: defaultProps.pointSize,
129 | // @ts-ignore
130 | order: arc.order,
131 | color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
132 | // @ts-ignore
133 | lat: arc.startLat,
134 | // @ts-ignore
135 | lng: arc.startLng,
136 | });
137 | points.push({
138 | size: defaultProps.pointSize,
139 | // @ts-ignore
140 | order: arc.order,
141 | color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
142 | // @ts-ignore
143 | lat: arc.endLat,
144 | // @ts-ignore
145 | lng: arc.endLng,
146 | });
147 | }
148 |
149 | // remove duplicates for same lat and lng
150 | const filteredPoints = points.filter(
151 | (v, i, a) =>
152 | a.findIndex((v2) =>
153 | ["lat", "lng"].every(
154 | (k) => v2[k as "lat" | "lng"] === v[k as "lat" | "lng"],
155 | ),
156 | ) === i,
157 | );
158 |
159 | setGlobeData(filteredPoints);
160 | };
161 |
162 | useEffect(() => {
163 | if (globeRef.current && globeData) {
164 | globeRef.current
165 | .hexPolygonsData(countries.features)
166 | .hexPolygonResolution(3)
167 | .hexPolygonMargin(0.7)
168 | .showAtmosphere(defaultProps.showAtmosphere)
169 | .atmosphereColor(defaultProps.atmosphereColor)
170 | .atmosphereAltitude(defaultProps.atmosphereAltitude)
171 | .hexPolygonColor((e) => {
172 | return defaultProps.polygonColor;
173 | });
174 | startAnimation();
175 | }
176 | }, [globeData]);
177 |
178 | const startAnimation = () => {
179 | if (!globeRef.current || !globeData) return;
180 |
181 | globeRef.current
182 | .arcsData(data)
183 | .arcStartLat((d) => (d as { startLat: number }).startLat * 1)
184 | .arcStartLng((d) => (d as { startLng: number }).startLng * 1)
185 | .arcEndLat((d) => (d as { endLat: number }).endLat * 1)
186 | .arcEndLng((d) => (d as { endLng: number }).endLng * 1)
187 | .arcColor((e: any) => (e as { color: string }).color)
188 | .arcAltitude((e) => {
189 | return (e as { arcAlt: number }).arcAlt * 1;
190 | })
191 | // @ts-ignore
192 | .arcStroke((e) => {
193 | return [0.32, 0.28, 0.3][Math.round(Math.random() * 2)];
194 | })
195 | .arcDashLength(defaultProps.arcLength)
196 | .arcDashInitialGap((e) => (e as { order: number }).order * 1)
197 | .arcDashGap(15)
198 | .arcDashAnimateTime((e) => defaultProps.arcTime);
199 |
200 | globeRef.current
201 | .pointsData(data)
202 | .pointColor((e) => (e as { color: string }).color)
203 | .pointsMerge(true)
204 | .pointAltitude(0.0)
205 | .pointRadius(2);
206 |
207 | globeRef.current
208 | .ringsData([])
209 | .ringColor((e: any) => (t: any) => e.color(t))
210 | .ringMaxRadius(defaultProps.maxRings)
211 | .ringPropagationSpeed(RING_PROPAGATION_SPEED)
212 | .ringRepeatPeriod(
213 | (defaultProps.arcTime * defaultProps.arcLength) / defaultProps.rings,
214 | );
215 | };
216 |
217 | useEffect(() => {
218 | if (!globeRef.current || !globeData) return;
219 |
220 | const interval = setInterval(() => {
221 | if (!globeRef.current || !globeData) return;
222 | numbersOfRings = genRandomNumbers(
223 | 0,
224 | data.length,
225 | Math.floor((data.length * 4) / 5),
226 | );
227 |
228 | globeRef.current.ringsData(
229 | globeData.filter((d, i) => numbersOfRings.includes(i)),
230 | );
231 | }, 2000);
232 |
233 | return () => {
234 | clearInterval(interval);
235 | };
236 | }, [globeRef.current, globeData]);
237 |
238 | return (
239 | <>
240 |
241 | >
242 | );
243 | }
244 |
245 | export function WebGLRendererConfig() {
246 | const { gl, size } = useThree();
247 |
248 | useEffect(() => {
249 | gl.setPixelRatio(window.devicePixelRatio);
250 | gl.setSize(size.width, size.height);
251 | gl.setClearColor(0xffaaff, 0);
252 | }, []);
253 |
254 | return null;
255 | }
256 |
257 | export function World(props: WorldProps) {
258 | const { globeConfig } = props;
259 | const scene = new Scene();
260 | scene.fog = new Fog(0xffffff, 400, 2000);
261 | return (
262 |
263 |
264 |
265 |
269 |
273 |
278 |
279 |
289 |
290 | );
291 | }
292 |
293 | export function hexToRgb(hex: string) {
294 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
295 | hex = hex.replace(shorthandRegex, function (m, r, g, b) {
296 | return r + r + g + g + b + b;
297 | });
298 |
299 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
300 | return result
301 | ? {
302 | // @ts-ignore
303 | r: parseInt(result[1], 16),
304 | // @ts-ignore
305 | g: parseInt(result[2], 16),
306 | // @ts-ignore
307 | b: parseInt(result[3], 16),
308 | }
309 | : null;
310 | }
311 |
312 | export function genRandomNumbers(min: number, max: number, count: number) {
313 | const arr = [];
314 | while (arr.length < count) {
315 | const r = Math.floor(Math.random() * (max - min)) + min;
316 | if (arr.indexOf(r) === -1) arr.push(r);
317 | }
318 |
319 | return arr;
320 | }
321 |
322 | // export const Globe = dynamic(
323 | // () => Promise.resolve(GlobeImpl),
324 | // {
325 | // ssr: false,
326 | // }
327 | // );
328 | //
329 | // export const World = dynamic(
330 | // () => Promise.resolve(WorldImpl),
331 | // {
332 | // ssr: false,
333 | // }
334 | // );
335 |
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | export { cn } from "./utils/cn";
2 |
--------------------------------------------------------------------------------
/packages/ui/src/infinite-moving-cards.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import * as React from "react";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | export const InfiniteMovingCards = ({
9 | items,
10 | direction = "left",
11 | speed = "fast",
12 | pauseOnHover = true,
13 | className,
14 | }: {
15 | items: {
16 | quote: string;
17 | name: string;
18 | title: string;
19 | }[];
20 | direction?: "left" | "right";
21 | speed?: "fast" | "normal" | "slow";
22 | pauseOnHover?: boolean;
23 | className?: string;
24 | }) => {
25 | const containerRef = React.useRef(null);
26 | const scrollerRef = React.useRef(null);
27 |
28 | useEffect(() => {
29 | addAnimation();
30 | }, []);
31 | const [start, setStart] = useState(false);
32 | function addAnimation() {
33 | if (containerRef.current && scrollerRef.current) {
34 | const scrollerContent = Array.from(scrollerRef.current.children);
35 |
36 | scrollerContent.forEach((item) => {
37 | const duplicatedItem = item.cloneNode(true);
38 | if (scrollerRef.current) {
39 | scrollerRef.current.appendChild(duplicatedItem);
40 | }
41 | });
42 |
43 | getDirection();
44 | getSpeed();
45 | setStart(true);
46 | }
47 | }
48 | const getDirection = () => {
49 | if (containerRef.current) {
50 | if (direction === "left") {
51 | containerRef.current.style.setProperty(
52 | "--animation-direction",
53 | "forwards",
54 | );
55 | } else {
56 | containerRef.current.style.setProperty(
57 | "--animation-direction",
58 | "reverse",
59 | );
60 | }
61 | }
62 | };
63 | const getSpeed = () => {
64 | if (containerRef.current) {
65 | if (speed === "fast") {
66 | containerRef.current.style.setProperty("--animation-duration", "20s");
67 | } else if (speed === "normal") {
68 | containerRef.current.style.setProperty("--animation-duration", "40s");
69 | } else {
70 | containerRef.current.style.setProperty("--animation-duration", "80s");
71 | }
72 | }
73 | };
74 | return (
75 |
82 |
90 | {items.map((item) => (
91 |
99 |
100 |
104 |
105 | {item.quote}
106 |
107 |
108 |
109 |
110 | {item.name}
111 |
112 |
113 | {item.title}
114 |
115 |
116 |
117 |
118 |
119 | ))}
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/packages/ui/src/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "./utils/cn";
4 |
5 | export type InputProps = React.InputHTMLAttributes;
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | },
21 | );
22 | Input.displayName = "Input";
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/packages/ui/src/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/packages/ui/src/meteors.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { cn } from "./utils/cn";
4 |
5 | export const Meteors = ({
6 | number,
7 | className,
8 | }: {
9 | number?: number;
10 | className?: string;
11 | }) => {
12 | const meteors = new Array(number ?? 20).fill(true);
13 | return (
14 | <>
15 | {meteors.map((el, idx) => (
16 |
30 | ))}
31 | >
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/packages/ui/src/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/packages/ui/src/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/packages/ui/src/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { Check, ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
50 |
57 | {children}
58 |
59 |
60 |
61 | ));
62 | SelectContent.displayName = SelectPrimitive.Content.displayName;
63 |
64 | const SelectLabel = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
75 |
76 | const SelectItem = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, children, ...props }, ref) => (
80 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {children}
95 |
96 | ));
97 | SelectItem.displayName = SelectPrimitive.Item.displayName;
98 |
99 | const SelectSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
110 |
111 | export {
112 | Select,
113 | SelectGroup,
114 | SelectValue,
115 | SelectTrigger,
116 | SelectContent,
117 | SelectLabel,
118 | SelectItem,
119 | SelectSeparator,
120 | };
121 |
--------------------------------------------------------------------------------
/packages/ui/src/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "./utils/cn";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const portalVariants = cva("fixed inset-0 z-50 flex", {
15 | variants: {
16 | position: {
17 | top: "items-start",
18 | bottom: "items-end",
19 | left: "justify-start",
20 | right: "justify-end",
21 | },
22 | },
23 | defaultVariants: { position: "right" },
24 | });
25 |
26 | interface SheetPortalProps
27 | extends SheetPrimitive.DialogPortalProps,
28 | VariantProps {}
29 |
30 | const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
31 |
32 | {children}
33 |
34 | );
35 | SheetPortal.displayName = SheetPrimitive.Portal.displayName;
36 |
37 | const SheetOverlay = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, ...props }, ref) => (
41 |
49 | ));
50 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
51 |
52 | const sheetVariants = cva(
53 | "fixed z-50 scale-100 gap-4 bg-background p-6 opacity-100 shadow-lg border",
54 | {
55 | variants: {
56 | position: {
57 | top: "animate-in slide-in-from-top w-full duration-300",
58 | bottom: "animate-in slide-in-from-bottom w-full duration-300",
59 | left: "animate-in slide-in-from-left h-full duration-300",
60 | right: "animate-in slide-in-from-right h-full duration-300",
61 | },
62 | size: {
63 | content: "",
64 | default: "",
65 | sm: "",
66 | lg: "",
67 | xl: "",
68 | full: "",
69 | },
70 | },
71 | compoundVariants: [
72 | {
73 | position: ["top", "bottom"],
74 | size: "content",
75 | class: "max-h-screen",
76 | },
77 | {
78 | position: ["top", "bottom"],
79 | size: "default",
80 | class: "h-1/3",
81 | },
82 | {
83 | position: ["top", "bottom"],
84 | size: "sm",
85 | class: "h-1/4",
86 | },
87 | {
88 | position: ["top", "bottom"],
89 | size: "lg",
90 | class: "h-1/2",
91 | },
92 | {
93 | position: ["top", "bottom"],
94 | size: "xl",
95 | class: "h-5/6",
96 | },
97 | {
98 | position: ["top", "bottom"],
99 | size: "full",
100 | class: "h-screen",
101 | },
102 | {
103 | position: ["right", "left"],
104 | size: "content",
105 | class: "max-w-screen",
106 | },
107 | {
108 | position: ["right", "left"],
109 | size: "default",
110 | class: "w-1/3",
111 | },
112 | {
113 | position: ["right", "left"],
114 | size: "sm",
115 | class: "w-1/4",
116 | },
117 | {
118 | position: ["right", "left"],
119 | size: "lg",
120 | class: "w-1/2",
121 | },
122 | {
123 | position: ["right", "left"],
124 | size: "xl",
125 | class: "w-5/6",
126 | },
127 | {
128 | position: ["right", "left"],
129 | size: "full",
130 | class: "w-screen",
131 | },
132 | ],
133 | defaultVariants: {
134 | position: "right",
135 | size: "default",
136 | },
137 | },
138 | );
139 |
140 | export interface DialogContentProps
141 | extends React.ComponentPropsWithoutRef,
142 | VariantProps {}
143 |
144 | const SheetContent = React.forwardRef<
145 | React.ElementRef,
146 | DialogContentProps
147 | >(({ position, size, className, children, ...props }, ref) => (
148 |
149 |
150 |
155 | {children}
156 |
157 |
158 | Close
159 |
160 |
161 |
162 | ));
163 | SheetContent.displayName = SheetPrimitive.Content.displayName;
164 |
165 | const SheetHeader = ({
166 | className,
167 | ...props
168 | }: React.HTMLAttributes) => (
169 |
176 | );
177 | SheetHeader.displayName = "SheetHeader";
178 |
179 | const SheetFooter = ({
180 | className,
181 | ...props
182 | }: React.HTMLAttributes) => (
183 |
190 | );
191 | SheetFooter.displayName = "SheetFooter";
192 |
193 | const SheetTitle = React.forwardRef<
194 | React.ElementRef,
195 | React.ComponentPropsWithoutRef
196 | >(({ className, ...props }, ref) => (
197 |
202 | ));
203 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
204 |
205 | const SheetDescription = React.forwardRef<
206 | React.ElementRef,
207 | React.ComponentPropsWithoutRef
208 | >(({ className, ...props }, ref) => (
209 |
214 | ));
215 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
216 |
217 | const SheetClose = SheetPrimitive.Close;
218 |
219 | export {
220 | Sheet,
221 | SheetTrigger,
222 | SheetClose,
223 | SheetContent,
224 | SheetHeader,
225 | SheetFooter,
226 | SheetTitle,
227 | SheetDescription,
228 | };
229 |
--------------------------------------------------------------------------------
/packages/ui/src/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "./utils/cn";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/packages/ui/src/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "@saasfly/ui";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/packages/ui/src/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "./utils/cn";
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = "Table";
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = "TableHeader";
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = "TableBody";
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 |
48 | ));
49 | TableFooter.displayName = "TableFooter";
50 |
51 | const TableRow = React.forwardRef<
52 | HTMLTableRowElement,
53 | React.HTMLAttributes & { disabled?: boolean }
54 | >(({ className, disabled, ...props }, ref) => (
55 |
64 | ));
65 | TableRow.displayName = "TableRow";
66 |
67 | const TableHead = React.forwardRef<
68 | HTMLTableCellElement,
69 | React.ThHTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
79 | ));
80 | TableHead.displayName = "TableHead";
81 |
82 | const TableCell = React.forwardRef<
83 | HTMLTableCellElement,
84 | React.TdHTMLAttributes
85 | >(({ className, ...props }, ref) => (
86 |
91 | ));
92 | TableCell.displayName = "TableCell";
93 |
94 | const TableCaption = React.forwardRef<
95 | HTMLTableCaptionElement,
96 | React.HTMLAttributes
97 | >(({ className, ...props }, ref) => (
98 |
103 | ));
104 | TableCaption.displayName = "TableCaption";
105 |
106 | export {
107 | Table,
108 | TableHeader,
109 | TableBody,
110 | TableFooter,
111 | TableHead,
112 | TableRow,
113 | TableCell,
114 | TableCaption,
115 | };
116 |
--------------------------------------------------------------------------------
/packages/ui/src/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/packages/ui/src/text-generate-effect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import dynamic from "next/dynamic";
5 | import { motion, stagger, useAnimate } from "framer-motion";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | const TextGenerateEffectImpl = ({
10 | words,
11 | className,
12 | }: {
13 | words: string;
14 | className?: string;
15 | }) => {
16 | const [scope, animate] = useAnimate();
17 | const wordsArray = words.split(" ");
18 |
19 | useEffect(() => {
20 | void animate(
21 | "span",
22 | {
23 | opacity: 1,
24 | },
25 | {
26 | duration: 2,
27 | delay: stagger(0.1),
28 | },
29 | );
30 | }, [scope.current, words]);
31 |
32 | const renderWords = () => {
33 | return (
34 |
35 | {wordsArray.map((word, idx) => {
36 | return (
37 |
41 | {word}{" "}
42 |
43 | );
44 | })}
45 |
46 | );
47 | };
48 |
49 | return (
50 |
51 |
52 |
53 | {renderWords()}
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export const TextGenerateEffect = dynamic(
61 | () => Promise.resolve(TextGenerateEffectImpl),
62 | {
63 | ssr: false,
64 | },
65 | );
66 |
--------------------------------------------------------------------------------
/packages/ui/src/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ToastPrimitives from "@radix-ui/react-toast";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { X } from "lucide-react";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "bg-background border",
31 | destructive:
32 | "group destructive border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | },
39 | );
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | );
53 | });
54 | Toast.displayName = ToastPrimitives.Root.displayName;
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | ToastAction.displayName = ToastPrimitives.Action.displayName;
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ));
87 | ToastClose.displayName = ToastPrimitives.Close.displayName;
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ));
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ));
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef;
114 |
115 | type ToastActionElement = React.ReactElement;
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | };
128 |
--------------------------------------------------------------------------------
/packages/ui/src/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "./toast";
11 | import { useToast } from "./use-toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/ui/src/typewriter-effect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect } from "react";
4 | import dynamic from "next/dynamic";
5 | import { motion, stagger, useAnimate, useInView } from "framer-motion";
6 |
7 | import { cn } from "./utils/cn";
8 |
9 | export const TypewriterEffectImpl = ({
10 | words,
11 | className,
12 | cursorClassName,
13 | }: {
14 | words: {
15 | text: string;
16 | className?: string;
17 | }[];
18 | className?: string;
19 | cursorClassName?: string;
20 | }) => {
21 | // split text inside of words into array of characters
22 | const wordsArray = words.map((word) => {
23 | return {
24 | ...word,
25 | text: word.text.split(""),
26 | };
27 | });
28 |
29 | const [scope, animate] = useAnimate();
30 | const isInView = useInView(scope);
31 | useEffect(() => {
32 | if (isInView) {
33 | void animate(
34 | "span",
35 | {
36 | display: "inline-block",
37 | opacity: 1,
38 | width: "fit-content",
39 | },
40 | {
41 | duration: 0.3,
42 | delay: stagger(0.1),
43 | ease: "easeInOut",
44 | },
45 | );
46 | }
47 | }, [isInView]);
48 |
49 | const renderWords = () => {
50 | return (
51 |
52 | {wordsArray.map((word, idx) => (
53 |
54 | {word.text.map((char, index) => (
55 |
63 | {char}
64 |
65 | ))}
66 |
67 |
68 | ))}
69 |
70 | );
71 | };
72 |
73 | return (
74 |
75 | {renderWords()}
76 |
93 |
94 | );
95 | };
96 |
97 | export const TextGenerateEffect = dynamic(
98 | () => Promise.resolve(TypewriterEffectImpl),
99 | {
100 | ssr: false,
101 | },
102 | );
103 |
--------------------------------------------------------------------------------
/packages/ui/src/use-toast.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react";
3 |
4 | import type { ToastActionElement, ToastProps } from "./toast";
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"];
43 | toastId?: ToasterToast["id"];
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
85 | ),
86 | };
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t,
110 | ),
111 | };
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: ((state: State) => void)[] = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/packages/ui/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is not used for any compilation purpose, it is only used
3 | * for Tailwind Intellisense & Autocompletion in the source files
4 | */
5 | import type { Config } from "tailwindcss";
6 |
7 | import baseConfig from "@saasfly/tailwind-config";
8 |
9 | export default {
10 | content: baseConfig.content,
11 | presets: [baseConfig],
12 | } satisfies Config;
13 |
--------------------------------------------------------------------------------
/packages/ui/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 |
--------------------------------------------------------------------------------
/saasfly-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
16 |
17 |
19 |
92 |
99 |
106 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/tooling/eslint-config/base.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | const config = {
3 | extends: [
4 | "turbo",
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended-type-checked",
7 | "plugin:@typescript-eslint/stylistic-type-checked",
8 | "prettier",
9 | ],
10 | env: {
11 | es2022: true,
12 | node: true,
13 | },
14 | parser: "@typescript-eslint/parser",
15 | parserOptions: {
16 | project: true,
17 | },
18 | plugins: ["@typescript-eslint", "import"],
19 | rules: {
20 | "turbo/no-undeclared-env-vars": "off",
21 | "import/consistent-type-specifier-style": "off",
22 | "@typescript-eslint/no-unused-vars": [
23 | "error",
24 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
25 | ],
26 | "@typescript-eslint/consistent-type-imports": [
27 | "warn",
28 | { prefer: "type-imports", fixStyle: "separate-type-imports" },
29 | ],
30 | "@typescript-eslint/no-misused-promises": [
31 | 2,
32 | { checksVoidReturn: { attributes: false } },
33 | ],
34 | },
35 | ignorePatterns: [
36 | "**/.eslintrc.cjs",
37 | "**/*.config.js",
38 | "**/*.config.cjs",
39 | ".next",
40 | "dist",
41 | "pnpm-lock.yaml",
42 | "bun.lockb",
43 | ],
44 | reportUnusedDisableDirectives: true,
45 | };
46 |
47 | module.exports = config;
48 |
--------------------------------------------------------------------------------
/tooling/eslint-config/nextjs.js:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.Config} */
2 | const config = {
3 | extends: ["plugin:@next/next/recommended"],
4 | rules: {
5 | "@next/next/no-html-link-for-pages": "off",
6 | },
7 | };
8 |
9 | module.exports = config;
10 |
--------------------------------------------------------------------------------
/tooling/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/eslint-config",
3 | "version": "0.0.1",
4 | "private": true,
5 | "files": [
6 | "./base.js",
7 | "./nextjs.js",
8 | "./react.js"
9 | ],
10 | "scripts": {
11 | "clean": "rm -rf .turbo node_modules",
12 | "lint": "eslint .",
13 | "format": "prettier --check '**/*.{js,json}' ",
14 | "typecheck": "tsc --noEmit"
15 | },
16 | "dependencies": {
17 | "@next/eslint-plugin-next": "14.1.4",
18 | "@types/eslint": "8.56.7",
19 | "@typescript-eslint/eslint-plugin": "7.5.0",
20 | "@typescript-eslint/parser": "7.5.0",
21 | "eslint-config-prettier": "9.1.0",
22 | "eslint-config-turbo": "1.13.2",
23 | "eslint-plugin-import": "2.29.1",
24 | "eslint-plugin-jsx-a11y": "6.8.0",
25 | "eslint-plugin-react": "7.34.1",
26 | "eslint-plugin-react-hooks": "4.6.0"
27 | },
28 | "devDependencies": {
29 | "@saasfly/prettier-config": "workspace:*",
30 | "@saasfly/typescript-config": "workspace:*",
31 | "eslint": "8.57.0",
32 | "typescript": "5.4.5"
33 | },
34 | "eslintConfig": {
35 | "root": true,
36 | "extends": [
37 | "./base.js"
38 | ]
39 | },
40 | "prettier": "@saasfly/prettier-config"
41 | }
42 |
--------------------------------------------------------------------------------
/tooling/eslint-config/react.js:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.Config} */
2 | const config = {
3 | extends: [
4 | "plugin:react/recommended",
5 | "plugin:react-hooks/recommended",
6 | "plugin:jsx-a11y/recommended",
7 | ],
8 | rules: {
9 | "react/prop-types": "off",
10 | },
11 | globals: {
12 | React: "writable",
13 | },
14 | settings: {
15 | react: {
16 | version: "detect",
17 | },
18 | },
19 | env: {
20 | browser: true,
21 | },
22 | };
23 |
24 | module.exports = config;
25 |
--------------------------------------------------------------------------------
/tooling/eslint-config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/prettier-config/index.mjs:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 |
3 | /** @typedef {import("prettier").Config} PrettierConfig */
4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */
5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
6 |
7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
8 | const config = {
9 | plugins: [
10 | "@ianvs/prettier-plugin-sort-imports",
11 | "prettier-plugin-tailwindcss",
12 | ],
13 | tailwindConfig: fileURLToPath(
14 | new URL("../../tooling/tailwind-config/index.ts", import.meta.url),
15 | ),
16 | importOrder: [
17 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
18 | "^(next/(.*)$)|^(next$)",
19 | "",
20 | "",
21 | "^@saasfly/(.*)$",
22 | "",
23 | "^~/",
24 | "^[../]",
25 | "^[./]",
26 | ],
27 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
28 | importOrderTypeScriptVersion: "5.4.5",
29 | };
30 |
31 | export default config;
32 |
--------------------------------------------------------------------------------
/tooling/prettier-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/prettier-config",
3 | "private": true,
4 | "version": "0.1.0",
5 | "main": "index.mjs",
6 | "scripts": {
7 | "clean": "rm -rf .turbo node_modules",
8 | "format": "prettier --check '**/*.{mjs,json}' ",
9 | "typecheck": "tsc --noEmit"
10 | },
11 | "dependencies": {
12 | "@ianvs/prettier-plugin-sort-imports": "4.2.1",
13 | "prettier": "3.2.5",
14 | "prettier-plugin-tailwindcss": "0.5.13"
15 | },
16 | "devDependencies": {
17 | "@saasfly/typescript-config": "workspace:*",
18 | "typescript": "5.4.5"
19 | },
20 | "prettier": "@saasfly/prettier-config"
21 | }
22 |
--------------------------------------------------------------------------------
/tooling/prettier-config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/tailwind-config/.eslintignore:
--------------------------------------------------------------------------------
1 | index.ts
--------------------------------------------------------------------------------
/tooling/tailwind-config/index.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette";
3 |
4 | export default {
5 | darkMode: ["class"],
6 | content: ["src/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: "2rem",
11 | screens: {
12 | "2xl": "1400px",
13 | },
14 | },
15 | extend: {
16 | colors: {
17 | border: "hsl(var(--border))",
18 | input: "hsl(var(--input))",
19 | ring: "hsl(var(--ring))",
20 | background: "hsl(var(--background))",
21 | foreground: "hsl(var(--foreground))",
22 | primary: {
23 | DEFAULT: "hsl(var(--primary))",
24 | foreground: "hsl(var(--primary-foreground))",
25 | },
26 | secondary: {
27 | DEFAULT: "hsl(var(--secondary))",
28 | foreground: "hsl(var(--secondary-foreground))",
29 | },
30 | destructive: {
31 | DEFAULT: "hsl(var(--destructive))",
32 | foreground: "hsl(var(--destructive-foreground))",
33 | },
34 | muted: {
35 | DEFAULT: "hsl(var(--muted))",
36 | foreground: "hsl(var(--muted-foreground))",
37 | },
38 | accent: {
39 | DEFAULT: "hsl(var(--accent))",
40 | foreground: "hsl(var(--accent-foreground))",
41 | },
42 | popover: {
43 | DEFAULT: "hsl(var(--popover))",
44 | foreground: "hsl(var(--popover-foreground))",
45 | },
46 | card: {
47 | DEFAULT: "hsl(var(--card))",
48 | foreground: "hsl(var(--card-foreground))",
49 | },
50 | },
51 | borderRadius: {
52 | lg: "var(--radius)",
53 | md: "calc(var(--radius) - 2px)",
54 | sm: "calc(var(--radius) - 4px)",
55 | },
56 | keyframes: {
57 | "accordion-down": {
58 | from: { height: "0" },
59 | to: { height: "var(--radix-accordion-content-height)" },
60 | },
61 | "accordion-up": {
62 | from: { height: "var(--radix-accordion-content-height)" },
63 | to: { height: "0" },
64 | },
65 | spin: {
66 | "0%": {
67 | rotate: "0deg",
68 | },
69 | "15%, 35%": {
70 | rotate: "90deg",
71 | },
72 | "65%, 85%": {
73 | rotate: "270deg",
74 | },
75 | "100%": {
76 | rotate: "360deg",
77 | },
78 | },
79 | marquee: {
80 | from: { transform: "translateX(0)" },
81 | to: { transform: "translateX(calc(-50% - var(--gap)/2))" },
82 | },
83 | slide: {
84 | to: {
85 | transform: "translate(calc(100cqw - 100%), 0)",
86 | },
87 | },
88 | meteor: {
89 | "0%": { transform: "rotate(215deg) translateX(0)", opacity: "1" },
90 | "70%": { opacity: "1" },
91 | "100%": {
92 | transform: "rotate(215deg) translateX(-500px)",
93 | opacity: "0",
94 | },
95 | },
96 | scroll: {
97 | to: {
98 | transform: "translate(calc(-50% - 0.5rem))",
99 | },
100 | },
101 | },
102 | animation: {
103 | "accordion-down": "accordion-down 0.2s ease-out",
104 | "accordion-up": "accordion-up 0.2s ease-out",
105 | spinLinear: "spin calc(var(--speed) * 2) infinite linear",
106 | slide: "slide var(--speed) ease-in-out infinite alternate",
107 | marquee: "marquee var(--duration) linear infinite",
108 | "meteor-effect": "meteor 5s linear infinite",
109 | scroll:
110 | "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite",
111 | },
112 | },
113 | },
114 | plugins: [require("tailwindcss-animate"), addVariablesForColors],
115 | } satisfies Config;
116 |
117 | function addVariablesForColors({ addBase, theme }: any) {
118 | const allColors = flattenColorPalette(theme("colors"));
119 | let newVars = Object.fromEntries(
120 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
121 | );
122 |
123 | addBase({
124 | ":root": newVars,
125 | });
126 | }
127 |
--------------------------------------------------------------------------------
/tooling/tailwind-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/tailwind-config",
3 | "private": true,
4 | "version": "0.1.0",
5 | "main": "index.ts",
6 | "files": [
7 | "index.ts",
8 | "postcss.js"
9 | ],
10 | "scripts": {
11 | "clean": "rm -rf .turbo node_modules",
12 | "lint": "eslint ",
13 | "format": "prettier --check '**/*.{ts,json}' ",
14 | "typecheck": "tsc --noEmit"
15 | },
16 | "dependencies": {
17 | "autoprefixer": "10.4.19",
18 | "postcss": "8.4.38",
19 | "tailwindcss": "3.4.3"
20 | },
21 | "devDependencies": {
22 | "@saasfly/eslint-config": "workspace:*",
23 | "@saasfly/prettier-config": "workspace:*",
24 | "@saasfly/typescript-config": "workspace:*",
25 | "eslint": "8.57.0",
26 | "prettier": "3.2.5",
27 | "typescript": "5.4.5"
28 | },
29 | "eslintConfig": {
30 | "root": true,
31 | "extends": [
32 | "@saasfly/eslint-config/base"
33 | ]
34 | },
35 | "prettier": "@saasfly/prettier-config"
36 | }
37 |
--------------------------------------------------------------------------------
/tooling/tailwind-config/postcss.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/tooling/tailwind-config/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 |
--------------------------------------------------------------------------------
/tooling/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "ES2017",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "checkJs": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "noUncheckedIndexedAccess": true
20 | },
21 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"]
22 | }
23 |
--------------------------------------------------------------------------------
/tooling/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/typescript-config",
3 | "version": "0.0.1",
4 | "private": true,
5 | "files": [
6 | "base.json"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "globalDependencies": ["**/.env"],
4 | "pipeline": {
5 | "topo": {
6 | "dependsOn": ["^topo"]
7 | },
8 | "build": {
9 | "dependsOn": ["^build"],
10 | "outputs": [".next/**", "!.next/cache/**", "next-env.d.ts"]
11 | },
12 | "dev": {
13 | "persistent": true,
14 | "cache": false
15 | },
16 | "format": {
17 | "outputs": ["node_modules/.cache/.prettiercache"],
18 | "outputMode": "new-only"
19 | },
20 | "lint": {
21 | "dependsOn": ["^topo"],
22 | "outputs": ["node_modules/.cache/.eslintcache"]
23 | },
24 | "typecheck": {
25 | "dependsOn": ["^topo"],
26 | "outputs": ["node_modules/.cache/tsbuildinfo.json"]
27 | },
28 | "clean": {
29 | "cache": false
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------