├── .node-version ├── .nvmrc ├── .prettierignore ├── .commitlintrc.json ├── public ├── favicon.ico ├── _static │ ├── og.jpg │ ├── og_zh.jpg │ ├── avatars │ │ ├── noone.png │ │ └── shadcn.jpeg │ ├── blog │ │ ├── blog-post-1.jpg │ │ ├── blog-post-2.jpg │ │ ├── blog-post-3.jpg │ │ └── blog-post-4.jpg │ ├── docs │ │ └── gg-auth-config.jpg │ ├── favicons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ └── android-chrome-512x512.png │ └── illustrations │ │ └── work-from-home.jpg └── site.webmanifest ├── src ├── db │ ├── migrations │ │ ├── 0004_white_winter_soldier.sql │ │ ├── 0006_wise_sprite.sql │ │ ├── 0001_strange_iron_man.sql │ │ ├── 0002_dazzling_whizzer.sql │ │ ├── 0003_strange_the_spike.sql │ │ ├── 0007_lying_lorna_dane.sql │ │ ├── 0005_neat_thena.sql │ │ └── meta │ │ │ └── _journal.json │ └── db.ts ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── user │ │ │ ├── count │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── apikeys │ │ │ └── [id] │ │ │ │ └── route.ts │ │ └── quota │ │ │ └── route.ts │ ├── opengraph-image.jpg │ ├── robots.ts │ └── [locale] │ │ ├── (protected) │ │ ├── dashboard │ │ │ ├── apikeys │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── quota │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── charts │ │ │ │ ├── page.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page-client.tsx │ │ │ ├── chat │ │ │ │ └── page.tsx │ │ │ ├── settings │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── billing │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── page-client.tsx │ │ └── admin │ │ │ ├── orders │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── loading.tsx │ │ │ └── page-client.tsx │ │ ├── (marketing) │ │ ├── error.tsx │ │ ├── blog │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ └── category │ │ │ │ └── [slug] │ │ │ │ └── page.tsx │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── pricing │ │ │ └── loading.tsx │ │ ├── layout.tsx │ │ └── [slug] │ │ │ └── page.tsx │ │ ├── (auth) │ │ ├── layout.tsx │ │ └── login │ │ │ └── page.tsx │ │ ├── (docs) │ │ ├── layout.tsx │ │ └── docs │ │ │ └── layout.tsx │ │ └── layout.tsx ├── assets │ └── fonts │ │ ├── GeistVF.woff2 │ │ ├── Inter-Bold.ttf │ │ ├── Inter-Regular.ttf │ │ ├── CalSans-SemiBold.ttf │ │ ├── CalSans-SemiBold.woff2 │ │ └── index.ts ├── lib │ ├── validations │ │ ├── auth.ts │ │ ├── og.ts │ │ └── user.ts │ ├── exceptions.ts │ ├── cx.ts │ ├── stripe.ts │ ├── session.ts │ ├── locales.ts │ ├── user.ts │ ├── apikeys.ts │ ├── email.ts │ └── toc.ts ├── components │ ├── analytics.tsx │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── progress.tsx │ │ ├── input.tsx │ │ ├── toaster.tsx │ │ ├── hover-card.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── popover.tsx │ │ ├── slider.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── radio-group.tsx │ │ ├── toggle.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── toggle-group.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ └── accordion.tsx │ ├── shared │ │ ├── max-width-wrapper.tsx │ │ ├── card-skeleton.tsx │ │ ├── blur-image.tsx │ │ ├── header-section.tsx │ │ ├── user-avatar.tsx │ │ ├── section-skeleton.tsx │ │ ├── copy-button.tsx │ │ └── callout.tsx │ ├── dashboard │ │ ├── header.tsx │ │ ├── section-columns.tsx │ │ ├── upgrade-card.tsx │ │ └── info-card.tsx │ ├── content │ │ ├── blog-posts.tsx │ │ ├── mdx-card.tsx │ │ └── author.tsx │ ├── tailwind-indicator.tsx │ ├── vectorstore │ │ ├── upstash-vector-store.d.ts │ │ └── upstash-vector-store.js │ ├── link │ │ └── link.tsx │ ├── sections │ │ ├── info-landing.tsx │ │ ├── preview-landing.tsx │ │ └── testimonials.tsx │ ├── cursor │ │ ├── ui │ │ │ ├── separator.tsx │ │ │ ├── sonner.tsx │ │ │ ├── avatar.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ ├── copy-button.tsx │ │ └── menu.tsx │ ├── modals │ │ └── providers.tsx │ ├── chat │ │ ├── form.tsx │ │ ├── message.tsx │ │ └── message-loading.tsx │ ├── forms │ │ ├── customer-portal-button.tsx │ │ ├── billing-form-button.tsx │ │ └── newsletter-form.tsx │ ├── docs │ │ ├── page-header.tsx │ │ ├── search.tsx │ │ └── pager.tsx │ ├── pricing │ │ └── pricing-faq.tsx │ ├── locale │ │ └── locale-switcher.tsx │ ├── layout │ │ └── mode-toggle.tsx │ └── charts │ │ └── radar-chart-simple.tsx ├── hooks │ ├── use-mounted.ts │ ├── use-lock-body.ts │ ├── use-scroll.ts │ ├── use-local-storage.ts │ ├── use-intersection-observer.ts │ └── use-media-query.ts ├── types │ └── next-auth.d.ts ├── config │ ├── marketing.ts │ ├── blog.ts │ ├── site.ts │ ├── dashboard.ts │ └── docs.ts ├── i18n.ts ├── actions │ ├── update-user-role.ts │ ├── update-user-name.ts │ └── open-customer-portal.ts ├── middleware.ts ├── styles │ └── mdx.css ├── data │ └── cursor │ │ ├── index.ts │ │ └── rules │ │ ├── swift.ts │ │ └── python.ts └── emails │ └── magic-link-email.tsx ├── .husky ├── commit-msg └── pre-commit ├── postcss.config.js ├── drizzle.config.json ├── content ├── docs │ ├── zh │ │ ├── index.md │ │ ├── in-progress.md │ │ ├── configuration │ │ │ ├── email.md │ │ │ ├── database.md │ │ │ ├── config-files.md │ │ │ ├── authentification.md │ │ │ ├── subscriptions.md │ │ │ └── layouts.md │ │ └── installation.md │ └── en │ │ ├── index.md │ │ ├── in-progress.md │ │ ├── configuration │ │ ├── email.md │ │ ├── database.md │ │ └── config-files.md │ │ └── installation.md ├── blog │ └── preview-mode-headless-cms.md └── pages │ └── privacy.md ├── wrangler.toml ├── .env.production ├── components.json ├── .gitignore ├── .eslintrc.json ├── crowdin.yml ├── tsconfig.json ├── prettier.config.js ├── LICENSE.md └── next.config.js /.node-version: -------------------------------------------------------------------------------- 1 | 18.18.2 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | .contentlayer -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/_static/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/og.jpg -------------------------------------------------------------------------------- /src/db/migrations/0004_white_winter_soldier.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "api_keys" ADD COLUMN "name" text NOT NULL; -------------------------------------------------------------------------------- /src/db/migrations/0006_wise_sprite.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "quotas" DROP COLUMN IF EXISTS "remaining_quota"; -------------------------------------------------------------------------------- /public/_static/og_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/og_zh.jpg -------------------------------------------------------------------------------- /src/db/migrations/0001_strange_iron_man.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" ALTER COLUMN "role" SET DATA TYPE varchar(63); -------------------------------------------------------------------------------- /src/db/migrations/0002_dazzling_whizzer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" ALTER COLUMN "role" SET DATA TYPE varchar(64); -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/auth" 2 | 3 | export const runtime = 'edge'; -------------------------------------------------------------------------------- /src/app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/app/opengraph-image.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/fonts/GeistVF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/assets/fonts/GeistVF.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /drizzle.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "out": "./src/db/migrations", 3 | "schema": "src/db/schema.ts", 4 | "dialect": "postgresql" 5 | } -------------------------------------------------------------------------------- /public/_static/avatars/noone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/avatars/noone.png -------------------------------------------------------------------------------- /public/_static/avatars/shadcn.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/avatars/shadcn.jpeg -------------------------------------------------------------------------------- /src/assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /public/_static/blog/blog-post-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/blog/blog-post-1.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/blog/blog-post-2.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/blog/blog-post-3.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/blog/blog-post-4.jpg -------------------------------------------------------------------------------- /public/_static/docs/gg-auth-config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/docs/gg-auth-config.jpg -------------------------------------------------------------------------------- /src/assets/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/assets/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/src/assets/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /public/_static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/_static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /content/docs/zh/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍 3 | description: 欢迎 4 | --- 5 | 6 | 你好,世界! 这是一个使用 MDX 创建的示例页面。你可以使用 MDX 来编写包含 JSX 和 React 组件的内容。 7 | -------------------------------------------------------------------------------- /public/_static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/lib/validations/auth.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const userAuthSchema = z.object({ 4 | email: z.string().email(), 5 | }) 6 | -------------------------------------------------------------------------------- /public/_static/illustrations/work-from-home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/illustrations/work-from-home.jpg -------------------------------------------------------------------------------- /public/_static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/_static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunterzhang86/fflow-next/HEAD/public/_static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "fflow-next" 2 | compatibility_date = "2024-07-29" 3 | compatibility_flags = ["nodejs_compat"] 4 | pages_build_output_dir = ".vercel/output/static" -------------------------------------------------------------------------------- /src/lib/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class RequiresProPlanError extends Error { 2 | constructor(message = "This action requires a pro plan") { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/analytics.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react" 4 | 5 | export function Analytics() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/db/db.ts: -------------------------------------------------------------------------------- 1 | import { neon } from '@neondatabase/serverless'; 2 | import { drizzle } from 'drizzle-orm/neon-http'; 3 | 4 | const sql = neon(process.env.DATABASE_URL!); 5 | export const db = drizzle(sql); 6 | -------------------------------------------------------------------------------- /src/lib/cx.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export default function cx(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /content/docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Welcome 4 | --- 5 | 6 | Hello, world! This is a sample page created with MDX. You can use MDX to write content with JSX and React components. 7 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe" 2 | 3 | import { env } from "@/env.mjs" 4 | 5 | export const stripe = new Stripe(env.STRIPE_API_KEY, { 6 | apiVersion: "2024-04-10", 7 | typescript: true, 8 | }) 9 | -------------------------------------------------------------------------------- /src/lib/validations/og.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ogImageSchema = z.object({ 4 | heading: z.string(), 5 | type: z.string(), 6 | mode: z.enum(["light", "dark"]).default("dark"), 7 | }) 8 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next" 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | }, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /content/docs/zh/in-progress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 正在进行中 3 | description: 此页面正在开发中。 4 | --- 5 | 6 | 7 | 8 | 本网站仍在建设中。如果您在页面上看到占位文本,这意味着我仍在处理该部分内容。您可以在 Twitter [@shadcn](https://twitter.com/shadcn) 上关注最新更新。 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # App - Don't add "/" in the end of the url (same in production) 3 | # ----------------------------------------------------------------------------- 4 | NEXT_PUBLIC_APP_URL=https://www.fflowlink.com 5 | -------------------------------------------------------------------------------- /src/lib/validations/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { UserRole } from "@/types"; 3 | 4 | export const userNameSchema = z.object({ 5 | name: z.string().min(3).max(32), 6 | }); 7 | 8 | export const userRoleSchema = z.object({ 9 | role: z.nativeEnum(UserRole), 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/session.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { cache } from "react"; 4 | import { auth } from "@/auth"; 5 | 6 | export const getCurrentUser = cache(async () => { 7 | const session = await auth(); 8 | if (!session?.user) { 9 | return undefined; 10 | } 11 | return session.user; 12 | }); -------------------------------------------------------------------------------- /content/docs/en/in-progress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not Implemented 3 | description: This page is in progress. 4 | --- 5 | 6 | 7 | 8 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/db/migrations/0003_strange_the_spike.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "api_keys" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "user_id" text NOT NULL, 4 | "key" text NOT NULL, 5 | "created_at" timestamp (3) DEFAULT now() NOT NULL, 6 | "updated_at" timestamp (3) DEFAULT now() NOT NULL, 7 | CONSTRAINT "api_keys_key_unique" UNIQUE("key") 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/apikeys/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardHeader } from "@/components/dashboard/header"; 2 | import { useTranslations } from "next-intl"; 3 | 4 | export default function APIKeysLoading() { 5 | const t = useTranslations("APIKeysPage"); 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/db/migrations/0007_lying_lorna_dane.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "projects" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "name" text NOT NULL, 4 | "key" text NOT NULL, 5 | "description" text, 6 | "created_by" text NOT NULL, 7 | "created_at" timestamp (3) DEFAULT now() NOT NULL, 8 | "updated_at" timestamp (3) DEFAULT now() NOT NULL, 9 | CONSTRAINT "projects_key_unique" UNIQUE("key") 10 | ); 11 | -------------------------------------------------------------------------------- /src/db/migrations/0005_neat_thena.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "quotas" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "created_by" text NOT NULL, 4 | "created_at" timestamp (3) DEFAULT now() NOT NULL, 5 | "updated_at" timestamp (3) DEFAULT now() NOT NULL, 6 | "type" text NOT NULL, 7 | "total_quota" integer NOT NULL, 8 | "used_quota" integer NOT NULL, 9 | "remaining_quota" integer NOT NULL 10 | ); 11 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@/types"; 2 | import { User } from "next-auth"; 3 | import { JWT } from "next-auth/jwt"; 4 | 5 | export type ExtendedUser = User & { 6 | role: UserRole; 7 | }; 8 | 9 | declare module "next-auth/jwt" { 10 | interface JWT { 11 | role: UserRole; 12 | } 13 | } 14 | 15 | declare module "next-auth" { 16 | interface Session { 17 | user: ExtendedUser; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/quota/loading.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | 3 | import { DashboardHeader } from "@/components/dashboard/header"; 4 | 5 | export default function QuotaLoading() { 6 | const t = useTranslations("QuotaPage"); 7 | return ( 8 | <> 9 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/admin/orders/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { DashboardHeader } from "@/components/dashboard/header"; 3 | 4 | export default function OrdersLoading() { 5 | return ( 6 | <> 7 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/config/marketing.ts: -------------------------------------------------------------------------------- 1 | import { MarketingConfig } from "@/types" 2 | 3 | export const getMarketingConfig = (t: (key: string) => string): MarketingConfig => ({ 4 | mainNav: [ 5 | { 6 | title: t('Marketing.nav.pricing'), 7 | href: `/pricing`, 8 | }, 9 | { 10 | title: t('Marketing.nav.blog'), 11 | href: `/blog`, 12 | }, 13 | { 14 | title: t('Marketing.nav.documentation'), 15 | href: `/docs`, 16 | }, 17 | ], 18 | }); -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import {notFound} from 'next/navigation'; 2 | import {getRequestConfig} from 'next-intl/server'; 3 | 4 | // Can be imported from a shared config 5 | const locales = ['en', 'zh']; 6 | 7 | export default getRequestConfig(async ({locale}) => { 8 | // Validate that the incoming `locale` parameter is valid 9 | if (!locales.includes(locale as any)) notFound(); 10 | 11 | return { 12 | messages: (await import(`./locales/${locale}.json`)).default 13 | }; 14 | }); -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FFlow Next", 3 | "short_name": "FFlow Next", 4 | "icons": [ 5 | { 6 | "src": "./favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./favicons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/use-scroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export 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 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import { DashboardHeader } from "@/components/dashboard/header"; 5 | 6 | export default function DashboardLoading() { 7 | const t = useTranslations("Dashboard.main"); 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/quota/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMessages } from 'next-intl/server'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import QuotaPageClient from './page-client'; 4 | 5 | export default async function QuotaPage({ 6 | params: { locale } 7 | }: { 8 | params: { locale: string } 9 | }) { 10 | const messages = await getMessages({ locale }); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/charts/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMessages } from 'next-intl/server'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import ChartsPageClient from './page-client'; 4 | 5 | export default async function ChartsPage({ 6 | params: { locale } 7 | }: { 8 | params: { locale: string } 9 | }) { 10 | const messages = await getMessages({ locale }); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import { NextIntlClientProvider } from 'next-intl'; 2 | import { getMessages } from 'next-intl/server'; 3 | import ChatPageClient from './page-client'; 4 | 5 | 6 | export default async function ChatPage({ 7 | params: { locale } 8 | }: { 9 | params: { locale: string } 10 | }) { 11 | const messages = await getMessages({ locale }); 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/shared/max-width-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export default function MaxWidthWrapper({ 6 | className, 7 | children, 8 | large = false, 9 | }: { 10 | className?: string; 11 | large?: boolean; 12 | children: ReactNode; 13 | }) { 14 | return ( 15 |
22 | {children} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /content/blog/preview-mode-headless-cms.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Preview Mode for Headless CMS 3 | description: How to implement preview mode in your headless CMS. 4 | date: "2023-04-09" 5 | image: /_static/blog/blog-post-1.jpg 6 | authors: 7 | - shadcn 8 | - noone 9 | categories: 10 | - education 11 | related: 12 | - deploying-next-apps 13 | - dynamic-routing-static-regeneration 14 | --- 15 | 16 | Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS. -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/apikeys/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMessages } from 'next-intl/server'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import APIKeysPageClient from './page-client'; 4 | 5 | export default async function APIKeysPage({ 6 | params: { locale } 7 | }: { 8 | params: { locale: string } 9 | }) { 10 | const messages = await getMessages({ locale }); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | 5 | export default function Error({ 6 | reset, 7 | }: { 8 | reset: () => void; 9 | }) { 10 | 11 | return ( 12 |
13 |

Something went wrong!

14 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /src/assets/fonts/index.ts: -------------------------------------------------------------------------------- 1 | import localFont from "next/font/local"; 2 | import { Inter as FontSans, Urbanist } from "next/font/google"; 3 | 4 | export const fontSans = FontSans({ 5 | subsets: ["latin"], 6 | variable: "--font-sans", 7 | }) 8 | 9 | export const fontUrban = Urbanist({ 10 | subsets: ["latin"], 11 | variable: "--font-urban", 12 | }) 13 | 14 | export const fontHeading = localFont({ 15 | src: "./CalSans-SemiBold.woff2", 16 | variable: "--font-heading", 17 | }) 18 | 19 | export const fontGeist = localFont({ 20 | src: "./GeistVF.woff2", 21 | variable: "--font-geist", 22 | }) 23 | -------------------------------------------------------------------------------- /src/components/dashboard/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 | -------------------------------------------------------------------------------- /src/components/shared/card-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardFooter, 5 | CardHeader, 6 | } from "@/components/ui/card"; 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | 9 | export function CardSkeleton() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /content/docs/zh/configuration/email.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 电子邮件 3 | description: 如何在此项目中管理电子邮件。 4 | --- 5 | 6 | Resend的魔法链接功能与Auth.js v5兼容!
7 | 您可以在本地环境和自己的生产设置中使用它。 8 | 9 | 然而,您无法在这个演示应用中测试它,因为我不想为这个演示使用Resend的高级计划。 10 | 11 | ## 步骤 12 | 13 | 电子邮件部分与[resend](https://resend.com/)文档类似。 14 | 如果您需要,可以在[这里](https://authjs.dev/getting-started/installation#setup-environment)找到官方文档。 15 | 16 | ### 创建账户 17 | 18 | 如果您还没有Resend账户,只需在[这里](https://resend.com/signup)注册后按照他们的步骤操作。 19 | 20 | ### 创建API密钥 21 | 22 | 登录Resend后,它会提示您创建第一个API密钥。 23 | 24 | 将其复制/粘贴到您的`.env`文件中。 25 | 26 | ```js 27 | RESEND_API_KEY = re_your_resend_api_key; 28 | ``` -------------------------------------------------------------------------------- /.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 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # email 40 | /.react-email/ 41 | 42 | .vscode 43 | .contentlayer -------------------------------------------------------------------------------- /src/app/api/user/count/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import { getCurrentUser } from "@/lib/session"; 3 | import { countUsers } from "@/lib/user"; 4 | import { ApiResponse } from "@/lib/utils"; 5 | 6 | export const runtime = 'edge'; 7 | export const GET = auth(async () => { 8 | const user = await getCurrentUser(); 9 | if (!user) { 10 | return new Response("Not authenticated", { status: 401 }); 11 | } 12 | const count = await countUsers(); 13 | 14 | return new Response(ApiResponse.success(count), { 15 | status: 200, 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | } 19 | }); 20 | }); -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | import { getCurrentUser } from "@/lib/session"; 3 | import {unstable_setRequestLocale} from 'next-intl/server'; 4 | 5 | interface ProtectedLayoutProps { 6 | children: React.ReactNode; 7 | params: { locale: string }; 8 | } 9 | 10 | export default async function Dashboard({ children, params: {locale} }: ProtectedLayoutProps) { 11 | unstable_setRequestLocale(locale); 12 | 13 | const user = await getCurrentUser(); 14 | if (!user || user.role as string !== "ADMIN") redirect("/login"); 15 | 16 | return <>{children}; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/content/blog-posts.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from "contentlayer/generated"; 2 | 3 | import { BlogCard } from "./blog-card"; 4 | 5 | export function BlogPosts({ 6 | posts, 7 | }: { 8 | posts: (Post & { 9 | blurDataURL: string; 10 | })[]; 11 | }) { 12 | return ( 13 |
14 | 15 | 16 |
17 | {posts.slice(1).map((post, idx) => ( 18 | 19 | ))} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/shared/blur-image.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import type { ComponentProps } from "react"; 5 | import Image from "next/image"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | export default function BlurImage(props: ComponentProps) { 10 | const [isLoading, setLoading] = useState(true); 11 | 12 | return ( 13 | {props.alt} setLoading(false)} 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /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 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardHeader } from "@/components/dashboard/header"; 2 | import { SkeletonSection } from "@/components/shared/section-skeleton"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | export default function DashboardSettingsLoading() { 6 | const t = useTranslations("Settings"); 7 | return ( 8 | <> 9 | 13 |
14 | 15 | 16 | 17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/[locale]/(protected)/dashboard/billing/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { DashboardHeader } from "@/components/dashboard/header"; 3 | import { CardSkeleton } from "@/components/shared/card-skeleton"; 4 | import { useTranslations } from "next-intl"; 5 | 6 | export default function DashboardBillingLoading() { 7 | const t = useTranslations("BillingPage"); 8 | 9 | return ( 10 | <> 11 | 15 |
16 | 17 | 18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { unstable_setRequestLocale } from "next-intl/server"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { getCurrentUser } from "@/lib/session"; 5 | 6 | interface AuthLayoutProps { 7 | children: React.ReactNode; 8 | params: { locale: string }; 9 | } 10 | 11 | export default async function AuthLayout({ 12 | children, 13 | params: { locale }, 14 | }: AuthLayoutProps) { 15 | unstable_setRequestLocale(locale); 16 | 17 | const user = await getCurrentUser(); 18 | 19 | if (user) { 20 | if ((user.role as string) === "ADMIN") redirect(`/${locale}/admin`); 21 | redirect(`/${locale}/dashboard`); 22 | } 23 | 24 | return
{children}
; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/dashboard/section-columns.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SectionColumnsType { 4 | title: string; 5 | description?: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | export function SectionColumns({ 10 | title, 11 | description, 12 | children, 13 | }: SectionColumnsType) { 14 | return ( 15 |
16 |
17 |

{title}

18 |

19 | {description} 20 |

21 |
22 |
{children}
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useLocalStorage = ( 4 | key: string, 5 | initialValue: T, 6 | ): [T, (value: T) => void] => { 7 | const [storedValue, setStoredValue] = useState(initialValue); 8 | 9 | useEffect(() => { 10 | // Retrieve from localStorage 11 | const item = window.localStorage.getItem(key); 12 | if (item) { 13 | setStoredValue(JSON.parse(item)); 14 | } 15 | }, [key]); 16 | 17 | const setValue = (value: T) => { 18 | // Save state 19 | setStoredValue(value); 20 | // Save to localStorage 21 | window.localStorage.setItem(key, JSON.stringify(value)); 22 | }; 23 | return [storedValue, setValue]; 24 | }; 25 | 26 | export default useLocalStorage; 27 | -------------------------------------------------------------------------------- /src/components/shared/header-section.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderSectionProps { 2 | label?: string; 3 | title: string; 4 | subtitle?: string; 5 | } 6 | 7 | export function HeaderSection({ label, title, subtitle }: HeaderSectionProps) { 8 | return ( 9 |
10 | {label ? ( 11 |
12 | {label} 13 |
14 | ) : null} 15 |

16 | {title} 17 |

18 | {subtitle ? ( 19 |

20 | {subtitle} 21 |

22 | ) : null} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | 3 | import { db } from "@/db/db"; 4 | import { users } from "@/db/schema"; 5 | import { eq } from "drizzle-orm"; 6 | export const runtime = 'edge'; 7 | 8 | export const DELETE = auth(async (req) => { 9 | if (!req.auth) { 10 | return new Response("Not authenticated", { status: 401 }); 11 | } 12 | 13 | const currentUser = req.auth.user; 14 | if (!currentUser) { 15 | return new Response("Invalid user", { status: 401 }); 16 | } 17 | 18 | try { 19 | await db.delete(users).where(eq(users.id, currentUser.id as string)); 20 | } catch (error) { 21 | return new Response("Internal server error", { status: 500 }); 22 | } 23 | 24 | return new Response("User deleted successfully!", { status: 200 }); 25 | }); -------------------------------------------------------------------------------- /src/lib/locales.ts: -------------------------------------------------------------------------------- 1 | import { LocalePrefix } from "next-intl/dist/types/src/routing/types"; 2 | 3 | import { createSharedPathnamesNavigation } from 'next-intl/navigation'; 4 | 5 | const localePrefix: LocalePrefix = 'always'; 6 | // FIXME: Update this configuration file based on your project information 7 | export const AppConfig = { 8 | name: 'FFlow Next', 9 | locales: [ 10 | { 11 | id: 'en', 12 | name: 'English', 13 | }, 14 | { id: 'zh', name: '中文' }, 15 | ], 16 | defaultLocale: 'zh', 17 | localePrefix, 18 | }; 19 | 20 | export const AllLocales = AppConfig.locales.map((locale) => locale.id); 21 | 22 | export const { usePathname, useRouter } = createSharedPathnamesNavigation({ 23 | locales: AllLocales, 24 | localePrefix: AppConfig.localePrefix, 25 | }); 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "tailwindcss/no-custom-classname": "off", 14 | "tailwindcss/classnames-order": "off" 15 | }, 16 | "settings": { 17 | "tailwindcss": { 18 | "callees": ["cn"], 19 | "config": "tailwind.config.ts" 20 | }, 21 | "next": { 22 | "rootDir": true 23 | } 24 | }, 25 | "overrides": [ 26 | { 27 | "files": ["*.ts", "*.tsx"], 28 | "parser": "@typescript-eslint/parser" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { allPosts } from "contentlayer/generated"; 2 | 3 | import { constructMetadata, getBlurDataURL } from "@/lib/utils"; 4 | import { BlogPosts } from "@/components/content/blog-posts"; 5 | 6 | export const metadata = constructMetadata({ 7 | title: "Blog - FFlow Next", 8 | description: "Latest news and updates from Next FFlow Next.", 9 | }); 10 | 11 | export default async function BlogPage() { 12 | const posts = await Promise.all( 13 | allPosts 14 | .filter((post) => post.published) 15 | .sort((a, b) => b.date.localeCompare(a.date)) 16 | .map(async (post) => ({ 17 | ...post, 18 | blurDataURL: await getBlurDataURL(post.image), 19 | })), 20 | ); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/vectorstore/upstash-vector-store.d.ts: -------------------------------------------------------------------------------- 1 | import { Index } from "@upstash/vector"; 2 | import { Document } from "@langchain/core/documents"; 3 | import { 4 | MaxMarginalRelevanceSearchOptions, 5 | VectorStore, 6 | } from "@langchain/core/vectorstores"; 7 | 8 | 9 | type UpstashMetadata = Record; 10 | 11 | 12 | export class UpstashVectorStore extends VectorStore { 13 | declare FilterType: PineconeMetadata; 14 | 15 | constructor(embeddings: any); 16 | index: Index; 17 | similaritySearchVectorWithScore( 18 | query: any, 19 | k: any, 20 | filter: any, 21 | ): Promise; 22 | 23 | maxMarginalRelevanceSearch( 24 | query: string, 25 | options: MaxMarginalRelevanceSearchOptions 26 | ): Promise 27 | } 28 | -------------------------------------------------------------------------------- /src/components/link/link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NextLink, { LinkProps as NextLinkProps } from 'next/link'; 3 | import { useLocale } from 'next-intl'; 4 | 5 | interface MultiLangLinkProps extends Omit { 6 | href: string; 7 | className?: string; 8 | children?: React.ReactNode; 9 | target?: string; 10 | rel?: string; 11 | } 12 | 13 | const Link: React.FC = ({ href, children, className, target, rel, ...props }) => { 14 | const locale = useLocale(); 15 | 16 | const localizedHref = href.startsWith('/') ? `/${locale}${href}` : href; 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default Link; 26 | -------------------------------------------------------------------------------- /src/components/sections/info-landing.tsx: -------------------------------------------------------------------------------- 1 | import { infos } from "@/config/landing"; 2 | import { InfoLdg } from "@/types"; 3 | import { useTranslations } from "next-intl"; 4 | import InfoLandingClient from "./info-landing-client"; 5 | 6 | export default function InfoLanding({ locale }: { locale: string }) { 7 | const t = useTranslations("InfoLanding"); 8 | 9 | const translatedInfos: InfoLdg[] = infos.map((info) => ({ 10 | ...info, 11 | title: t(`${info.title}.title`), 12 | description: t(`${info.title}.description`), 13 | list: info.list.map((item) => ({ 14 | ...item, 15 | title: t(`${info.title}.list.${item.title}.title`), 16 | description: t(`${info.title}.list.${item.title}.description`), 17 | })), 18 | })); 19 | 20 | return ; 21 | } -------------------------------------------------------------------------------- /content/docs/zh/configuration/database.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 数据库 3 | description: 如何配置你的 Neon 数据库。 4 | --- 5 | 6 | 这个项目最初使用的是类似 PlanetScale 的数据库。但是最近免费套餐已被取消。 7 | 8 | 目前,本项目使用 **Neon** 的免费计划。查看他们的[定价页面](https://neon.tech/pricing)以获取更多信息。 9 | 10 | ## 步骤 11 | 12 | ### 创建 Neon 账户 13 | 14 | 如果你还没有 Neon 账户,只需按照[这里](https://neon.tech/docs/get-started-with-neon/signing-up)的步骤创建一个。 15 | 16 | ### 复制数据库 URL 17 | 18 | 在 Neon 上创建项目后,你可以复制数据库 URL。从列表中选择 `Prisma`,然后选择 `env` 标签。 19 | 20 | 21 | 别忘了使用眼睛图标显示隐藏在星号后面的信息。在深色模式下这个图标不太明显。 22 | 23 | 24 | 将其粘贴到你的 `.env` 文件中。 25 | 26 | ```js 27 | DATABASE_URL = "postgres://alex:AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname"; 28 | ``` 29 | 30 | ### 推送迁移 31 | 32 | 你可以使用命令行工具将迁移推送到你的数据库。 33 | 34 | ```bash 35 | npx prisma db push 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/shared/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@/types" 2 | import { AvatarProps } from "@radix-ui/react-avatar" 3 | 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 5 | import { Icons } from "@/components/shared/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 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { VariantProps, cva } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 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 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Your Crowdin credentials 3 | # 4 | "project_id_env": "CROWDIN_PROJECT_ID" 5 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 6 | "base_path": "." 7 | "base_url": "https://api.crowdin.com" 8 | 9 | # 10 | # Choose file structure in Crowdin 11 | # e.g. true or false 12 | # 13 | "preserve_hierarchy": true 14 | 15 | # 16 | # Files configuration 17 | # 18 | files: [ 19 | { 20 | # 21 | # Source files filter 22 | # e.g. "/resources/en/*.json" 23 | # 24 | "source": "/src/locales/en.json", 25 | 26 | # 27 | # Where translations will be placed 28 | # e.g. "/resources/%two_letters_code%/%original_file_name%" 29 | # 30 | "translation": "/src/locales/%two_letters_code%.json", 31 | 32 | # 33 | # File type 34 | # e.g. "json" 35 | # 36 | "type": "json", 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BlogHeaderLayout } from "@/components/content/blog-header-layout"; 2 | import MaxWidthWrapper from "@/components/shared/max-width-wrapper"; 3 | import { NextIntlClientProvider } from "next-intl"; 4 | import {getMessages, unstable_setRequestLocale} from 'next-intl/server'; 5 | 6 | export default async function BlogLayout({ 7 | children, 8 | params: { locale }, 9 | }: { 10 | children: React.ReactNode; 11 | params: { locale: string }; 12 | }) { 13 | unstable_setRequestLocale(locale); 14 | const messages = await getMessages({ locale }); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |