├── tooling
├── tailwind-config
│ ├── .eslintignore
│ ├── postcss.js
│ ├── tsconfig.json
│ └── package.json
├── typescript-config
│ ├── package.json
│ └── base.json
├── eslint-config
│ ├── tsconfig.json
│ ├── nextjs.js
│ ├── react.js
│ ├── package.json
│ └── base.js
└── prettier-config
│ ├── tsconfig.json
│ ├── package.json
│ └── index.mjs
├── packages
├── common
│ ├── .eslintignore
│ ├── src
│ │ ├── email.ts
│ │ ├── index.ts
│ │ ├── config
│ │ │ └── site.ts
│ │ ├── env.mjs
│ │ ├── emails
│ │ │ └── magic-link-email.tsx
│ │ └── subscriptions.ts
│ ├── tsconfig.json
│ └── package.json
├── stripe
│ ├── .eslintignore
│ ├── tsconfig.json
│ ├── src
│ │ ├── plans.ts
│ │ └── env.mjs
│ └── package.json
├── api
│ ├── .eslintignore
│ ├── src
│ │ ├── root.ts
│ │ ├── router
│ │ │ ├── health_check.ts
│ │ │ ├── auth.ts
│ │ │ └── customer.ts
│ │ ├── edge.ts
│ │ ├── index.ts
│ │ ├── transformer.ts
│ │ ├── trpc.ts
│ │ └── env.mjs
│ ├── tsconfig.json
│ └── package.json
├── auth
│ ├── .eslintignore
│ ├── .prettierignore
│ ├── tsconfig.json
│ ├── db.ts
│ ├── package.json
│ └── env.mjs
├── ui
│ ├── src
│ │ ├── index.ts
│ │ ├── utils
│ │ │ └── cn.ts
│ │ ├── skeleton.tsx
│ │ ├── card-skeleton.tsx
│ │ ├── label.tsx
│ │ ├── input.tsx
│ │ ├── toaster.tsx
│ │ ├── callout.tsx
│ │ ├── animated-gradient-text.tsx
│ │ ├── meteors.tsx
│ │ ├── checkbox.tsx
│ │ ├── popover.tsx
│ │ ├── switch.tsx
│ │ ├── marquee.tsx
│ │ ├── text-generate-effect.tsx
│ │ ├── avatar.tsx
│ │ ├── scroll-area.tsx
│ │ ├── button.tsx
│ │ ├── animated-list.tsx
│ │ ├── alert.tsx
│ │ ├── animated-tooltip.tsx
│ │ ├── tabs.tsx
│ │ ├── text-reveal.tsx
│ │ ├── accordion.tsx
│ │ ├── card.tsx
│ │ └── data-table.tsx
│ ├── tsconfig.json
│ ├── .eslintignore
│ └── tailwind.config.ts
└── db
│ ├── prisma
│ ├── README.md
│ ├── enums.ts
│ └── types.ts
│ ├── tsconfig.json
│ ├── index.ts
│ └── package.json
├── apps
├── nextjs
│ ├── .prettierignore
│ ├── postcss.config.cjs
│ ├── public
│ │ ├── favicon.ico
│ │ └── images
│ │ │ ├── noise.webp
│ │ │ ├── avatars
│ │ │ └── nok8s.jpeg
│ │ │ └── blog
│ │ │ ├── blog-post-1.jpg
│ │ │ ├── blog-post-2.jpg
│ │ │ ├── blog-post-3.jpg
│ │ │ └── blog-post-4.jpg
│ ├── src
│ │ ├── styles
│ │ │ ├── calsans.ttf
│ │ │ ├── fonts
│ │ │ │ ├── Inter-Bold.ttf
│ │ │ │ ├── Inter-Regular.ttf
│ │ │ │ ├── CalSans-SemiBold.ttf
│ │ │ │ ├── CalSans-SemiBold.woff
│ │ │ │ └── CalSans-SemiBold.woff2
│ │ │ ├── mdx.css
│ │ │ ├── theme
│ │ │ │ └── default.css
│ │ │ └── globals.css
│ │ ├── content
│ │ │ ├── authors
│ │ │ │ └── nok8s.mdx
│ │ │ └── docs
│ │ │ │ ├── in-progress.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ └── documentation
│ │ │ │ └── index.mdx
│ │ ├── config
│ │ │ ├── ph-config.ts
│ │ │ ├── site.ts
│ │ │ ├── i18n-config.ts
│ │ │ ├── ui
│ │ │ │ ├── marketing.ts
│ │ │ │ └── dashboard.ts
│ │ │ └── providers.tsx
│ │ ├── lib
│ │ │ ├── validations
│ │ │ │ └── user.ts
│ │ │ ├── currency.ts
│ │ │ ├── use-mounted.ts
│ │ │ ├── utils.ts
│ │ │ ├── use-debounce.tsx
│ │ │ ├── zod-form.tsx
│ │ │ ├── get-dictionary.ts
│ │ │ └── toc.ts
│ │ ├── types
│ │ │ ├── meteors.d.ts
│ │ │ ├── next-auth.d.ts
│ │ │ ├── k8s.d.ts
│ │ │ └── index.d.ts
│ │ ├── components
│ │ │ ├── theme-provider.tsx
│ │ │ ├── base-item.tsx
│ │ │ ├── word-reveal.tsx
│ │ │ ├── modal-provider.tsx
│ │ │ ├── textGenerateEffect.tsx
│ │ │ ├── card-skeleton.tsx
│ │ │ ├── header.tsx
│ │ │ ├── docs
│ │ │ │ ├── page-header.tsx
│ │ │ │ ├── search.tsx
│ │ │ │ ├── pager.tsx
│ │ │ │ └── sidebar-nav.tsx
│ │ │ ├── tailwind-indicator.tsx
│ │ │ ├── user-avatar.tsx
│ │ │ ├── typewriterEffectSmooth.tsx
│ │ │ ├── document-guide.tsx
│ │ │ ├── card-hover-effect.tsx
│ │ │ ├── shell.tsx
│ │ │ ├── content
│ │ │ │ └── mdx-card.tsx
│ │ │ ├── meteors-card.tsx
│ │ │ ├── site-footer.tsx
│ │ │ ├── price
│ │ │ │ ├── pricing-faq.tsx
│ │ │ │ └── billing-form-button.tsx
│ │ │ ├── locale-change.tsx
│ │ │ ├── mode-toggle.tsx
│ │ │ ├── infiniteMovingCards.tsx
│ │ │ ├── k8s
│ │ │ │ ├── cluster-item.tsx
│ │ │ │ └── cluster-create-button.tsx
│ │ │ ├── modal.tsx
│ │ │ ├── nav.tsx
│ │ │ ├── mobile-nav.tsx
│ │ │ ├── theme-toggle.tsx
│ │ │ ├── sparkles.tsx
│ │ │ ├── empty-placeholder.tsx
│ │ │ ├── shimmer-button.tsx
│ │ │ ├── sign-in-modal.tsx
│ │ │ └── user-account-nav.tsx
│ │ ├── app
│ │ │ ├── admin
│ │ │ │ ├── layout.tsx
│ │ │ │ └── (dashboard)
│ │ │ │ │ └── dashboard
│ │ │ │ │ └── loading.tsx
│ │ │ ├── [lang]
│ │ │ │ ├── (auth)
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── login
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── register
│ │ │ │ │ │ └── page.tsx
│ │ │ │ ├── (dashboard)
│ │ │ │ │ └── dashboard
│ │ │ │ │ │ ├── billing
│ │ │ │ │ │ ├── subscription-form.tsx
│ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── settings
│ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ └── loading.tsx
│ │ │ │ ├── (marketing)
│ │ │ │ │ ├── blog
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── pricing
│ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ ├── (docs)
│ │ │ │ │ ├── docs
│ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ └── (editor)
│ │ │ │ │ └── editor
│ │ │ │ │ ├── cluster
│ │ │ │ │ └── [clusterId]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ ├── robots.ts
│ │ │ └── api
│ │ │ │ ├── auth
│ │ │ │ └── [...nextauth]
│ │ │ │ │ └── route.ts
│ │ │ │ ├── trpc
│ │ │ │ └── edge
│ │ │ │ │ └── [trpc]
│ │ │ │ │ └── route.ts
│ │ │ │ └── webhooks
│ │ │ │ └── stripe
│ │ │ │ └── route.ts
│ │ ├── utils
│ │ │ └── api.ts
│ │ ├── hooks
│ │ │ ├── use-mounted.ts
│ │ │ ├── use-signin-modal.ts
│ │ │ ├── use-lock-body.ts
│ │ │ ├── use-scroll.ts
│ │ │ └── use-media-query.ts
│ │ ├── trpc
│ │ │ ├── client.ts
│ │ │ ├── server.ts
│ │ │ └── shared.ts
│ │ └── env.mjs
│ ├── tailwind.config.ts
│ ├── .eslintignore
│ ├── tsconfig.json
│ └── next.config.mjs
├── .gitignore
└── auth-proxy
│ ├── tsconfig.json
│ ├── .env.example
│ ├── routes
│ └── [...auth].ts
│ └── package.json
├── vercel.json
├── twillot.png
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── tailwind.config.js
├── turbo
└── generators
│ ├── templates
│ ├── tsconfig.json.hbs
│ └── package.json.hbs
│ └── config.ts
├── .gitignore
├── turbo.json
├── LICENSE
├── package.json
├── SECURITY.md
├── .env.example
└── CONTRIBUTING.md
/tooling/tailwind-config/.eslintignore:
--------------------------------------------------------------------------------
1 | index.ts
--------------------------------------------------------------------------------
/packages/common/.eslintignore:
--------------------------------------------------------------------------------
1 | src/subscriptions.ts
--------------------------------------------------------------------------------
/apps/nextjs/.prettierignore:
--------------------------------------------------------------------------------
1 | .next/
2 | .contentlayer/
3 |
--------------------------------------------------------------------------------
/packages/stripe/.eslintignore:
--------------------------------------------------------------------------------
1 | src/index.ts
2 | src/plans.ts
--------------------------------------------------------------------------------
/packages/api/.eslintignore:
--------------------------------------------------------------------------------
1 | src/transformer.ts
2 | src/trpc.ts
--------------------------------------------------------------------------------
/packages/auth/.eslintignore:
--------------------------------------------------------------------------------
1 | auth-rest-adapter.ts
2 | index.ts
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | export { cn } from "./utils/cn";
2 |
--------------------------------------------------------------------------------
/apps/.gitignore:
--------------------------------------------------------------------------------
1 | nextjs/.contentlayer
2 | nextjs/tailwind.config.js
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "silent": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/twillot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/twillot.png
--------------------------------------------------------------------------------
/packages/auth/.prettierignore:
--------------------------------------------------------------------------------
1 | #auth-rest-adapter.ts
2 | .next/
3 | .contentlayer
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 | open_collective: saasfly
3 |
--------------------------------------------------------------------------------
/apps/nextjs/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("@saasfly/tailwind-config/postcss");
2 |
--------------------------------------------------------------------------------
/apps/nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/calsans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/calsans.ttf
--------------------------------------------------------------------------------
/apps/auth-proxy/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "include": ["routes"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/nextjs/public/images/noise.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/noise.webp
--------------------------------------------------------------------------------
/apps/nextjs/src/content/authors/nok8s.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: repo
3 | twitter: repo
4 | avatar: /images/avatars/repo.jpeg
5 | ---
6 |
--------------------------------------------------------------------------------
/packages/db/prisma/README.md:
--------------------------------------------------------------------------------
1 | #NOTE
2 |
3 | ##FAQ
4 | if you found can't generate prisma types, please use bun i -g prisma-kysely
5 |
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/apps/nextjs/public/images/avatars/nok8s.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/avatars/nok8s.jpeg
--------------------------------------------------------------------------------
/tooling/tailwind-config/postcss.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/nextjs/public/images/blog/blog-post-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/blog/blog-post-1.jpg
--------------------------------------------------------------------------------
/apps/nextjs/public/images/blog/blog-post-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/blog/blog-post-2.jpg
--------------------------------------------------------------------------------
/apps/nextjs/public/images/blog/blog-post-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/blog/blog-post-3.jpg
--------------------------------------------------------------------------------
/apps/nextjs/public/images/blog/blog-post-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/public/images/blog/blog-post-4.jpg
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Jadeja/saasfly/main/apps/nextjs/src/styles/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/apps/auth-proxy/.env.example:
--------------------------------------------------------------------------------
1 |
2 | AUTH_SECRET=""
3 | GITHUB_CLIENT_ID=""
4 | GITHUB_CLIENT_SECRET=""
5 | AUTH_REDIRECT_PROXY_URL=""
6 |
7 | NITRO_PRESET="vercel_edge"
--------------------------------------------------------------------------------
/apps/nextjs/src/config/ph-config.ts:
--------------------------------------------------------------------------------
1 | import { PostHog } from "posthog-node";
2 |
3 | export const phConfig = new PostHog("", { host: "https://app.posthog.com" });
4 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const userNameSchema = z.object({
4 | name: z.string().min(3).max(32),
5 | });
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/types/meteors.d.ts:
--------------------------------------------------------------------------------
1 | export interface Meteor {
2 | name: string;
3 | description: string;
4 | button_content: string;
5 | url: string;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/currency.ts:
--------------------------------------------------------------------------------
1 | export const currencySymbol = (curr: string) =>
2 | ({
3 | USD: "$",
4 | EUR: "€",
5 | GBP: "£",
6 | })[curr] ?? curr;
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/content/docs/in-progress.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Not Implemented
3 | description: This page is in progress.
4 | ---
5 |
6 | This site is a work in progress.
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/admin/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode;
3 | }
4 |
5 | export default function AuthLayout({ children }: AuthLayoutProps) {
6 | return
{children}
;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/api/src/root.ts:
--------------------------------------------------------------------------------
1 | import { edgeRouter } from "./edge";
2 | import { mergeRouters } from "./trpc";
3 |
4 | export const appRouter = mergeRouters(edgeRouter);
5 | // export type definition of API
6 | export type AppRouter = typeof appRouter;
7 |
--------------------------------------------------------------------------------
/packages/auth/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode;
3 | }
4 |
5 | export default function AuthLayout({ children }: AuthLayoutProps) {
6 | return {children}
;
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/stripe/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/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/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/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 |
3 | import type { AppRouter } from "@saasfly/api";
4 |
5 | export const api = createTRPCReact();
6 |
7 | export { type RouterInputs, type RouterOutputs } from "@saasfly/api";
8 |
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "prisma", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | setMounted(true);
8 | }, []);
9 |
10 | return mounted;
11 | }
12 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | setMounted(true);
8 | }, []);
9 |
10 | return mounted;
11 | }
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
12 | src/following-pointer.tsx
13 | src/marquee.tsx
14 | src/text-reveal.tsx
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": [
7 | "*.ts",
8 | "src"
9 | ],
10 | "exclude": [
11 | "node_modules"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2 |
3 | import NextAuth from "next-auth";
4 |
5 | import { authOptions } from "@saasfly/auth";
6 |
7 | const handler = NextAuth(authOptions);
8 |
9 | export { handler as GET, handler as POST };
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/db/index.ts:
--------------------------------------------------------------------------------
1 | import { createKysely } from "@vercel/postgres-kysely";
2 |
3 | import type { DB } from "./prisma/types";
4 |
5 | export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
6 |
7 | export * from "./prisma/types";
8 | export * from "./prisma/enums";
9 |
10 | export const db = createKysely();
11 |
--------------------------------------------------------------------------------
/apps/nextjs/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import type { User } from "next-auth";
2 |
3 | type UserId = string;
4 |
5 | declare module "next-auth/jwt" {
6 | interface JWT {
7 | id: UserId;
8 | }
9 | }
10 |
11 | declare module "next-auth" {
12 | interface Session {
13 | user: User & {
14 | id: UserId;
15 | };
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/turbo/generators/templates/tsconfig.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": [
7 | "*.ts",
8 | "src"
9 | ],
10 | "exclude": [
11 | "node_modules"
12 | ]
13 | }
--------------------------------------------------------------------------------
/apps/nextjs/src/config/i18n-config.ts:
--------------------------------------------------------------------------------
1 | export const i18n = {
2 | defaultLocale: "zh",
3 | locales: ["en", "zh", "ko", "ja"],
4 | } as const;
5 |
6 | export type Locale = (typeof i18n)["locales"][number];
7 |
8 | // 新增的映射对象
9 | export const localeMap = {
10 | en: "English",
11 | zh: "中文",
12 | ko: "한국어",
13 | ja: "日本語",
14 | } as const;
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/.eslintignore:
--------------------------------------------------------------------------------
1 | .next/
2 | .contentlayer/
3 | src/components/billing-form.tsx
4 | src/components/content/mdx-components.tsx
5 | src/components/docs/pager.tsx
6 | src/components/k8s/cluster-operation.tsx
7 | src/components/price/pricing-cards.tsx
8 | src/components/user-account-nav.tsx
9 | src/lib/toc.ts
10 | src/middleware.ts
11 | contentlayer.config.ts
--------------------------------------------------------------------------------
/apps/nextjs/src/components/base-item.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@saasfly/ui/skeleton";
2 |
3 | export function BasicItemSkeleton() {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/word-reveal.tsx:
--------------------------------------------------------------------------------
1 | import TextReveal from "@saasfly/ui/text-reveal";
2 |
3 | export function WordReveal() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/hooks/use-signin-modal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface useSigninModalStore {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | export const useSigninModal = create((set) => ({
10 | isOpen: false,
11 | onOpen: () => set({ isOpen: true }),
12 | onClose: () => set({ isOpen: false }),
13 | }));
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { env } from "~/env.mjs";
2 |
3 | export function formatDate(input: string | number): string {
4 | const date = new Date(input);
5 | return date.toLocaleDateString("en-US", {
6 | month: "long",
7 | day: "numeric",
8 | year: "numeric",
9 | });
10 | }
11 |
12 | export function absoluteUrl(path: string) {
13 | return `${env.NEXT_PUBLIC_APP_URL}${path}`;
14 | }
15 |
--------------------------------------------------------------------------------
/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/components/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SignInModal } from "~/components/sign-in-modal";
4 | import { useMounted } from "~/hooks/use-mounted";
5 |
6 | export const ModalProvider = ({ dict }: { dict: Record }) => {
7 | const mounted = useMounted();
8 |
9 | if (!mounted) {
10 | return null;
11 | }
12 |
13 | return (
14 | <>
15 |
16 | >
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/textGenerateEffect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { TextGenerateEffect } from "@saasfly/ui/text-generate-effect";
4 |
5 | const words = `Your complete All-in-One solution for building SaaS services. From coding to product launch, we have
6 | everything you need covered!`;
7 |
8 | const TextGenerateEffects = () => {
9 | return ;
10 | };
11 |
12 | export default TextGenerateEffects;
13 |
--------------------------------------------------------------------------------
/packages/api/src/router/health_check.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import { createTRPCRouter, protectedProcedure } from "../trpc";
4 |
5 | export const helloRouter = createTRPCRouter({
6 | hello: protectedProcedure
7 | .input(
8 | z.object({
9 | text: z.string(),
10 | }),
11 | )
12 | .query((opts: { input: { text: string } }) => {
13 | return {
14 | greeting: `hello ${opts.input.text}`,
15 | };
16 | }),
17 | });
18 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/use-debounce.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useDebounce(value: T, delay: number) {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const timeoutId = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 |
11 | return () => {
12 | clearTimeout(timeoutId);
13 | };
14 | }, [value, delay]);
15 |
16 | return debouncedValue;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/db/prisma/enums.ts:
--------------------------------------------------------------------------------
1 | export const SubscriptionPlan = {
2 | FREE: "FREE",
3 | PRO: "PRO",
4 | BUSINESS: "BUSINESS",
5 | } as const;
6 | export type SubscriptionPlan =
7 | (typeof SubscriptionPlan)[keyof typeof SubscriptionPlan];
8 | export const Status = {
9 | PENDING: "PENDING",
10 | CREATING: "CREATING",
11 | INITING: "INITING",
12 | RUNNING: "RUNNING",
13 | STOPPED: "STOPPED",
14 | DELETED: "DELETED",
15 | } as const;
16 | export type Status = (typeof Status)[keyof typeof Status];
17 |
--------------------------------------------------------------------------------
/packages/api/src/edge.ts:
--------------------------------------------------------------------------------
1 | import { authRouter } from "./router/auth";
2 | import { customerRouter } from "./router/customer";
3 | import { helloRouter } from "./router/health_check";
4 | import { k8sRouter } from "./router/k8s";
5 | import { stripeRouter } from "./router/stripe";
6 | import { createTRPCRouter } from "./trpc";
7 |
8 | export const edgeRouter = createTRPCRouter({
9 | stripe: stripeRouter,
10 | hello: helloRouter,
11 | k8s: k8sRouter,
12 | auth: authRouter,
13 | customer: customerRouter,
14 | });
15 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/zod-form.tsx:
--------------------------------------------------------------------------------
1 | import { zodResolver } from "@hookform/resolvers/zod";
2 | import { useForm, type UseFormProps } from "react-hook-form";
3 | import type { ZodType } from "zod";
4 |
5 | export function useZodForm(
6 | props: Omit, "resolver"> & {
7 | schema: TSchema;
8 | },
9 | ) {
10 | const form = useForm({
11 | ...props,
12 | resolver: zodResolver(props.schema, undefined),
13 | });
14 |
15 | return form;
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/billing/subscription-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 |
5 | import { cn } from "@saasfly/ui";
6 | import { buttonVariants } from "@saasfly/ui/button";
7 |
8 | export function SubscriptionForm(props: {
9 | hasSubscription: boolean;
10 | dict: Record;
11 | }) {
12 | return (
13 |
14 | {props.hasSubscription
15 | ? props.dict.manage_subscription
16 | : props.dict.upgrade}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "~/components/card-skeleton";
2 | import { DashboardHeader } from "~/components/header";
3 | import { DashboardShell } from "~/components/shell";
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/auth-proxy/routes/[...auth].ts:
--------------------------------------------------------------------------------
1 | import { Auth } from "@auth/core";
2 | import GitHub from "@auth/core/providers/github";
3 | import { eventHandler, toWebRequest } from "h3";
4 |
5 | export default eventHandler(async (event) =>
6 | Auth(toWebRequest(event), {
7 | secret: process.env.AUTH_SECRET,
8 | trustHost: !!process.env.VERCEL,
9 | redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL,
10 | providers: [
11 | GitHub({
12 | clientId: process.env.GITHUB_CLIENT_ID,
13 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
14 | }),
15 | ],
16 | }),
17 | );
18 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(marketing)/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import { compareDesc } from "date-fns";
2 |
3 | import { BlogPosts } from "~/components/blog/blog-posts";
4 | import { allPosts } from ".contentlayer/generated";
5 |
6 | export const metadata = {
7 | title: "Blog",
8 | };
9 |
10 | export default function BlogPage() {
11 | const posts = allPosts
12 | .filter((post) => post.published)
13 | .sort((a, b) => {
14 | return compareDesc(new Date(a.date), new Date(b.date));
15 | });
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | interface DashboardHeaderProps {
2 | heading: string;
3 | text?: string;
4 | children?: React.ReactNode;
5 | }
6 |
7 | export function DashboardHeader({
8 | heading,
9 | text,
10 | children,
11 | }: DashboardHeaderProps) {
12 | return (
13 |
14 |
15 |
{heading}
16 | {text &&
{text}
}
17 |
18 | {children}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@saasfly/typescript-config/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"],
7 | "contentlayer/generated": ["./.contentlayer/generated"]
8 | },
9 | "plugins": [
10 | {
11 | "name": "next"
12 | }
13 | ],
14 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
15 | },
16 | "include": [
17 | "next-env.d.ts",
18 | ".next/types/**/*.ts",
19 | "*.ts",
20 | "*.tsx",
21 | "*.mjs",
22 | "src",
23 | "contentlayer.config.ts"
24 | ],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/apps/nextjs/src/types/k8s.d.ts:
--------------------------------------------------------------------------------
1 | interface ClusterStatus {
2 | PENDING: "PENDING";
3 | CREATING: "CREATING";
4 | INITING: "INITING";
5 | RUNNING: "RUNNING";
6 | STOPPED: "STOPPED";
7 | DELETED: "DELETED";
8 | }
9 |
10 | type ClusterPlan = "FREE" | "BUSINESS" | "PRO";
11 |
12 | export interface Cluster {
13 | id: number;
14 | name: string;
15 | status: keyof ClusterStatus | null;
16 | location: string;
17 | authUserId: string;
18 | plan: ClusterPlan | null;
19 | network: string | null;
20 | createdAt: Date;
21 | updatedAt: Date;
22 | delete: boolean | null;
23 | }
24 |
25 | export type ClustersArray = Cluster[] | undefined;
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/admin/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { BasicItemSkeleton } from "~/components/base-item";
2 | import { DashboardHeader } from "~/components/header";
3 | import { DashboardShell } from "~/components/shell";
4 |
5 | export default function DashboardLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { BasicItemSkeleton } from "~/components/base-item";
2 | import { DashboardHeader } from "~/components/header";
3 | import { DashboardShell } from "~/components/shell";
4 |
5 | export default function DashboardLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
2 |
3 | import type { AppRouter } from "./root";
4 |
5 | export { createTRPCContext, createInnerTRPCContext } from "./trpc";
6 |
7 | export { t } from "./trpc";
8 |
9 | export type { AppRouter } from "./root";
10 |
11 | /**
12 | * Inference helpers for input types
13 | * @example type HelloInput = RouterInputs['example']['hello']
14 | **/
15 | export type RouterInputs = inferRouterInputs;
16 |
17 | /**
18 | * Inference helpers for output types
19 | * @example type HelloOutput = RouterOutputs['example']['hello']
20 | **/
21 | export type RouterOutputs = inferRouterOutputs;
22 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/docs/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@saasfly/ui";
2 |
3 | interface DocsPageHeaderProps extends React.HTMLAttributes {
4 | heading: string;
5 | text?: string;
6 | }
7 |
8 | export function DocsPageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
17 |
18 | {heading}
19 |
20 | {text &&
{text}
}
21 |
22 |
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/packages/api/src/router/auth.ts:
--------------------------------------------------------------------------------
1 | import { unstable_noStore as noStore } from "next/cache";
2 |
3 | import { db } from "@saasfly/db";
4 |
5 | import { createTRPCRouter, protectedProcedure } from "../trpc";
6 |
7 | export const authRouter = createTRPCRouter({
8 | mySubscription: protectedProcedure.query(async (opts) => {
9 | noStore();
10 | const userId = opts.ctx.userId as string;
11 | const customer = await db
12 | .selectFrom("Customer")
13 | .select(["plan", "stripeCurrentPeriodEnd"])
14 | .where("authUserId", "=", userId)
15 | .executeTakeFirst();
16 |
17 | if (!customer) return null;
18 | return {
19 | plan: customer.plan,
20 | endsAt: customer.stripeCurrentPeriodEnd,
21 | };
22 | }),
23 | });
24 |
--------------------------------------------------------------------------------
/tooling/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es2022",
5 | "lib": ["dom", "dom.iterable", "ES2022"],
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": "Bundler",
15 | "resolveJsonModule": true,
16 | "moduleDetection": "force",
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true,
20 | "noUncheckedIndexedAccess": true
21 | },
22 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"]
23 | }
24 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/trpc/edge/[trpc]/route.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
3 |
4 | import { createTRPCContext } from "@saasfly/api";
5 | import { edgeRouter } from "@saasfly/api/edge";
6 |
7 | // export const runtime = "edge";
8 |
9 | const handler = (req: NextRequest) =>
10 | fetchRequestHandler({
11 | endpoint: "/api/trpc/edge",
12 | router: edgeRouter,
13 | req: req,
14 | createContext: () => createTRPCContext({ req }),
15 | // createContext: () => ({}),
16 | onError: ({ error, path }) => {
17 | console.log("Error in tRPC handler (edge) on path", path);
18 | console.error(error);
19 | },
20 | });
21 |
22 | export { handler as GET, handler as POST };
23 |
--------------------------------------------------------------------------------
/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/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import type { AvatarProps } from "@radix-ui/react-avatar";
2 | import type { User } from "next-auth";
3 |
4 | import { Avatar, AvatarFallback, AvatarImage } from "@saasfly/ui/avatar";
5 | import * as Icons from "@saasfly/ui/icons";
6 |
7 | interface UserAvatarProps extends AvatarProps {
8 | user: Pick;
9 | }
10 |
11 | export function UserAvatar({ user, ...props }: UserAvatarProps) {
12 | return (
13 |
14 | {user.image ? (
15 |
16 | ) : (
17 |
18 | {user.name}
19 |
20 |
21 | )}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/api/src/transformer.ts:
--------------------------------------------------------------------------------
1 | import { dinero, type Dinero, type DineroSnapshot } from "dinero.js";
2 | import superjson from "superjson";
3 | //@ts-ignore
4 | import { JSONValue } from "superjson/dist/types";
5 |
6 | superjson.registerCustom(
7 | {
8 | isApplicable: (val): val is Dinero => {
9 | try {
10 | // if this doesn't crash we're kinda sure it's a Dinero instance
11 | (val as Dinero).calculator.add(1, 2);
12 | return true;
13 | } catch {
14 | return false;
15 | }
16 | },
17 | serialize: (val) => {
18 | return val.toJSON() as JSONValue;
19 | },
20 | deserialize: (val) => {
21 | return dinero(val as DineroSnapshot);
22 | },
23 | },
24 | "Dinero",
25 | );
26 |
27 | export const transformer = superjson;
28 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(docs)/docs/layout.tsx:
--------------------------------------------------------------------------------
1 | import { DocsSidebarNav } from "~/components/docs/sidebar-nav";
2 | import type { Locale } from "~/config/i18n-config";
3 | import { getDocsConfig } from "~/config/ui/docs";
4 |
5 | export default function DocsLayout({
6 | children,
7 | params: { lang },
8 | }: {
9 | children: React.ReactNode;
10 | params: {
11 | lang: Locale;
12 | };
13 | }) {
14 | return (
15 |
16 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/typewriterEffectSmooth.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { TextGenerateEffect } from "@saasfly/ui/typewriter-effect";
4 |
5 | export function TypewriterEffectSmooths() {
6 | const words = [
7 | {
8 | text: "Build",
9 | },
10 | {
11 | text: "awesome",
12 | },
13 | {
14 | text: "apps",
15 | },
16 | {
17 | text: "and",
18 | },
19 | {
20 | text: "ship",
21 | },
22 | {
23 | text: "fast",
24 | },
25 | {
26 | text: "with",
27 | },
28 | {
29 | text: "Saasfly.",
30 | className: "text-blue-500",
31 | },
32 | ];
33 | return (
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/document-guide.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { ChevronRight } from "lucide-react";
3 |
4 | import { cn } from "@saasfly/ui";
5 | import { AnimatedGradientText } from "@saasfly/ui/animated-gradient-text";
6 |
7 | export function DocumentGuide({ children }: { children: ReactNode }) {
8 | return (
9 |
10 | 🚀 {" "}
11 |
16 | {children}
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/stripe/src/plans.ts:
--------------------------------------------------------------------------------
1 | import { SubscriptionPlan } from "@saasfly/db";
2 |
3 | import { env } from "./env.mjs";
4 |
5 | export const PLANS: Record<
6 | string,
7 | (typeof SubscriptionPlan)[keyof typeof SubscriptionPlan]
8 | > = {
9 | // @ts-ignore
10 | [env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID]: SubscriptionPlan.PRO,
11 | // @ts-ignore
12 | [env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID]: SubscriptionPlan.PRO,
13 | // @ts-ignore
14 | [env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID]: SubscriptionPlan.BUSINESS,
15 | // @ts-ignore
16 | [env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID]: SubscriptionPlan.BUSINESS,
17 | };
18 |
19 | type PlanType = (typeof SubscriptionPlan)[keyof typeof SubscriptionPlan];
20 |
21 | export function getSubscriptionPlan(priceId: string | undefined): PlanType {
22 | return priceId && PLANS[priceId] ? PLANS[priceId]! : SubscriptionPlan.FREE;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/billing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@saasfly/ui/card";
2 |
3 | import { DashboardShell } from "~/components/shell";
4 |
5 | export default function Loading() {
6 | return (
7 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | function LoadingCard(props: { title: string }) {
19 | return (
20 |
21 |
22 | {props.title}
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/nextjs/src/config/ui/marketing.ts:
--------------------------------------------------------------------------------
1 | import type { Locale } from "~/config/i18n-config";
2 | import { getDictionary } from "~/lib/get-dictionary";
3 | import type { MarketingConfig } from "~/types";
4 |
5 | export const getMarketingConfig = async ({
6 | params: { lang },
7 | }: {
8 | params: {
9 | lang: Locale;
10 | };
11 | }): Promise => {
12 | const dict = await getDictionary(lang);
13 | return {
14 | mainNav: [
15 | {
16 | title: dict.marketing.main_nav_features,
17 | href: `/#features`,
18 | },
19 | {
20 | title: dict.marketing.main_nav_pricing,
21 | href: `/pricing`,
22 | },
23 | {
24 | title: dict.marketing.main_nav_blog,
25 | href: `/blog`,
26 | },
27 | {
28 | title: dict.marketing.main_nav_documentation,
29 | href: `/docs`,
30 | },
31 | ],
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/get-dictionary.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import type { Locale } from "~/config/i18n-config";
4 |
5 | // We enumerate all dictionaries here for better linting and typescript support
6 | // We also get the default import for cleaner types
7 | const dictionaries = {
8 | en: () =>
9 | import("~/config/dictionaries/en.json").then((module) => module.default),
10 | zh: () =>
11 | import("~/config/dictionaries/zh.json").then((module) => module.default),
12 | ko: () =>
13 | import("~/config/dictionaries/ko.json").then((module) => module.default),
14 | ja: () =>
15 | import("~/config/dictionaries/ja.json").then((module) => module.default),
16 | };
17 |
18 | export const getDictionary = async (locale: Locale) =>
19 | dictionaries[locale]?.() ?? dictionaries.en();
20 |
21 | export const getDictionarySync = (locale: Locale) =>
22 | dictionaries[locale]?.() ?? dictionaries.en();
23 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/trpc/client.ts:
--------------------------------------------------------------------------------
1 | import { loggerLink } from "@trpc/client";
2 | import { experimental_createTRPCNextAppDirClient } from "@trpc/next/app-dir/client";
3 |
4 | import type { AppRouter } from "@saasfly/api";
5 |
6 | import { endingLink, transformer } from "./shared";
7 |
8 | export const trpc = experimental_createTRPCNextAppDirClient({
9 | config() {
10 | return {
11 | transformer,
12 | links: [
13 | // loggerLink({
14 | // enabled: (opts) =>
15 | // process.env.NODE_ENV === "development" ||
16 | // (opts.direction === "down" && opts.result instanceof Error),
17 | // }),
18 | loggerLink({
19 | enabled: () => true,
20 | }),
21 | endingLink({
22 | headers: {
23 | "x-trpc-source": "client",
24 | },
25 | }),
26 | ],
27 | };
28 | },
29 | });
30 |
31 | export { type RouterInputs, type RouterOutputs } from "@saasfly/api";
32 |
--------------------------------------------------------------------------------
/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.17",
18 | "postcss": "8.4.34",
19 | "tailwindcss": "3.4.1"
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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/card-hover-effect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | import { HoverEffect } from "@saasfly/ui/card-hover-effect";
6 |
7 | export const projects = [
8 | {
9 | title: "Kubernetes",
10 | description:
11 | "Kubernetes is an open-source container-orchestration system for automating computer application deployment, scaling, and management.",
12 | link: "/",
13 | },
14 | {
15 | title: "DevOps + FinOps",
16 | description:
17 | "DevOps is a set of practices that combines software development and IT operations. FinOps is the practice of bringing financial accountability to the variable spend model of cloud.",
18 | link: "/",
19 | },
20 | {
21 | title: "AI First",
22 | description:
23 | "AI-first is a strategy that leverages artificial intelligence to improve products and services.",
24 | link: "/",
25 | },
26 | ];
27 | export function HoverEffects() {
28 | return ;
29 | }
30 |
--------------------------------------------------------------------------------
/apps/auth-proxy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/auth-proxy",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "clean": "rm -rf .turbo node_modules",
7 | "lint": "eslint",
8 | "format": "prettier --write '**/*.{js,cjs,mjs,ts,tsx,md,json}' --ignore-path .prettierignore",
9 | "typecheck": "tsc --noEmit"
10 | },
11 | "dependencies": {
12 | "@auth/core": "0.31.0"
13 | },
14 | "devDependencies": {
15 | "@saasfly/eslint-config": "workspace:*",
16 | "@saasfly/prettier-config": "workspace:*",
17 | "@saasfly/tailwind-config": "workspace:*",
18 | "@saasfly/typescript-config": "workspace:*",
19 | "@types/node": "20.12.12",
20 | "eslint": "8.57.0",
21 | "h3": "1.11.1",
22 | "nitropack": "2.9.6",
23 | "prettier": "3.2.5",
24 | "typescript": "5.4.5"
25 | },
26 | "eslintConfig": {
27 | "root": true,
28 | "extends": [
29 | "@saasfly/eslint-config/base"
30 | ]
31 | },
32 | "prettier": "@saasfly/prettier-config"
33 | }
34 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { authOptions, getCurrentUser } from "@saasfly/auth";
4 |
5 | import { DashboardHeader } from "~/components/header";
6 | import { DashboardShell } from "~/components/shell";
7 | import { UserNameForm } from "~/components/user-name-form";
8 |
9 | export const metadata = {
10 | title: "Settings",
11 | description: "Manage account and website settings.",
12 | };
13 |
14 | export default async function SettingsPage() {
15 | const user = await getCurrentUser();
16 | if (!user) {
17 | redirect(authOptions?.pages?.signIn ?? "/login");
18 | }
19 | return (
20 |
21 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/turbo/generators/templates/package.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "name":"@saasfly/{{name}}",
3 | "private":true,
4 | "version":"0.1.0",
5 | "exports":{
6 | ".":"./index.ts"
7 | },
8 | "typesVersions":{
9 | "*":{
10 | "*":[
11 | "src/*"
12 | ]
13 | }
14 | },
15 | "scripts":{
16 | "clean":"rm -rf .turbo node_modules",
17 | "lint":"eslint .",
18 | "format":"prettier --check \"**/*.{mjs,ts,md,json}\"",
19 | "typecheck":"tsc --noEmit"
20 | },
21 | "dependencies":{
22 | },
23 | "devDependencies":{
24 | "@saasfly/eslint-config":"workspace:*",
25 | "@saasfly/prettier-config":"workspace:*",
26 | "@saasfly/typescript-config":"workspace:*",
27 | "eslint":"8.57.0",
28 | "typescript":"5.4.5"
29 | },
30 | "eslintConfig":{
31 | "extends":[
32 | "@saasfly/eslint-config/base"
33 | ]
34 | },
35 | "prettier":"@saasfly/prettier-config"
36 | }
--------------------------------------------------------------------------------
/apps/nextjs/src/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function DashboardShell(props: {
4 | title?: string;
5 | description?: React.ReactNode;
6 | breadcrumb?: boolean;
7 | headerAction?: React.ReactNode;
8 | children: React.ReactNode;
9 | className?: string;
10 | }) {
11 | return (
12 |
13 |
14 |
15 |
16 | {props.title}
17 |
18 | {typeof props.description === "string" ? (
19 |
20 | {props.description}
21 |
22 | ) : (
23 | props.description
24 | )}
25 |
26 | {props.headerAction}
27 |
28 | {/*{props.breadcrumb &&
}*/}
29 |
{props.children}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/webhooks/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, type NextRequest } from "next/server";
2 |
3 | import { handleEvent, stripe, type Stripe } from "@saasfly/stripe";
4 |
5 | import { env } from "~/env.mjs";
6 |
7 | const handler = async (req: NextRequest) => {
8 | const payload = await req.text();
9 | const signature = req.headers.get("Stripe-Signature")!;
10 | try {
11 | const event = stripe.webhooks.constructEvent(
12 | payload,
13 | signature,
14 | env.STRIPE_WEBHOOK_SECRET,
15 | ) as Stripe.DiscriminatedEvent;
16 | await handleEvent(event);
17 |
18 | console.log("✅ Handled Stripe Event", event.type);
19 | return NextResponse.json({ received: true }, { status: 200 });
20 | } catch (error) {
21 | const message = error instanceof Error ? error.message : "Unknown error";
22 | console.log(`❌ Error when handling Stripe Event: ${message}`);
23 | return NextResponse.json({ error: message }, { status: 400 });
24 | }
25 | };
26 |
27 | export { handler as GET, handler as POST };
28 |
--------------------------------------------------------------------------------
/packages/auth/db.ts:
--------------------------------------------------------------------------------
1 | import { createKysely } from "@vercel/postgres-kysely";
2 | import type { GeneratedAlways } from "kysely";
3 |
4 | interface Database {
5 | User: {
6 | id: GeneratedAlways;
7 | name: string | null;
8 | email: string;
9 | emailVerified: Date | null;
10 | image: string | null;
11 | };
12 | Account: {
13 | id: GeneratedAlways;
14 | userId: string;
15 | type: string;
16 | provider: string;
17 | providerAccountId: string;
18 | refresh_token: string | null;
19 | access_token: string | null;
20 | expires_at: number | null;
21 | token_type: string | null;
22 | scope: string | null;
23 | id_token: string | null;
24 | session_state: string | null;
25 | };
26 | Session: {
27 | id: GeneratedAlways;
28 | userId: string;
29 | sessionToken: string;
30 | expires: Date;
31 | };
32 | VerificationToken: {
33 | identifier: string;
34 | token: string;
35 | expires: Date;
36 | };
37 | }
38 |
39 | export const db = createKysely();
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/content/mdx-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { cn } from "@saasfly/ui";
4 |
5 | interface CardProps extends React.HTMLAttributes {
6 | href?: string;
7 | disabled?: boolean;
8 | }
9 |
10 | export function MdxCard({
11 | href,
12 | className,
13 | children,
14 | disabled,
15 | ...props
16 | }: CardProps) {
17 | return (
18 |
26 |
27 |
28 | {children}
29 |
30 |
31 | {href && (
32 |
33 |
View
34 |
35 | )}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/mdx.css:
--------------------------------------------------------------------------------
1 | [data-rehype-pretty-code-fragment] code {
2 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black;
3 | counter-reset: line;
4 | box-decoration-break: clone;
5 | }
6 |
7 | [data-rehype-pretty-code-fragment] .line {
8 | @apply px-4 py-1;
9 | }
10 |
11 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
12 | counter-increment: line;
13 | content: counter(line);
14 | display: inline-block;
15 | width: 1rem;
16 | margin-right: 1rem;
17 | text-align: right;
18 | color: gray;
19 | }
20 |
21 | [data-rehype-pretty-code-fragment] .line--highlighted {
22 | @apply bg-slate-300 bg-opacity-10;
23 | }
24 |
25 | [data-rehype-pretty-code-fragment] .line-highlighted span {
26 | @apply relative;
27 | }
28 |
29 | [data-rehype-pretty-code-fragment] .word--highlighted {
30 | @apply rounded-md bg-slate-300 bg-opacity-10 p-1;
31 | }
32 |
33 | [data-rehype-pretty-code-title] {
34 | @apply mt-4 px-4 py-2 text-sm font-medium;
35 | }
36 |
37 | [data-rehype-pretty-code-title] + pre {
38 | @apply mt-0;
39 | }
40 |
--------------------------------------------------------------------------------
/packages/ui/src/animated-gradient-text.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | import { cn } from "./utils/cn";
4 |
5 | function AnimatedGradientText({
6 | children,
7 | className,
8 | }: {
9 | children: ReactNode;
10 | className?: string;
11 | }) {
12 | return (
13 |
19 |
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
28 | export { AnimatedGradientText };
29 |
--------------------------------------------------------------------------------
/packages/auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/auth",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "./index.ts",
6 | "types": "./index.ts",
7 | "scripts": {
8 | "clean": "rm -rf .turbo node_modules",
9 | "lint": "eslint .",
10 | "format": "prettier --check '**/*.{mjs,ts,json}' ",
11 | "typecheck": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@auth/kysely-adapter": "0.4.2",
15 | "@saasfly/db": "workspace:*",
16 | "@t3-oss/env-nextjs": "0.8.0",
17 | "next": "14.2.5",
18 | "next-auth": "4.24.7",
19 | "react": "18.3.1",
20 | "react-dom": "18.3.1",
21 | "zod": "3.22.4"
22 | },
23 | "devDependencies": {
24 | "@saasfly/eslint-config": "workspace:*",
25 | "@saasfly/prettier-config": "workspace:*",
26 | "@saasfly/typescript-config": "workspace:*",
27 | "eslint": "8.57.0",
28 | "prettier": "3.2.5",
29 | "typescript": "5.4.5"
30 | },
31 | "eslintConfig": {
32 | "root": true,
33 | "extends": [
34 | "@saasfly/eslint-config/base"
35 | ]
36 | },
37 | "prettier": "@saasfly/prettier-config"
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(marketing)/pricing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@saasfly/ui/skeleton";
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/next.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import "./src/env.mjs";
3 | import "@saasfly/auth/env.mjs";
4 |
5 | import { withNextDevtools } from "@next-devtools/core/plugin";
6 | // import "@saasfly/api/env"
7 | import withMDX from "@next/mdx";
8 |
9 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs"));
10 |
11 | /** @type {import("next").NextConfig} */
12 | const config = {
13 | reactStrictMode: true,
14 | /** Enables hot reloading for local packages without a build step */
15 | transpilePackages: [
16 | "@saasfly/api",
17 | "@saasfly/auth",
18 | "@saasfly/db",
19 | "@saasfly/common",
20 | "@saasfly/ui",
21 | "@saasfly/stripe",
22 | ],
23 | pageExtensions: ["ts", "tsx", "mdx"],
24 | experimental: {
25 | mdxRs: true,
26 | // serverActions: true,
27 | },
28 | images: {
29 | domains: ["images.unsplash.com", "avatars.githubusercontent.com"],
30 | },
31 | /** We already do linting and typechecking as separate tasks in CI */
32 | eslint: { ignoreDuringBuilds: true },
33 | typescript: { ignoreBuildErrors: true },
34 | output: "standalone",
35 | };
36 |
37 | export default withNextDevtools(withMDX()(config));
38 |
--------------------------------------------------------------------------------
/apps/nextjs/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Welcome to the repo documentation.
4 | ---
5 |
6 | This is the documentation for the Taxonomy site.
7 |
8 | This is an example of a doc site built using [ContentLayer](/docs/documentation/contentlayer) and MDX.
9 |
10 |
11 |
12 | This site is a work in progress.
13 |
14 |
15 |
16 | ## Features
17 |
18 | Select a feature below to learn more about it.
19 |
20 |
21 |
22 |
23 |
24 | ### Documentation
25 |
26 | This documentation site built using Contentlayer.
27 |
28 |
29 |
30 |
31 |
32 | ### Marketing
33 |
34 | The marketing site with landing pages.
35 |
36 |
37 |
38 |
39 |
40 | ### App
41 |
42 | The dashboard with auth and subscriptions.
43 |
44 |
45 |
46 |
47 |
48 | ### Blog
49 |
50 | The blog built using Contentlayer and MDX.
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/packages/stripe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/stripe",
3 | "private": true,
4 | "version": "0.1.0",
5 | "exports": {
6 | ".": "./src/index.ts",
7 | "./plans": "./src/plans.ts",
8 | "./env": "./src/env.mjs"
9 | },
10 | "typesVersions": {
11 | "*": {
12 | "*": [
13 | "src/*"
14 | ]
15 | }
16 | },
17 | "scripts": {
18 | "clean": "rm -rf .turbo node_modules",
19 | "dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe",
20 | "lint": "eslint .",
21 | "format": "prettier --check '**/*.{mjs,ts,json}' ",
22 | "typecheck": "tsc --noEmit"
23 | },
24 | "dependencies": {
25 | "@saasfly/db": "workspace:*",
26 | "@t3-oss/env-nextjs": "0.8.0",
27 | "stripe": "14.15.0"
28 | },
29 | "devDependencies": {
30 | "@saasfly/eslint-config": "workspace:*",
31 | "@saasfly/prettier-config": "workspace:*",
32 | "@saasfly/typescript-config": "workspace:*",
33 | "eslint": "8.57.0",
34 | "prettier": "3.2.5",
35 | "typescript": "5.4.5"
36 | },
37 | "eslintConfig": {
38 | "extends": [
39 | "@saasfly/eslint-config/base"
40 | ]
41 | },
42 | "prettier": "@saasfly/prettier-config"
43 | }
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(marketing)/pricing/page.tsx:
--------------------------------------------------------------------------------
1 | import { getCurrentUser } from "@saasfly/auth";
2 |
3 | import { PricingCards } from "~/components/price/pricing-cards";
4 | import { PricingFaq } from "~/components/price/pricing-faq";
5 | import type { Locale } from "~/config/i18n-config";
6 | import { getDictionary } from "~/lib/get-dictionary";
7 | import { trpc } from "~/trpc/server";
8 |
9 | export const metadata = {
10 | title: "Pricing",
11 | };
12 |
13 | export default async function PricingPage({
14 | params: { lang },
15 | }: {
16 | params: {
17 | lang: Locale;
18 | };
19 | }) {
20 | const user = await getCurrentUser();
21 | const dict = await getDictionary(lang);
22 | let subscriptionPlan;
23 |
24 | if (user) {
25 | subscriptionPlan = await trpc.stripe.userPlans.query();
26 | }
27 | return (
28 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/config/ui/dashboard.ts:
--------------------------------------------------------------------------------
1 | import type { Locale } from "~/config/i18n-config";
2 | import { getDictionary } from "~/lib/get-dictionary";
3 | import type { DashboardConfig } from "~/types";
4 |
5 | export const getDashboardConfig = async ({
6 | params: { lang },
7 | }: {
8 | params: {
9 | lang: Locale;
10 | };
11 | }): Promise => {
12 | const dict = await getDictionary(lang);
13 |
14 | return {
15 | mainNav: [
16 | {
17 | title: dict.common.dashboard.main_nav_documentation,
18 | href: "/docs",
19 | },
20 | {
21 | title: dict.common.dashboard.main_nav_support,
22 | href: "/support",
23 | disabled: true,
24 | },
25 | ],
26 | sidebarNav: [
27 | {
28 | id: "clusters",
29 | title: dict.common.dashboard.sidebar_nav_clusters,
30 | href: "/dashboard/",
31 | },
32 | {
33 | id: "billing",
34 | title: dict.common.dashboard.sidebar_nav_billing,
35 | href: "/dashboard/billing",
36 | },
37 | {
38 | id: "settings",
39 | title: dict.common.dashboard.sidebar_nav_settings,
40 | href: "/dashboard/settings",
41 | },
42 | ],
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/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.0.1",
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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/docs/search.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@saasfly/ui";
6 | import { Input } from "@saasfly/ui/input";
7 | import { toast } from "@saasfly/ui/use-toast";
8 |
9 | interface DocsSearchProps extends React.HTMLAttributes {
10 | lang: string;
11 | }
12 |
13 | export function DocsSearch({ className, ...props }: DocsSearchProps) {
14 | function onSubmit(event: React.SyntheticEvent) {
15 | event.preventDefault();
16 |
17 | return toast({
18 | title: "Not implemented",
19 | description: "We're still working on the search.",
20 | });
21 | }
22 |
23 | return (
24 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/trpc/server.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import "server-only";
4 |
5 | import { cookies, headers } from "next/headers";
6 | import { loggerLink } from "@trpc/client";
7 | import { experimental_createTRPCNextAppDirServer } from "@trpc/next/app-dir/server";
8 |
9 | import type { AppRouter } from "@saasfly/api";
10 |
11 | import { endingLink, transformer } from "./shared";
12 |
13 | export const trpc = experimental_createTRPCNextAppDirServer({
14 | config() {
15 | return {
16 | ssr: true,
17 | transformer,
18 | links: [
19 | // loggerLink({
20 | // enabled: (opts) =>
21 | // process.env.NODE_ENV === "development" ||
22 | // (opts.direction === "down" && opts.result instanceof Error),
23 | // }),
24 | loggerLink({
25 | enabled: () => true,
26 | }),
27 | endingLink({
28 | headers: () => {
29 | const h = new Map(headers());
30 | h.delete("connection");
31 | h.delete("transfer-encoding");
32 | h.set("x-trpc-source", "server");
33 | h.set("cookie", cookies().toString());
34 | return Object.fromEntries(h.entries());
35 | },
36 | }),
37 | ],
38 | };
39 | },
40 | });
41 |
42 | export { type RouterInputs, type RouterOutputs } from "@saasfly/api";
43 |
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/db",
3 | "version": "0.1.0",
4 | "private": true,
5 | "exports": {
6 | ".": "./index.ts"
7 | },
8 | "main": "./index.ts",
9 | "types": "./index.ts",
10 | "scripts": {
11 | "clean": "rm -rf .turbo node_modules",
12 | "lint": "eslint .",
13 | "format": "prisma format && prettier --check '**/*.{ts,json}' ",
14 | "db:generate": "prisma generate",
15 | "db:push": "bun with-env prisma db push --skip-generate",
16 | "typecheck": "tsc --noEmit",
17 | "with-env": "dotenv -e ../../.env.local --"
18 | },
19 | "dependencies": {
20 | "kysely": "0.27.3",
21 | "@vercel/postgres-kysely": "0.8.0"
22 | },
23 | "devDependencies": {
24 | "@saasfly/eslint-config": "workspace:*",
25 | "@saasfly/prettier-config": "workspace:*",
26 | "@saasfly/typescript-config": "workspace:*",
27 | "dotenv-cli": "7.3.0",
28 | "eslint": "8.57.0",
29 | "prettier": "3.2.5",
30 | "prisma": "5.9.1",
31 | "prisma-kysely": "1.7.1",
32 | "@types/pg": "8.11.0",
33 | "typescript": "5.4.5"
34 | },
35 | "eslintConfig": {
36 | "root": true,
37 | "extends": [
38 | "@saasfly/eslint-config/base"
39 | ],
40 | "rules": {
41 | "@typescript-eslint/consistent-type-definitions": "off"
42 | }
43 | },
44 | "prettier": "@saasfly/prettier-config"
45 | }
46 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/meteors-card.tsx:
--------------------------------------------------------------------------------
1 | import { Meteors } from "@saasfly/ui/meteors";
2 |
3 | import type { Meteor } from "~/types/meteors";
4 |
5 | export function Meteorss({ meteor }: { meteor: Meteor }) {
6 | return (
7 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/apps/nextjs/src/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useMediaQuery() {
4 | const [device, setDevice] = useState<"mobile" | "tablet" | "desktop" | null>(
5 | null,
6 | );
7 | const [dimensions, setDimensions] = useState<{
8 | width: number;
9 | height: number;
10 | } | null>(null);
11 |
12 | useEffect(() => {
13 | const checkDevice = () => {
14 | if (window.matchMedia("(max-width: 640px)").matches) {
15 | setDevice("mobile");
16 | } else if (
17 | window.matchMedia("(min-width: 641px) and (max-width: 1024px)").matches
18 | ) {
19 | setDevice("tablet");
20 | } else {
21 | setDevice("desktop");
22 | }
23 | setDimensions({ width: window.innerWidth, height: window.innerHeight });
24 | };
25 |
26 | // Initial detection
27 | checkDevice();
28 |
29 | // Listener for windows resize
30 | window.addEventListener("resize", checkDevice);
31 |
32 | // Cleanup listener
33 | return () => {
34 | window.removeEventListener("resize", checkDevice);
35 | };
36 | }, []);
37 |
38 | return {
39 | device,
40 | width: dimensions?.width,
41 | height: dimensions?.height,
42 | isMobile: device === "mobile",
43 | isTablet: device === "tablet",
44 | isDesktop: device === "desktop",
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/packages/ui/src/marquee.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "./utils/cn";
2 |
3 | interface MarqueeProps {
4 | className?: string;
5 | reverse?: boolean;
6 | pauseOnHover?: boolean;
7 | children?: React.ReactNode;
8 | vertical?: boolean;
9 | repeat?: number;
10 | [key: string]: any;
11 | }
12 |
13 | export default function Marquee({
14 | className,
15 | reverse,
16 | pauseOnHover = false,
17 | children,
18 | vertical = false,
19 | repeat = 4,
20 | ...props
21 | }: MarqueeProps) {
22 | return (
23 |
34 | {Array(repeat)
35 | .fill(0)
36 | .map((_, i) => (
37 |
46 | {children}
47 |
48 | ))}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/packages/auth/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | // This is optional because it's only used in development.
7 | // See https://next-auth.js.org/deployment.
8 | NEXTAUTH_URL: z.string().url().optional(),
9 | NEXTAUTH_SECRET: z.string().min(1),
10 | GITHUB_CLIENT_ID: z.string().min(1),
11 | GITHUB_CLIENT_SECRET: z.string().min(1),
12 | STRIPE_API_KEY: z.string().min(1),
13 | STRIPE_WEBHOOK_SECRET: z.string().min(1),
14 | RESEND_API_KEY: z.string().min(1),
15 | RESEND_FROM: z.string().min(1),
16 | ADMIN_EMAIL: z.string().optional(),
17 | IS_DEBUG: z.string().optional(),
18 | },
19 | client: {
20 | NEXT_PUBLIC_APP_URL: z.string().min(1),
21 | },
22 | runtimeEnv: {
23 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
24 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
25 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
26 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
27 | STRIPE_API_KEY: process.env.STRIPE_API_KEY,
28 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
29 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
30 | RESEND_API_KEY: process.env.RESEND_API_KEY,
31 | RESEND_FROM: process.env.RESEND_FROM,
32 | ADMIN_EMAIL: process.env.ADMIN_EMAIL,
33 | IS_DEBUG: process.env.IS_DEBUG,
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Image from "next/image";
3 |
4 | import { cn } from "@saasfly/ui";
5 |
6 | import { ModeToggle } from "~/components/mode-toggle";
7 |
8 | function getCopyrightText(
9 | dict: Record>,
10 | ) {
11 | const currentYear = new Date().getFullYear();
12 | const copyrightTemplate = String(dict.copyright);
13 | return copyrightTemplate?.replace("${currentYear}", String(currentYear));
14 | }
15 |
16 | export function SiteFooter({
17 | className,
18 | dict,
19 | }: {
20 | className?: string;
21 | params: {
22 | lang: string;
23 | };
24 |
25 | dict: Record>;
26 | }) {
27 | return (
28 |
29 |
30 |
31 |
37 |
38 | {getCopyrightText(dict)}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/price/pricing-faq.tsx:
--------------------------------------------------------------------------------
1 | import Balancer from "react-wrap-balancer";
2 |
3 | import {
4 | Accordion,
5 | AccordionContent,
6 | AccordionItem,
7 | AccordionTrigger,
8 | } from "@saasfly/ui/accordion";
9 |
10 | import type { Locale } from "~/config/i18n-config";
11 | import { priceFaqDataMap } from "~/config/price/price-faq-data";
12 |
13 | export function PricingFaq({
14 | params: { lang },
15 | dict,
16 | }: {
17 | params: {
18 | lang: Locale;
19 | };
20 | dict: Record;
21 | }) {
22 | const pricingFaqData = priceFaqDataMap[lang];
23 | return (
24 |
25 |
26 |
27 | {dict.faq}
28 |
29 |
30 | {dict.faq_detail}
31 |
32 |
33 |
34 | {pricingFaqData?.map((faqItem) => (
35 |
36 | {faqItem.question}
37 | {faqItem.answer}
38 |
39 | ))}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/apps/nextjs/src/config/providers.tsx:
--------------------------------------------------------------------------------
1 | // app/providers.tsx
2 | "use client";
3 |
4 | import { useEffect } from "react";
5 | import { usePathname, useSearchParams } from "next/navigation";
6 | import posthog from "posthog-js";
7 | import { PostHogProvider } from "posthog-js/react";
8 |
9 | import { env } from "~/env.mjs";
10 |
11 | if (typeof window !== "undefined") {
12 | const posthogKey = env.NEXT_PUBLIC_POSTHOG_KEY + "";
13 | const posthogHost = env.NEXT_PUBLIC_POSTHOG_HOST + "";
14 |
15 | // 你也可以先检查这些变量是否存在
16 | if (posthogKey && posthogHost) {
17 | posthog.init(posthogKey, {
18 | api_host: posthogHost,
19 | capture_pageview: false,
20 | });
21 | } else {
22 | console.error("PostHog environment variables are missing");
23 | }
24 | }
25 |
26 | export function PostHogPageview() {
27 | const pathname = usePathname();
28 | const searchParams = useSearchParams();
29 |
30 | useEffect(() => {
31 | if (pathname) {
32 | let url = window.origin + pathname;
33 | if (searchParams?.toString()) {
34 | url = url + `?${searchParams.toString()}`;
35 | }
36 | posthog.capture("$pageview", {
37 | $current_url: url,
38 | });
39 | }
40 | }, [pathname, searchParams]);
41 |
42 | return <>>;
43 | }
44 |
45 | export function PHProvider({ children }: { children: React.ReactNode }) {
46 | return {children} ;
47 | }
48 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/locale-change.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useRouter } from "next/navigation";
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 | import { i18n, localeMap } from "~/config/i18n-config";
16 |
17 | export function LocaleChange({ url }: { url: string }) {
18 | const router = useRouter();
19 |
20 | function onClick(locale: string) {
21 | // console.log(url);
22 | router.push(`/${locale}/` + url);
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {i18n.locales.map((locale) => {
36 | return (
37 | // {locale}
38 | onClick(locale)}>
39 | {localeMap[locale]}
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(editor)/editor/cluster/[clusterId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from "next/navigation";
2 | import type { User } from "next-auth";
3 |
4 | import { authOptions, getCurrentUser } from "@saasfly/auth";
5 | import { db } from "@saasfly/db";
6 |
7 | import { ClusterConfig } from "~/components/k8s/cluster-config";
8 | import type { Cluster } from "~/types/k8s";
9 |
10 | async function getClusterForUser(clusterId: Cluster["id"], userId: User["id"]) {
11 | return await db
12 | .selectFrom("K8sClusterConfig")
13 | .selectAll()
14 | .where("id", "=", Number(clusterId))
15 | .where("authUserId", "=", userId)
16 | .executeTakeFirst();
17 | }
18 |
19 | interface EditorClusterProps {
20 | params: {
21 | clusterId: number;
22 | lang: string;
23 | };
24 | }
25 |
26 | export default async function EditorClusterPage({
27 | params,
28 | }: EditorClusterProps) {
29 | const user = await getCurrentUser();
30 | if (!user) {
31 | redirect(authOptions?.pages?.signIn ?? "/login");
32 | }
33 |
34 | // console.log("EditorClusterPage user:" + user.id + "params:", params);
35 | const cluster = await getClusterForUser(params.clusterId, user.id);
36 |
37 | if (!cluster) {
38 | notFound();
39 | }
40 | return (
41 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/nextjs/src/trpc/shared.ts:
--------------------------------------------------------------------------------
1 | import {
2 | httpBatchLink,
3 | type HTTPBatchLinkOptions,
4 | type HTTPHeaders,
5 | type TRPCLink,
6 | } from "@trpc/client";
7 |
8 | import type { AppRouter } from "@saasfly/api";
9 |
10 | import { env } from "~/env.mjs";
11 |
12 | export { transformer } from "@saasfly/api/transformer";
13 | const getBaseUrl = () => {
14 | if (typeof window !== "undefined") return "";
15 | const vc = env.NEXT_PUBLIC_APP_URL;
16 | if (vc) return vc;
17 | return `http://localhost:3000`;
18 | };
19 |
20 | const lambdas = [""];
21 |
22 | export const endingLink = (opts?: {
23 | headers?: HTTPHeaders | (() => HTTPHeaders);
24 | }) =>
25 | ((runtime) => {
26 | const sharedOpts = {
27 | headers: opts?.headers,
28 | } satisfies Partial;
29 |
30 | const edgeLink = httpBatchLink({
31 | ...sharedOpts,
32 | url: `${getBaseUrl()}/api/trpc/edge`,
33 | })(runtime);
34 | const lambdaLink = httpBatchLink({
35 | ...sharedOpts,
36 | url: `${getBaseUrl()}/api/trpc/lambda`,
37 | })(runtime);
38 |
39 | return (ctx) => {
40 | const path = ctx.op.path.split(".") as [string, ...string[]];
41 | const endpoint = lambdas.includes(path[0]) ? "lambda" : "edge";
42 |
43 | const newCtx = {
44 | ...ctx,
45 | op: { ...ctx.op, path: path.join(".") },
46 | };
47 | return endpoint === "edge" ? edgeLink(newCtx) : lambdaLink(newCtx);
48 | };
49 | }) satisfies TRPCLink;
50 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | import { getCurrentUser } from "@saasfly/auth";
4 |
5 | import { ModalProvider } from "~/components/modal-provider";
6 | import { NavBar } from "~/components/navbar";
7 | import { SiteFooter } from "~/components/site-footer";
8 | import type { Locale } from "~/config/i18n-config";
9 | import { getMarketingConfig } from "~/config/ui/marketing";
10 | import { getDictionary } from "~/lib/get-dictionary";
11 |
12 | export default async function MarketingLayout({
13 | children,
14 | params: { lang },
15 | }: {
16 | children: React.ReactNode;
17 | params: {
18 | lang: Locale;
19 | };
20 | }) {
21 | const dict = await getDictionary(lang);
22 | const user = await getCurrentUser();
23 | return (
24 |
25 |
26 |
36 |
37 |
38 | {children}
39 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(docs)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | import { getCurrentUser } from "@saasfly/auth";
4 |
5 | import { NavBar } from "~/components/navbar";
6 | import { SiteFooter } from "~/components/site-footer";
7 | import type { Locale } from "~/config/i18n-config";
8 | import { getMarketingConfig } from "~/config/ui/marketing";
9 | import { getDictionary } from "~/lib/get-dictionary";
10 |
11 | interface DocsLayoutProps {
12 | children: React.ReactNode;
13 | params: {
14 | lang: Locale;
15 | };
16 | }
17 |
18 | export default async function DocsLayout({
19 | children,
20 | params: { lang },
21 | }: DocsLayoutProps) {
22 | // const dashboardConfig = await getDashboardConfig({ params: { lang } });
23 | const dict = await getDictionary(lang);
24 | const user = await getCurrentUser();
25 |
26 | return (
27 |
28 |
29 |
39 |
40 |
{children}
41 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/api/src/trpc.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 | import { initTRPC } from "@trpc/server";
3 | import { getToken, type JWT } from "next-auth/jwt";
4 | import { ZodError } from "zod";
5 |
6 | import { transformer } from "./transformer";
7 |
8 | interface CreateContextOptions {
9 | req?: NextRequest;
10 | }
11 |
12 | export const createInnerTRPCContext = (opts: CreateContextOptions) => {
13 | return {
14 | ...opts,
15 | };
16 | };
17 |
18 | export const createTRPCContext = (opts: { req: NextRequest }) => {
19 | return createInnerTRPCContext({
20 | req: opts.req,
21 | });
22 | };
23 |
24 | export const t = initTRPC.context().create({
25 | transformer,
26 | errorFormatter({ shape, error }) {
27 | return {
28 | ...shape,
29 | data: {
30 | ...shape.data,
31 | zodError:
32 | error.cause instanceof ZodError ? error.cause.flatten() : null,
33 | },
34 | };
35 | },
36 | });
37 |
38 | export const createTRPCRouter = t.router;
39 | export const procedure = t.procedure;
40 | export const mergeRouters = t.mergeRouters;
41 |
42 | export const protectedProcedure = procedure.use(async (opts) => {
43 | const { req } = opts.ctx;
44 | const nreq = req!;
45 | const jwt = await handler(nreq);
46 | return opts.next({ ctx: { req, userId: jwt?.id } });
47 | });
48 |
49 | async function handler(req: NextRequest): Promise {
50 | // if using `NEXTAUTH_SECRET` env variable, we detect it, and you won't actually need to `secret`
51 | return await getToken({ req });
52 | }
53 |
--------------------------------------------------------------------------------
/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 | "db:push": "cd ./packages/db/ && bun db:push",
9 | "dev": "turbo dev --parallel",
10 | "dev:web": "turbo dev --parallel --filter !stripe",
11 | "format": "turbo format --continue -- --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'",
12 | "format:fix": "turbo format --continue -- --write --cache --cache-location='node_modules/.cache/.prettiercache' --ignore-path='../../.gitignore'",
13 | "lint": "turbo lint -- --quiet -- --cache --cache-location 'node_modules/.cache/.eslintcache' && manypkg check",
14 | "lint:fix": "turbo lint --continue -- --fix --cache --cache-location 'node_modules/.cache/.eslintcache' ",
15 | "typecheck": "turbo typecheck",
16 | "postinstall": "bun run check-deps",
17 | "check-deps": "check-dependency-version-consistency .",
18 | "gen": "turbo gen --config 'turbo/generators/config.ts'"
19 | },
20 | "devDependencies": {
21 | "@turbo/gen": "1.13.3",
22 | "check-dependency-version-consistency": "4.1.0",
23 | "prettier": "3.2.5",
24 | "tailwind-config-viewer": "^2.0.4",
25 | "turbo": "1.13.3",
26 | "typescript": "5.4.5"
27 | },
28 | "engines": {
29 | "node": ">=18"
30 | },
31 | "prettier": "@saasfly/prettier-config",
32 | "workspaces": [
33 | "apps/*",
34 | "packages/*",
35 | "tooling/*"
36 | ],
37 | "packageManager": "bun@v1.1.10"
38 | }
39 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@saasfly/api",
3 | "version": "0.1.0",
4 | "private": true,
5 | "exports": {
6 | ".": "./src/index.ts",
7 | "./env": "./src/env.mjs",
8 | "./edge": "./src/edge.ts",
9 | "./lambda": "./src/lambda.ts",
10 | "./transformer": "./src/transformer.ts",
11 | "./resend": "./src/email.ts",
12 | "./MagicLinkEmail": "./src/emails/magic-link-email.tsx",
13 | "./subscriptions": "./src/subscriptions.ts"
14 | },
15 | "typesVersions": {
16 | "*": {
17 | "*": [
18 | "src/*"
19 | ]
20 | }
21 | },
22 | "scripts": {
23 | "clean": "rm -rf .turbo node_modules",
24 | "format": "prettier --check '**/*.{ts,mjs}' ",
25 | "typecheck": "tsc --noEmit"
26 | },
27 | "dependencies": {
28 | "@saasfly/auth": "workspace:*",
29 | "@saasfly/db": "workspace:*",
30 | "@trpc/client": "10.44.1",
31 | "@trpc/server": "10.44.1",
32 | "@t3-oss/env-nextjs": "0.8.0",
33 | "superjson": "2.2.1",
34 | "dinero.js": "2.0.0-alpha.14",
35 | "@dinero.js/currencies": "2.0.0-alpha.14",
36 | "zod": "3.22.4",
37 | "zod-form-data": "2.0.2"
38 | },
39 | "devDependencies": {
40 | "@saasfly/eslint-config": "workspace:*",
41 | "@saasfly/prettier-config": "workspace:*",
42 | "@saasfly/typescript-config": "workspace:*",
43 | "eslint": "8.57.0",
44 | "prettier": "3.2.5",
45 | "typescript": "5.4.5"
46 | },
47 | "eslintConfig": {
48 | "root": true,
49 | "extends": [
50 | "@saasfly/eslint-config/base"
51 | ]
52 | },
53 | "prettier": "@saasfly/prettier-config"
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/content/docs/documentation/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Build your documentation site using Contentlayer and MDX.
4 | ---
5 |
6 | Taxonomy includes a documentation site built using [Contentlayer](https://contentlayer.dev) and [MDX](http://mdxjs.com).
7 |
8 | ## Features
9 |
10 | It comes with the following features out of the box:
11 |
12 | 1. Write content using MDX.
13 | 2. Transform and validate content using Contentlayer.
14 | 3. MDX components such as ` ` and ` `.
15 | 4. Support for table of contents.
16 | 5. Custom navigation with prev and next pager.
17 | 6. Beautiful code blocks using `rehype-pretty-code`.
18 | 7. Syntax highlighting using `shiki`.
19 | 8. Built-in search (_in progress_).
20 | 9. Dark mode (_in progress_).
21 |
22 | ## How is it built
23 |
24 | Click on a section below to learn how the documentation site built.
25 |
26 |
27 |
28 |
29 |
30 | ### Contentlayer
31 |
32 | Learn how to use MDX with Contentlayer.
33 |
34 |
35 |
36 |
37 |
38 | ### Components
39 |
40 | Using React components in Mardown.
41 |
42 |
43 |
44 |
45 |
46 | ### Code Blocks
47 |
48 | Beautiful code blocks with syntax highlighting.
49 |
50 |
51 |
52 |
53 |
54 | ### Style Guide
55 |
56 | View a sample page with all the styles.
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/infiniteMovingCards.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | import { InfiniteMovingCards } from "@saasfly/ui/infinite-moving-cards";
6 |
7 | export function InfiniteMovingCardss() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | const reviews = [
16 | {
17 | quote:
18 | "这款 SaaS 服务简直是办公利器!它的功能非常强大,界面也十分友好。自从使用它以后,我的工作效率提高了很多。我真的很庆幸选择了这个服务。",
19 | name: "王伟",
20 | title: "高级用户",
21 | },
22 | {
23 | quote:
24 | "I've tried many SaaS services before, but this one really stands out. It offers a wide range of features and integrates seamlessly with other tools I use. The customer support is also top-notch. Highly recommended!",
25 | name: "John Smith",
26 | title: "Power User",
27 | },
28 | {
29 | quote:
30 | "このSaaSサービスには本当に感謝しています。おかげで業務の効率が大幅に向上しました。機能が豊富で、使いやすいインターフェースも魅力的です。これからもずっと使い続けたいと思います。",
31 | name: "山田太郎",
32 | title: "ゴールドユーザー",
33 | },
34 | {
35 | quote:
36 | "저는 이 SaaS 서비스에 매우 만족하고 있습니다. 기능이 다양하고 강력할 뿐만 아니라, 고객 지원도 훌륭합니다. 이 서비스 덕분에 업무 성과가 크게 향상되었어요. 강력히 추천합니다!",
37 | name: "김민수",
38 | title: "VIP 사용자",
39 | },
40 | {
41 | quote:
42 | "This SaaS service has revolutionized the way our team works. It's feature-rich, user-friendly, and the pricing is quite competitive. We've seen a significant boost in our productivity since we started using it.",
43 | name: "Emily Johnson",
44 | title: "Verified Buyer",
45 | },
46 | ];
47 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/k8s/cluster-item.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { TableBody, TableCell, TableRow } from "@saasfly/ui/table";
4 |
5 | import { ClusterOperations } from "~/components/k8s/cluster-operation";
6 | import { formatDate } from "~/lib/utils";
7 | import type { Cluster } from "~/types/k8s";
8 |
9 | // import { ClusterOperations } from "~/components/k8s/cluster-operation";
10 | // import { formatDate } from "~/lib/utils";
11 |
12 | interface ClusterItemProps {
13 | cluster: Pick;
14 | }
15 |
16 | export function ClusterItem({ cluster }: ClusterItemProps) {
17 | return (
18 |
19 |
20 |
21 |
25 | {cluster.name}
26 |
27 |
28 | {cluster.location}
29 |
30 | {formatDate(cluster.updatedAt?.toDateString())}
31 |
32 | {cluster.plan}
33 | RUNNING
34 |
35 | {/* */}
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/price/billing-form-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTransition } from "react";
4 |
5 | import { Button } from "@saasfly/ui/button";
6 | import * as Icons from "@saasfly/ui/icons";
7 |
8 | import { trpc } from "~/trpc/client";
9 | import type { SubscriptionPlan, UserSubscriptionPlan } from "~/types";
10 |
11 | interface BillingFormButtonProps {
12 | offer: SubscriptionPlan;
13 | subscriptionPlan: UserSubscriptionPlan;
14 | year: boolean;
15 | dict: Record;
16 | }
17 |
18 | export function BillingFormButton({
19 | year,
20 | offer,
21 | dict,
22 | subscriptionPlan,
23 | }: BillingFormButtonProps) {
24 | const [isPending, startTransition] = useTransition();
25 |
26 | async function createSession(planId: string) {
27 | const res = await trpc.stripe.createSession.mutate({ planId: planId });
28 | if (res?.url) window.location.href = res?.url;
29 | }
30 |
31 | const stripePlanId = year
32 | ? offer?.stripeIds?.yearly
33 | : offer?.stripeIds?.monthly;
34 |
35 | const stripeSessionAction = () =>
36 | startTransition(async () => await createSession(stripePlanId!));
37 |
38 | return (
39 |
45 | {isPending ? (
46 | <>
47 | Loading...
48 | >
49 | ) : (
50 | <>
51 | {subscriptionPlan.stripePriceId
52 | ? dict.manage_subscription
53 | : dict.upgrade}
54 | >
55 | )}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/stripe/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 | server: {
7 | STRIPE_API_KEY: z.string(),
8 | },
9 | client: {
10 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string().optional(),
11 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string().optional(),
12 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID: z.string().optional(),
13 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID: z.string().optional(),
14 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID: z.string().optional(),
15 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID: 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 | experimental__runtimeEnv: {
20 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID:
21 | process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
22 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID:
23 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
24 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID:
25 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID,
26 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID:
27 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID,
28 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID:
29 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID,
30 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID:
31 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID,
32 | },
33 | skipValidation:
34 | !!process.env.SKIP_ENV_VALIDATION ||
35 | process.env.npm_lifecycle_event === "lint",
36 | });
37 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # -----------------------------------------------------------------------------
2 | # App
3 | # -----------------------------------------------------------------------------
4 | NEXT_PUBLIC_APP_URL='http://localhost:3000'
5 | # -----------------------------------------------------------------------------
6 | # Authentication (NextAuth.js)
7 | # openssl rand -base64 32
8 | # -----------------------------------------------------------------------------
9 | NEXTAUTH_URL='http://localhost:3000'
10 | NEXTAUTH_SECRET='1'
11 |
12 | GITHUB_CLIENT_ID='1'
13 | GITHUB_CLIENT_SECRET='1'
14 |
15 | # -----------------------------------------------------------------------------
16 | # Email (RESEND)
17 | # -----------------------------------------------------------------------------
18 | RESEND_API_KEY='1'
19 | RESEND_FROM='1'
20 |
21 | # -----------------------------------------------------------------------------
22 | # Subscriptions (Stripe)
23 | # -----------------------------------------------------------------------------
24 | # Stripe
25 | STRIPE_API_KEY="1"
26 | STRIPE_WEBHOOK_SECRET="1"
27 | NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID="prod_"
28 | NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID="price_"
29 |
30 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID="prod_"
31 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID="price_"
32 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID="price_"
33 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID="prod_"
34 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID="price_"
35 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID="price_"
36 |
37 | # posthog
38 | NEXT_PUBLIC_POSTHOG_KEY=" "
39 | NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
40 |
41 | # admin account
42 | ADMIN_EMAIL="admin@saasfly.io,root@saasfly.io"
43 |
44 | # next auth debug
45 | IS_DEBUG=false
--------------------------------------------------------------------------------
/apps/nextjs/src/components/modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Drawer } from "vaul";
4 |
5 | import { cn } from "@saasfly/ui";
6 | import { Dialog, DialogContent, DialogTitle } from "@saasfly/ui/dialog";
7 |
8 | import useMediaQuery from "~/hooks/use-media-query";
9 |
10 | interface ModalProps {
11 | children: React.ReactNode;
12 | className?: string;
13 | showModal: boolean;
14 | setShowModal: () => void;
15 | }
16 |
17 | export function Modal({
18 | children,
19 | className,
20 | showModal,
21 | setShowModal,
22 | }: ModalProps) {
23 | const { isMobile } = useMediaQuery();
24 |
25 | if (isMobile) {
26 | return (
27 |
28 |
29 |
30 |
36 |
39 | {children}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | return (
47 |
48 |
49 |
50 | {children}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 |
6 | import { cn } from "@saasfly/ui";
7 | import * as Icons from "@saasfly/ui/icons";
8 |
9 | import type { SidebarNavItem } from "~/types";
10 |
11 | interface DashboardNavProps {
12 | items: SidebarNavItem[];
13 | params: {
14 | lang: string;
15 | };
16 | }
17 |
18 | const iconMapObj = new Map([
19 | ["clusters", Icons.Cluster],
20 | ["billing", Icons.Billing],
21 | ["settings", Icons.Settings],
22 | ]);
23 |
24 | export function DashboardNav({ items, params: { lang } }: DashboardNavProps) {
25 | const path = usePathname();
26 |
27 | if (!items?.length) {
28 | return null;
29 | }
30 |
31 | return (
32 |
33 | {items.map((item, index) => {
34 | // const Icon = item.icon;
35 | const Icon = iconMapObj.get(item.id) ?? Icons.ArrowRight;
36 | return (
37 | item.href && (
38 |
42 |
49 |
50 | {item.title}
51 |
52 |
53 | )
54 | );
55 | })}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import type { PlopTypes } from "@turbo/gen";
3 | const rootPath = process.cwd();
4 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
5 | plop.setGenerator("init", {
6 | description: "Generate a new package for the Monorepo",
7 | prompts: [
8 | {
9 | type: "input",
10 | name: "name",
11 | message:
12 | "What is the name of the package? ",
13 | }
14 | ],
15 | actions: [
16 | (answers) => {
17 | if ("name" in answers && typeof answers.name === "string") {
18 | if (answers.name.startsWith("@saasfly/")) {
19 | answers.name = answers.name.replace("@saasfly/", "");
20 | }
21 | }
22 | return "Config sanitized";
23 | },
24 | {
25 | type: "add",
26 | path: rootPath+"/packages/{{ name }}/package.json",
27 | templateFile: "templates/package.json.hbs",
28 | },
29 | {
30 | type: "add",
31 | path: rootPath+"/packages/{{ name }}/tsconfig.json",
32 | templateFile: "templates/tsconfig.json.hbs",
33 | },
34 | {
35 | type: "add",
36 | path: rootPath+"/packages/{{ name }}/index.ts",
37 | template: "export * from './src';",
38 | },
39 | {
40 | type: "add",
41 | path: rootPath+"/packages/{{ name }}/src/index.ts",
42 | template: "export const name = '{{ name }}';",
43 | }
44 | ],
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ "*" ]
6 | push:
7 | branches: [ "main" ]
8 | merge_group:
9 |
10 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds
11 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
12 | # env:
13 | # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
14 | # TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
15 |
16 | jobs:
17 | build-lint:
18 | runs-on: ubuntu-latest
19 |
20 | services:
21 | postgres:
22 | image: postgres:16.1
23 | env:
24 | POSTGRES_USER: default
25 | POSTGRES_PASSWORD: default
26 | POSTGRES_DB: verceldb
27 | ports:
28 | - 5432:5432
29 | options: >-
30 | --health-cmd pg_isready
31 | --health-interval 10s
32 | --health-timeout 5s
33 | --health-retries 5
34 |
35 | steps:
36 | - name: Checkout repo
37 | uses: actions/checkout@v4
38 |
39 | - name: Copy env
40 | shell: bash
41 | run: cp .env.example .env.local
42 |
43 | - name: Setup bun
44 | uses: oven-sh/setup-bun@v1
45 |
46 | - name: Install lib
47 | run: bun i
48 |
49 | - name: Build
50 | run: bun run build
51 | env:
52 | # The hostname used to communicate with the PostgreSQL service container
53 | POSTGRES_HOST: postgres
54 | # The default PostgreSQL port
55 | POSTGRES_PORT: 5432
56 | POSTGRES_USER: default
57 | POSTGRES_PASSWORD: default
58 | POSTGRES_DB: verceldb
59 | POSTGRES_URL: postgres://default:default@localhost:5432/verceldb
60 |
61 |
62 | - name: lint and type-check
63 | run: bun run build lint format typecheck
--------------------------------------------------------------------------------
/apps/nextjs/src/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Link from "next/link";
3 |
4 | import { cn } from "@saasfly/ui";
5 | import * as Icons from "@saasfly/ui/icons";
6 |
7 | import { siteConfig } from "~/config/site";
8 | import { useLockBody } from "~/hooks/use-lock-body";
9 | import type { MainNavItem } from "~/types";
10 |
11 | interface MobileNavProps {
12 | items: MainNavItem[];
13 | children?: React.ReactNode;
14 | menuItemClick?: () => void;
15 | }
16 |
17 | export function MobileNav({ items, children, menuItemClick }: MobileNavProps) {
18 | useLockBody();
19 | return (
20 |
25 |
26 |
27 |
28 | {siteConfig.name}
29 |
30 |
31 | {items.map((item, index) => (
32 |
41 | {item.title}
42 |
43 | ))}
44 |
45 | {children}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/lib/toc.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { toc } from "mdast-util-toc";
3 | import { remark } from "remark";
4 | import { visit } from "unist-util-visit";
5 |
6 | const textTypes = ["text", "emphasis", "strong", "inlineCode"];
7 |
8 | function flattenNode(node) {
9 | const p = [];
10 | visit(node, (node) => {
11 | if (!textTypes.includes(node.type)) return;
12 | p.push(node.value);
13 | });
14 | return p.join(``);
15 | }
16 |
17 | interface Item {
18 | title: string;
19 | url: string;
20 | items?: Item[];
21 | }
22 |
23 | interface Items {
24 | items?: Item[];
25 | }
26 |
27 | function getItems(node, current): Items {
28 | if (!node) {
29 | return {};
30 | }
31 |
32 | if (node.type === "paragraph") {
33 | visit(node, (item) => {
34 | if (item.type === "link") {
35 | current.url = item.url;
36 | current.title = flattenNode(node);
37 | }
38 |
39 | if (item.type === "text") {
40 | current.title = flattenNode(node);
41 | }
42 | });
43 |
44 | return current;
45 | }
46 |
47 | if (node.type === "list") {
48 | current.items = node.children.map((i) => getItems(i, {}));
49 |
50 | return current;
51 | } else if (node.type === "listItem") {
52 | const heading = getItems(node.children[0], {});
53 |
54 | if (node.children.length > 1) {
55 | getItems(node.children[1], heading);
56 | }
57 |
58 | return heading;
59 | }
60 |
61 | return {};
62 | }
63 |
64 | const getToc = () => (node, file) => {
65 | const table = toc(node);
66 | file.data = getItems(table.map, {});
67 | };
68 |
69 | export type TableOfContents = Items;
70 |
71 | export async function getTableOfContents(
72 | content: string,
73 | ): Promise {
74 | const result = await remark().use(getToc).process(content);
75 |
76 | return result.data;
77 | }
78 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type * as Lucide from "lucide-react";
2 |
3 | import type { Customer } from "@saasfly/db";
4 |
5 | export interface NavItem {
6 | title: string;
7 | href: string;
8 | disabled?: boolean;
9 | }
10 |
11 | export type MainNavItem = NavItem;
12 |
13 | export interface DocsConfig {
14 | mainNav: MainNavItem[];
15 | sidebarNav: SidebarNavItem[];
16 | }
17 |
18 | export type SidebarNavItem = {
19 | id: string;
20 | title: string;
21 | disabled?: boolean;
22 | external?: boolean;
23 | icon?: Lucide.LucideIcon;
24 | } & (
25 | | {
26 | href: string;
27 | items?: never;
28 | }
29 | | {
30 | href?: string;
31 | items: NavLink[];
32 | }
33 | );
34 |
35 | export interface SiteConfig {
36 | name: string;
37 | description: string;
38 | url: string;
39 | ogImage: string;
40 | links: {
41 | github: string;
42 | };
43 | }
44 |
45 | export interface DocsConfig {
46 | mainNav: MainNavItem[];
47 | sidebarNav: SidebarNavItem[];
48 | }
49 |
50 | export interface MarketingConfig {
51 | mainNav: MainNavItem[];
52 | }
53 |
54 | export interface DashboardConfig {
55 | mainNav: MainNavItem[];
56 | sidebarNav: SidebarNavItem[];
57 | }
58 |
59 | export interface SubscriptionPlan {
60 | title?: string;
61 | description?: string;
62 | benefits?: string[];
63 | limitations?: string[];
64 | prices?: {
65 | monthly: number;
66 | yearly: number;
67 | };
68 | stripeIds?: {
69 | monthly: string | null;
70 | yearly: string | null;
71 | };
72 | }
73 |
74 | export type UserSubscriptionPlan = SubscriptionPlan &
75 | Pick<
76 | Customer,
77 | "stripeCustomerId" | "stripeSubscriptionId" | "stripePriceId"
78 | > & {
79 | stripeCurrentPeriodEnd: number;
80 | isPaid: boolean | "" | null;
81 | interval: string | null;
82 | isCanceled?: boolean;
83 | };
84 |
--------------------------------------------------------------------------------
/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/animated-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useMemo, useState, type ReactElement } from "react";
4 | import { AnimatePresence, motion } from "framer-motion";
5 |
6 | export const AnimatedList = React.memo(
7 | ({
8 | className,
9 | children,
10 | delay = 1000,
11 | }: {
12 | className?: string;
13 | children: React.ReactNode;
14 | delay?: number;
15 | }) => {
16 | const [index, setIndex] = useState(0);
17 | const childrenArray = React.Children.toArray(children);
18 |
19 | useEffect(() => {
20 | const interval = setInterval(() => {
21 | setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length);
22 | }, delay);
23 |
24 | return () => clearInterval(interval);
25 | }, [childrenArray.length, delay]);
26 |
27 | const itemsToShow = useMemo(
28 | () => childrenArray.slice(0, index + 1).reverse(),
29 | [index, childrenArray],
30 | );
31 |
32 | return (
33 |
34 |
35 | {itemsToShow.map((item) => (
36 |
37 | {item}
38 |
39 | ))}
40 |
41 |
42 | );
43 | },
44 | );
45 |
46 | AnimatedList.displayName = "AnimatedList";
47 |
48 | export function AnimatedListItem({ children }: { children: React.ReactNode }) {
49 | const animations = {
50 | initial: { scale: 0, opacity: 0 },
51 | animate: { scale: 1, opacity: 1, originY: 0 },
52 | exit: { scale: 0, opacity: 0 },
53 | transition: { type: "spring", stiffness: 350, damping: 40 },
54 | };
55 |
56 | return (
57 |
58 | {children}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/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/api/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 | NEXTAUTH_URL: z.string(),
13 | },
14 | server: {
15 | NEXTAUTH_URL: z.string(),
16 | NEXTAUTH_SECRET: z.string().min(1),
17 | RESEND_API_KEY: z.string().min(1),
18 | },
19 | // Client side variables gets destructured here due to Next.js static analysis
20 | // Shared ones are also included here for good measure since the behavior has been inconsistent
21 | runtimeEnv: {
22 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
23 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
24 | RESEND_API_KEY: process.env.RESEND_API_KEY,
25 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID:
26 | process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
27 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID:
28 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
29 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID:
30 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID,
31 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID:
32 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID,
33 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID:
34 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID,
35 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID:
36 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID,
37 | },
38 | skipValidation:
39 | !!process.env.SKIP_ENV_VALIDATION ||
40 | process.env.npm_lifecycle_event === "lint",
41 | });
42 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/theme-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 default function ThemeToggle(props: {
16 | align?: "center" | "start" | "end";
17 | side?: "top" | "bottom";
18 | }) {
19 | const { setTheme, theme } = useTheme();
20 |
21 | const triggerIcon = {
22 | light: ,
23 | dark: ,
24 | system: ,
25 | }[theme as "light" | "dark" | "system"];
26 |
27 | return (
28 |
29 |
30 |
35 | {triggerIcon}
36 | {theme}
37 | Toggle theme
38 |
39 |
40 |
41 | setTheme("light")}>
42 |
43 | Light
44 |
45 | setTheme("dark")}>
46 |
47 | Dark
48 |
49 | setTheme("system")}>
50 |
51 | System
52 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/sparkles.tsx:
--------------------------------------------------------------------------------
1 | // "use client";
2 |
3 | import React from "react";
4 |
5 | // import { useTheme } from "next-themes";
6 |
7 | // import { SparklesCore } from "@saasfly/ui/sparkles";
8 |
9 | export function Sparkless() {
10 | // const { theme } = useTheme();
11 | // let color = "#FFFFFF";
12 | // if (theme == "light") {
13 | // color = "#000000";
14 | // }
15 | return (
16 |
17 |
18 | Saasfly: A new SaaS player?
19 |
20 | {/*
*/}
21 | {/* /!* Gradients *!/*/}
22 | {/*
*/}
23 | {/*
*/}
24 | {/*
*/}
25 | {/*
*/}
26 |
27 | {/* /!* Core component *!/*/}
28 | {/*
*/}
36 |
37 | {/* /!* Radial Gradient to prevent sharp edges *!/*/}
38 | {/*
*/}
39 | {/*
*/}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/docs/pager.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Doc } from "contentlayer/generated";
3 |
4 | import { cn } from "@saasfly/ui";
5 | import { buttonVariants } from "@saasfly/ui/button";
6 | import * as Icons from "@saasfly/ui/icons";
7 |
8 | import { getDocsConfig } from "~/config/ui/docs";
9 |
10 | interface DocsPagerProps {
11 | doc: Doc;
12 | }
13 |
14 | export function DocsPager({ doc }: DocsPagerProps) {
15 | const pager = getPagerForDoc(doc);
16 |
17 | if (!pager) {
18 | return null;
19 | }
20 |
21 | return (
22 |
23 | {pager?.prev && (
24 |
28 |
29 | {pager.prev.title}
30 |
31 | )}
32 | {pager?.next && (
33 |
37 | {pager.next.title}
38 |
39 |
40 | )}
41 |
42 | );
43 | }
44 |
45 | export function getPagerForDoc(doc: Doc) {
46 | const flattenedLinks = [
47 | null,
48 | ...flatten(getDocsConfig("en").sidebarNav),
49 | null,
50 | ];
51 | const activeIndex = flattenedLinks.findIndex(
52 | (link) => doc.slug === link?.href,
53 | );
54 | const prev = activeIndex !== 0 ? flattenedLinks[activeIndex - 1] : null;
55 | const next =
56 | activeIndex !== flattenedLinks.length - 1
57 | ? flattenedLinks[activeIndex + 1]
58 | : null;
59 | return {
60 | prev,
61 | next,
62 | };
63 | }
64 |
65 | // @ts-ignore
66 | export function flatten(
67 | links: {
68 | items?: { items?: any }[];
69 | }[],
70 | ) {
71 | return links.reduce((flat, link) => {
72 | return flat.concat(link.items ? flatten(link.items) : link);
73 | }, []);
74 | }
75 |
--------------------------------------------------------------------------------
/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: 0 0 0;
41 | /* --background: 224 71% 4%; */
42 | --foreground: 213 31% 91%;
43 |
44 | --muted: 223 47% 11%;
45 | --muted-foreground: 215.4 16.3% 56.9%;
46 |
47 | --accent: 216 34% 17%;
48 | --accent-foreground: 210 40% 98%;
49 |
50 | --popover: 224 71% 4%;
51 | --popover-foreground: 215 20.2% 65.1%;
52 |
53 | --border: 0, 0%, 100%, .1;
54 | --input: 0, 0%, 100%, .1;
55 |
56 | --card: 224 71% 4%;
57 | --card-foreground: 213 31% 91%;
58 |
59 | --primary: 210 40% 98%;
60 | --primary-foreground: 222.2 47.4% 1.2%;
61 |
62 | --secondary: 222.2 47.4% 11.2%;
63 | --secondary-foreground: 210 40% 98%;
64 |
65 | --destructive: 0 63% 31%;
66 | --destructive-foreground: 210 40% 98%;
67 |
68 | --ring: 216 34% 17%;
69 |
70 | --radius: 0.5rem;
71 | }
72 | }
73 |
74 | @layer base {
75 | body {
76 | @apply bg-background text-foreground;
77 | font-feature-settings:
78 | "rlig" 1,
79 | "calt" 1;
80 | }
81 |
82 | .container {
83 | @apply max-sm:px-4;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/docs/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 |
6 | import { cn } from "@saasfly/ui";
7 |
8 | import type { SidebarNavItem } from "~/types";
9 |
10 | export interface DocsSidebarNavProps {
11 | items: SidebarNavItem[];
12 | }
13 |
14 | export function DocsSidebarNav({ items }: DocsSidebarNavProps) {
15 | const pathname = usePathname();
16 |
17 | return items.length ? (
18 |
19 | {items.map((item) => (
20 |
21 |
22 | {item.title}
23 |
24 | {item.items ? (
25 |
26 | ) : null}
27 |
28 | ))}
29 |
30 | ) : null;
31 | }
32 |
33 | interface DocsSidebarNavItemsProps {
34 | items: SidebarNavItem[];
35 | pathname: string | null;
36 | }
37 |
38 | export function DocsSidebarNavItems({
39 | items,
40 | pathname,
41 | }: DocsSidebarNavItemsProps) {
42 | return items?.length ? (
43 |
44 | {items.map((item) =>
45 | !item.disabled && item.href ? (
46 |
58 | {item.title}
59 |
60 | ) : (
61 |
65 | {item.title}
66 |
67 | ),
68 | )}
69 |
70 | ) : null;
71 | }
72 |
--------------------------------------------------------------------------------
/packages/ui/src/text-reveal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, ReactNode, useRef } from "react";
4 | import { motion, useScroll, useTransform } from "framer-motion";
5 |
6 | import { cn } from "./utils/cn";
7 |
8 | interface TextRevealByWordProps {
9 | text: string;
10 | className?: string;
11 | }
12 |
13 | export const TextRevealByWord: FC = ({
14 | text,
15 | className,
16 | }) => {
17 | const targetRef = useRef(null);
18 |
19 | const { scrollYProgress } = useScroll({
20 | target: targetRef,
21 | });
22 | const words = text.split(" ");
23 |
24 | return (
25 |
26 |
31 |
37 | {words.map((word, i) => {
38 | const start = i / words.length;
39 | const end = start + 1 / words.length;
40 | return (
41 |
42 | {word}
43 |
44 | );
45 | })}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | interface WordProps {
53 | children: ReactNode;
54 | progress: any;
55 | range: [number, number];
56 | }
57 |
58 | const Word: FC = ({ children, progress, range }) => {
59 | const opacity = useTransform(progress, range, [0, 1]);
60 | return (
61 |
62 | {children}
63 |
67 | {children}
68 |
69 |
70 | );
71 | };
72 |
73 | export default TextRevealByWord;
74 |
--------------------------------------------------------------------------------
/packages/db/prisma/types.ts:
--------------------------------------------------------------------------------
1 | import type { ColumnType } from "kysely";
2 |
3 | import type { Status, SubscriptionPlan } from "./enums";
4 |
5 | export type Generated =
6 | T extends ColumnType
7 | ? ColumnType
8 | : ColumnType;
9 | export type Timestamp = ColumnType;
10 |
11 | export type Account = {
12 | id: Generated;
13 | userId: string;
14 | type: string;
15 | provider: string;
16 | providerAccountId: string;
17 | refresh_token: string | null;
18 | access_token: string | null;
19 | expires_at: number | null;
20 | token_type: string | null;
21 | scope: string | null;
22 | id_token: string | null;
23 | session_state: string | null;
24 | };
25 | export type Customer = {
26 | id: Generated;
27 | authUserId: string;
28 | name: string | null;
29 | plan: SubscriptionPlan | null;
30 | stripeCustomerId: string | null;
31 | stripeSubscriptionId: string | null;
32 | stripePriceId: string | null;
33 | stripeCurrentPeriodEnd: Timestamp | null;
34 | createdAt: Generated;
35 | updatedAt: Generated;
36 | };
37 | export type K8sClusterConfig = {
38 | id: Generated;
39 | name: string;
40 | location: string;
41 | authUserId: string;
42 | plan: Generated;
43 | network: string | null;
44 | createdAt: Generated;
45 | updatedAt: Generated;
46 | status: Generated;
47 | delete: Generated;
48 | };
49 | export type Session = {
50 | id: Generated;
51 | sessionToken: string;
52 | userId: string;
53 | expires: Timestamp;
54 | };
55 | export type User = {
56 | id: Generated;
57 | name: string | null;
58 | email: string | null;
59 | emailVerified: Timestamp | null;
60 | image: string | null;
61 | };
62 | export type VerificationToken = {
63 | identifier: string;
64 | token: string;
65 | expires: Timestamp;
66 | };
67 | export type DB = {
68 | Account: Account;
69 | Customer: Customer;
70 | K8sClusterConfig: K8sClusterConfig;
71 | Session: Session;
72 | User: User;
73 | VerificationToken: VerificationToken;
74 | };
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/empty-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@saasfly/ui";
4 | import * as Icons from "@saasfly/ui/icons";
5 |
6 | type EmptyPlaceholderProps = React.HTMLAttributes;
7 |
8 | export function EmptyPlaceholder({
9 | className,
10 | children,
11 | ...props
12 | }: EmptyPlaceholderProps) {
13 | return (
14 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
28 | interface EmptyPlaceholderIconProps
29 | extends Partial> {
30 | name: keyof typeof Icons;
31 | }
32 |
33 | EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
34 | name,
35 | className, // ...props
36 | }: EmptyPlaceholderIconProps) {
37 | const Icon = Icons[name];
38 |
39 | if (!Icon) {
40 | return null;
41 | }
42 |
43 | return (
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | type EmptyPlacholderTitleProps = React.HTMLAttributes;
51 |
52 | EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
53 | className,
54 | ...props
55 | }: EmptyPlacholderTitleProps) {
56 | return (
57 | // eslint-disable-next-line jsx-a11y/heading-has-content
58 |
59 | );
60 | };
61 |
62 | type EmptyPlacholderDescriptionProps =
63 | React.HTMLAttributes;
64 |
65 | EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
66 | className,
67 | ...props
68 | }: EmptyPlacholderDescriptionProps) {
69 | return (
70 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/shimmer-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { type CSSProperties } from "react";
2 |
3 | import { cn } from "@saasfly/ui";
4 |
5 | interface ShimmerButtonProps {
6 | shimmerColor?: string;
7 | shimmerSize?: string;
8 | borderRadius?: string;
9 | shimmerDuration?: string;
10 | background?: string;
11 | className?: string;
12 | children?: React.ReactNode;
13 | }
14 |
15 | const ShimmerButton = ({
16 | shimmerColor = "#ffffff",
17 | shimmerSize = "0.1em",
18 | shimmerDuration = "1.5s",
19 | borderRadius = "100px",
20 | background = "radial-gradient(ellipse 80% 50% at 50% 120%,rgba(62, 61, 117),rgba(18, 18, 38))",
21 | className,
22 | children,
23 | ...props
24 | }: ShimmerButtonProps) => {
25 | return (
26 |
44 | {/* spark container */}
45 |
46 | {/* spark */}
47 |
48 | {/* spark before */}
49 |
50 |
51 |
52 | {/* backdrop */}
53 |
54 | {/* content */}
55 | {children}
56 |
57 | );
58 | };
59 |
60 | export default ShimmerButton;
61 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/api/src/router/customer.ts:
--------------------------------------------------------------------------------
1 | import { unstable_noStore as noStore } from "next/cache";
2 | import { getServerSession } from "next-auth/next";
3 | import { z } from "zod";
4 |
5 | import { authOptions } from "@saasfly/auth";
6 | import { db, SubscriptionPlan } from "@saasfly/db";
7 |
8 | import { createTRPCRouter, protectedProcedure } from "../trpc";
9 |
10 | const updateUserNameSchema = z.object({
11 | name: z.string(),
12 | userId: z.string(),
13 | });
14 | const insertCustomerSchema = z.object({
15 | userId: z.string(),
16 | });
17 | z.object({
18 | userId: z.string(),
19 | });
20 | export const customerRouter = createTRPCRouter({
21 | updateUserName: protectedProcedure
22 | .input(updateUserNameSchema)
23 | .mutation(async ({ input }) => {
24 | const { userId } = input;
25 | const session = await getServerSession(authOptions);
26 | if (!session?.user || userId !== session?.user.id) {
27 | return { success: false, reason: "no auth" };
28 | }
29 | await db
30 | .updateTable("User")
31 | .set({
32 | name: input.name,
33 | })
34 | .where("id", "=", userId)
35 | .execute();
36 | return { success: true, reason: "" };
37 | }),
38 |
39 | insertCustomer: protectedProcedure
40 | .input(insertCustomerSchema)
41 | .mutation(async ({ input }) => {
42 | const { userId } = input;
43 | await db
44 | .insertInto("Customer")
45 | .values({
46 | authUserId: userId,
47 | plan: SubscriptionPlan.FREE,
48 | })
49 | .executeTakeFirst();
50 | }),
51 |
52 | queryCustomer: protectedProcedure
53 | .input(insertCustomerSchema)
54 | .query(async ({ input }) => {
55 | noStore();
56 | const { userId } = input;
57 | console.log("userId:", userId);
58 | try {
59 | console.log(
60 | "result:",
61 | await db
62 | .selectFrom("Customer")
63 | .where("authUserId", "=", userId)
64 | .executeTakeFirst(),
65 | );
66 | } catch (e) {
67 | console.error("e:", e);
68 | }
69 |
70 | return await db
71 | .selectFrom("Customer")
72 | .where("authUserId", "=", userId)
73 | .executeTakeFirst();
74 | }),
75 | });
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { Metadata } from "next";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | import { cn } from "@saasfly/ui";
7 | import { buttonVariants } from "@saasfly/ui/button";
8 | import * as Icons from "@saasfly/ui/icons";
9 |
10 | import { UserAuthForm } from "~/components/user-auth-form";
11 | import type { Locale } from "~/config/i18n-config";
12 | import { getDictionary } from "~/lib/get-dictionary";
13 |
14 | export const metadata: Metadata = {
15 | title: "Login",
16 | description: "Login to your account",
17 | };
18 |
19 | export default async function LoginPage({
20 | params: { lang },
21 | }: {
22 | params: {
23 | lang: Locale;
24 | };
25 | }) {
26 | const dict = await getDictionary(lang);
27 | return (
28 |
29 |
36 | <>
37 |
38 | {dict.login.back}
39 | >
40 |
41 |
42 |
43 |
50 |
51 | {dict.login.welcome_back}
52 |
53 |
54 | {dict.login.signin_title}
55 |
56 |
57 |
58 | {/*
59 |
63 | {dict.login.singup_title}
64 |
65 |
*/}
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(editor)/editor/layout.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | import { getCurrentUser } from "@saasfly/auth";
4 |
5 | import { MainNav } from "~/components/main-nav";
6 | import { DashboardNav } from "~/components/nav";
7 | import { SiteFooter } from "~/components/site-footer";
8 | import { UserAccountNav } from "~/components/user-account-nav";
9 | import type { Locale } from "~/config/i18n-config";
10 | import { getDashboardConfig } from "~/config/ui/dashboard";
11 | import { getDictionary } from "~/lib/get-dictionary";
12 |
13 | interface EditLayoutProps {
14 | children?: React.ReactNode;
15 | params: {
16 | lang: Locale;
17 | };
18 | }
19 |
20 | export default async function DashboardLayout({
21 | children,
22 | params: { lang },
23 | }: EditLayoutProps) {
24 | const user = await getCurrentUser();
25 | const dict = await getDictionary(lang);
26 |
27 | const dashboardConfig = await getDashboardConfig({ params: { lang } });
28 | if (!user) {
29 | return notFound();
30 | }
31 |
32 | return (
33 |
34 |
51 |
52 |
58 |
59 | {children}
60 |
61 |
62 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/sign-in-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import Image from "next/image";
5 | import { signIn } from "next-auth/react";
6 |
7 | import { Button } from "@saasfly/ui/button";
8 | import * as Icons from "@saasfly/ui/icons";
9 |
10 | import { Modal } from "~/components/modal";
11 | import { siteConfig } from "~/config/site";
12 | import { useSigninModal } from "~/hooks/use-signin-modal";
13 |
14 | export const SignInModal = ({ dict }: { dict: Record }) => {
15 | const signInModal = useSigninModal();
16 | const [signInClicked, setSignInClicked] = useState(false);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
30 |
31 |
{dict.signup}
32 |
{dict.privacy}
33 |
34 |
35 |
36 | {
40 | setSignInClicked(true);
41 | signIn("github", { redirect: false })
42 | .then(() =>
43 | setTimeout(() => {
44 | signInModal.onClose();
45 | }, 1000),
46 | )
47 | .catch((error) => {
48 | console.error("signUp failed:", error);
49 | });
50 | }}
51 | >
52 | {signInClicked ? (
53 |
54 | ) : (
55 |
56 | )}{" "}
57 | {dict.signup_github}
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/apps/nextjs/src/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | // This is optional because it's only used in development.
7 | // See https://next-auth.js.org/deployment.
8 | NEXTAUTH_URL: z.string().url().optional(),
9 | NEXTAUTH_SECRET: z.string().min(1),
10 | GITHUB_CLIENT_ID: z.string().min(1),
11 | GITHUB_CLIENT_SECRET: z.string().min(1),
12 | STRIPE_API_KEY: z.string().min(1),
13 | STRIPE_WEBHOOK_SECRET: z.string().min(1),
14 | },
15 | client: {
16 | NEXT_PUBLIC_APP_URL: z.string().min(1),
17 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string().optional(),
18 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string().optional(),
19 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID: z.string().optional(),
20 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID: z.string().optional(),
21 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID: z.string().optional(),
22 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID: z.string().optional(),
23 | NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
24 | NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
25 | },
26 | runtimeEnv: {
27 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
28 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
29 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
30 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
31 | STRIPE_API_KEY: process.env.STRIPE_API_KEY,
32 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
33 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
34 | NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID:
35 | process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
36 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID:
37 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
38 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID:
39 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID,
40 | NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID:
41 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_PRODUCT_ID,
42 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID:
43 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID,
44 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID:
45 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PRICE_ID,
46 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
47 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { cn } from "@saasfly/ui";
4 | import { buttonVariants } from "@saasfly/ui/button";
5 |
6 | import { UserAuthForm } from "~/components/user-auth-form";
7 | import type { Locale } from "~/config/i18n-config";
8 | import { getDictionary } from "~/lib/get-dictionary";
9 |
10 | export const metadata = {
11 | title: "Create an account",
12 | description: "Create an account to get started.",
13 | };
14 |
15 | export default async function RegisterPage({
16 | params: { lang },
17 | }: {
18 | params: {
19 | lang: Locale;
20 | };
21 | }) {
22 | const dict = await getDictionary(lang);
23 |
24 | return (
25 |
26 |
33 | {dict.marketing.login}
34 |
35 |
36 |
37 |
38 |
39 | {/*
*/}
40 |
41 | Create an account
42 |
43 |
44 | Enter your email below to create your account
45 |
46 |
47 |
48 |
49 | By clicking continue, you agree to our{" "}
50 |
54 | Terms of Service
55 | {" "}
56 | and{" "}
57 |
61 | Privacy Policy
62 |
63 | .
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/k8s/cluster-create-button.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2 | // @ts-nocheck
3 | "use client";
4 |
5 | import * as React from "react";
6 | //navigate to new page
7 | import { useRouter } from "next/navigation";
8 |
9 | import { cn } from "@saasfly/ui";
10 | //button self design
11 | import { buttonVariants, type ButtonProps } from "@saasfly/ui/button";
12 | import * as Icons from "@saasfly/ui/icons";
13 | import { toast } from "@saasfly/ui/use-toast";
14 |
15 | import { trpc } from "~/trpc/client";
16 |
17 | interface K8sCreateButtonProps extends ButtonProps {
18 | customProp?: string;
19 | dict: Record;
20 | }
21 |
22 | export function K8sCreateButton({
23 | className,
24 | variant,
25 | dict,
26 | ...props
27 | }: K8sCreateButtonProps) {
28 | const router = useRouter();
29 | const [isLoading, setIsLoading] = React.useState(false);
30 |
31 | async function onClick() {
32 | const res = await trpc.k8s.createCluster.mutate({
33 | name: "Default Cluster",
34 | location: "Hong Kong",
35 | });
36 | setIsLoading(false);
37 |
38 | if (!res?.success) {
39 | // if (response.status === 402) {
40 | // return toast({
41 | // title: "Limit of 1 cluster reached.",
42 | // description: "Please upgrade to the PROD plan.",
43 | // variant: "destructive",
44 | // });
45 | // }
46 | return toast({
47 | title: "Something went wrong.",
48 | description: "Your cluster was not created. Please try again.",
49 | variant: "destructive",
50 | });
51 | }
52 | if (res) {
53 | const cluster = res;
54 |
55 | // This forces a cache invalidation.
56 | router.refresh();
57 |
58 | if (cluster?.id) {
59 | router.push(`/editor/cluster/${cluster.id}`);
60 | }
61 | } else {
62 | // console.log("error ");
63 | }
64 | }
65 |
66 | return (
67 |
79 | {isLoading ? (
80 |
81 | ) : (
82 |
83 | )}
84 | {dict.k8s?.new_cluster}
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/user-account-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import type { User } from "next-auth";
5 | import { signOut } from "next-auth/react";
6 |
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger,
13 | } from "@saasfly/ui/dropdown-menu";
14 |
15 | import { UserAvatar } from "~/components/user-avatar";
16 |
17 | interface UserAccountNavProps extends React.HTMLAttributes {
18 | user: Pick;
19 | params: {
20 | lang: string;
21 | };
22 | dict: Record;
23 | }
24 |
25 | export function UserAccountNav({
26 | user,
27 | params: { lang },
28 | dict,
29 | }: UserAccountNavProps) {
30 | return (
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 | {user.name &&
{user.name}
}
42 | {user.email && (
43 |
44 | {user.email}
45 |
46 | )}
47 |
48 |
49 |
50 |
51 | {dict.dashboard}
52 |
53 |
54 | {dict.billing}
55 |
56 |
57 | {dict.settings}
58 |
59 |
60 | {
63 | event.preventDefault();
64 | signOut({
65 | callbackUrl: `${window.location.origin}/${lang}/login`,
66 | }).catch((error) => {
67 | console.error("Error during sign out:", error);
68 | });
69 | }}
70 | >
71 | {dict.sign_out}
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[lang]/(dashboard)/dashboard/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardHeader,
6 | CardTitle,
7 | } from "@saasfly/ui/card";
8 |
9 | import { DashboardShell } from "~/components/shell";
10 | import type { Locale } from "~/config/i18n-config";
11 | import { getDictionary } from "~/lib/get-dictionary";
12 | import { trpc } from "~/trpc/server";
13 | import { SubscriptionForm } from "./subscription-form";
14 |
15 | export const metadata = {
16 | title: "Billing",
17 | description: "Manage billing and your subscription plan.",
18 | };
19 |
20 | interface Subscription {
21 | plan: string | null;
22 | endsAt: Date | null;
23 | }
24 |
25 | export default async function BillingPage({
26 | params: { lang },
27 | }: {
28 | params: {
29 | lang: Locale;
30 | };
31 | }) {
32 | const dict = await getDictionary(lang);
33 | return (
34 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | function generateSubscriptionMessage(
47 | dict: Record,
48 | subscription: Subscription,
49 | ): string {
50 | const content = String(dict.subscriptionInfo);
51 | if (subscription.plan && subscription.endsAt) {
52 | return content
53 | .replace("{plan}", subscription.plan)
54 | .replace("{date}", subscription.endsAt.toLocaleDateString());
55 | }
56 | return "";
57 | }
58 |
59 | async function SubscriptionCard({ dict }: { dict: Record }) {
60 | const subscription = (await trpc.auth.mySubscription.query()) as Subscription;
61 | const content = generateSubscriptionMessage(dict, subscription);
62 | return (
63 |
64 |
65 | Subscription
66 |
67 |
68 | {subscription ? (
69 |
70 | ) : (
71 | {dict.noSubscription}
72 | )}
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | function UsageCard() {
82 | return (
83 |
84 |
85 | Usage
86 |
87 | None
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------