├── app ├── store │ ├── index.ts │ └── user.ts ├── .server │ ├── aisdk │ │ ├── index.ts │ │ └── kie-ai │ │ │ ├── type.ts │ │ │ └── index.ts │ ├── libs │ │ ├── markdown │ │ │ ├── tags │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── db.ts │ │ ├── creem │ │ │ ├── index.ts │ │ │ ├── type.ts │ │ │ ├── types.ts │ │ │ └── client.ts │ │ └── session.ts │ ├── constants │ │ ├── index.ts │ │ ├── product.ts │ │ └── pricing.ts │ ├── drizzle │ │ ├── migrations │ │ │ ├── 0003_clammy_strong_guy.sql │ │ │ ├── 0001_sleepy_whizzer.sql │ │ │ ├── meta │ │ │ │ └── _journal.json │ │ │ ├── 0004_melodic_speed.sql │ │ │ └── 0000_workable_quentin_quire.sql │ │ └── config.ts │ ├── model │ │ ├── signin_log.ts │ │ ├── user_auth.ts │ │ ├── order.ts │ │ ├── credit_consumptions.ts │ │ ├── credit_record.ts │ │ ├── subscriptions.ts │ │ ├── user.ts │ │ └── ai_tasks.ts │ ├── prompt │ │ ├── ai-hairstyle-kontext.ts │ │ └── ai-hairstyle.ts │ ├── services │ │ ├── r2-bucket.ts │ │ ├── auth.ts │ │ ├── credits.ts │ │ └── order.ts │ └── schema │ │ └── task.ts ├── components │ ├── ui │ │ ├── index.ts │ │ └── dropzone.tsx │ ├── common │ │ ├── index.ts │ │ ├── image.tsx │ │ ├── logo.tsx │ │ └── link.tsx │ ├── icons │ │ ├── index.ts │ │ ├── other.tsx │ │ ├── linktree.tsx │ │ ├── twitter.tsx │ │ ├── pinterest.tsx │ │ └── google.tsx │ ├── pages │ │ ├── legal │ │ │ └── index.tsx │ │ └── landing │ │ │ ├── faqs.tsx │ │ │ ├── features.tsx │ │ │ ├── index.tsx │ │ │ ├── how-it-works.tsx │ │ │ ├── alternating-content.tsx │ │ │ ├── testimonials.tsx │ │ │ ├── cta.tsx │ │ │ ├── pricing.tsx │ │ │ └── hero.tsx │ └── markdown │ │ ├── index.tsx │ │ └── TOC.tsx ├── features │ ├── oauth │ │ ├── index.ts │ │ └── google │ │ │ ├── btn.tsx │ │ │ └── index.tsx │ ├── layout │ │ ├── index.ts │ │ └── base-layout │ │ │ ├── index.tsx │ │ │ ├── socials.tsx │ │ │ ├── footer.tsx │ │ │ └── header.tsx │ ├── document │ │ └── index.tsx │ └── hairstyle_changer │ │ ├── hairstyle-select.tsx │ │ ├── confirm-preview.tsx │ │ └── style-configuration.tsx ├── hooks │ ├── data │ │ ├── index.ts │ │ └── use-tasks.ts │ └── dom │ │ ├── index.ts │ │ └── use-window-scroll.ts ├── utils │ └── meta.ts ├── routes │ ├── _meta │ │ ├── [robots.txt] │ │ │ ├── file.txt │ │ │ └── route.ts │ │ └── [sitemap.xml].tsx │ ├── _api │ │ ├── task.$task_no │ │ │ └── route.ts │ │ ├── create-order │ │ │ └── route.ts │ │ ├── create.ai-hairstyle │ │ │ └── route.ts │ │ └── auth │ │ │ └── route.ts │ ├── _webhooks │ │ ├── kie-image │ │ │ └── route.ts │ │ └── payment │ │ │ └── route.ts │ ├── _legal │ │ ├── terms │ │ │ ├── route.tsx │ │ │ └── content.md │ │ ├── cookie │ │ │ ├── route.tsx │ │ │ └── content.md │ │ └── privacy │ │ │ ├── route.tsx │ │ │ └── content.md │ ├── _callback │ │ └── payment │ │ │ └── route.ts │ └── base │ │ └── layout │ │ └── index.tsx ├── routes.ts ├── entry.server.tsx ├── app.css └── root.tsx ├── public ├── favicon.ico └── assets │ └── logo.webp ├── react-router.config.ts ├── tsconfig.node.json ├── types └── global.d.ts ├── tsconfig.json ├── vite.config.ts ├── workers └── app.ts ├── .gitignore ├── tsconfig.cloudflare.json ├── LICENSE ├── package.json ├── wrangler.jsonc ├── README.zh-CN.md └── README.md /app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user"; 2 | -------------------------------------------------------------------------------- /app/.server/aisdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kie-ai"; 2 | -------------------------------------------------------------------------------- /app/.server/libs/markdown/tags/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /app/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dropzone"; 2 | -------------------------------------------------------------------------------- /app/features/oauth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./google"; 2 | -------------------------------------------------------------------------------- /app/hooks/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-tasks"; 2 | -------------------------------------------------------------------------------- /app/features/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base-layout"; 2 | -------------------------------------------------------------------------------- /app/hooks/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-window-scroll"; 2 | -------------------------------------------------------------------------------- /app/.server/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pricing"; 2 | export * from "./product"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neyric/ai-hairstyle/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neyric/ai-hairstyle/HEAD/public/assets/logo.webp -------------------------------------------------------------------------------- /app/.server/drizzle/migrations/0003_clammy_strong_guy.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `ai_tasks` ADD `ext` text DEFAULT '{}' NOT NULL; -------------------------------------------------------------------------------- /app/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logo"; 2 | export * from "./link"; 3 | export * from "./image"; 4 | -------------------------------------------------------------------------------- /app/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./linktree"; 2 | export * from "./twitter"; 3 | export * from "./pinterest"; 4 | export * from "./google"; 5 | export * from "./other"; 6 | -------------------------------------------------------------------------------- /react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | ssr: true, 5 | future: { 6 | unstable_viteEnvironmentApi: true, 7 | }, 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /app/utils/meta.ts: -------------------------------------------------------------------------------- 1 | import type { MetaDescriptor } from "react-router"; 2 | 3 | export const createCanonical = ( 4 | pathname: string, 5 | domain: string 6 | ): MetaDescriptor => { 7 | return { 8 | tagName: "link", 9 | rel: "canonical", 10 | href: new URL(pathname, domain).toString(), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /app/.server/libs/markdown/index.ts: -------------------------------------------------------------------------------- 1 | import markdoc from "@markdoc/markdoc"; 2 | import * as tags from "./tags"; 3 | 4 | export function parseMarkdown(markdown: string) { 5 | const ast = markdoc.parse(markdown); 6 | 7 | const node = markdoc.transform(ast, { 8 | tags, 9 | }); 10 | 11 | return { ast, node }; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface UserInfo { 2 | email: string; 3 | name: string; 4 | avatar: string | null; 5 | created_at: number; 6 | } 7 | 8 | declare interface GoogleUserInfo { 9 | sub: string; 10 | name: string; 11 | given_name: string; 12 | picture?: string; 13 | email: string; 14 | email_verified: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /app/.server/libs/db.ts: -------------------------------------------------------------------------------- 1 | import { env } from "cloudflare:workers"; 2 | 3 | import { drizzle } from "drizzle-orm/d1"; 4 | import * as schema from "~/.server/drizzle/schema"; 5 | 6 | export * from "~/.server/drizzle/schema"; 7 | 8 | function connectDB() { 9 | const db = drizzle(env.DB, { schema }); 10 | 11 | return db; 12 | } 13 | 14 | export { schema, connectDB }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.cloudflare.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "types": [ 14 | "./worker-configuration.d.ts" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/.server/libs/creem/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "cloudflare:workers"; 2 | import { CreemApiClient } from "./client"; 3 | 4 | export const createCreem = () => { 5 | let client: CreemApiClient; 6 | if (import.meta.env.PROD) client = new CreemApiClient(); 7 | else { 8 | client = new CreemApiClient( 9 | "https://test-api.creem.io", 10 | env.CREEM_TEST_KEY 11 | ); 12 | } 13 | 14 | return client; 15 | }; 16 | -------------------------------------------------------------------------------- /app/routes/_meta/[robots.txt]/file.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /assets/ 4 | 5 | Sitemap: {DOMAIN}/sitemap.xml 6 | 7 | # For AI robots rule 8 | User-agent: GPTBot 9 | User-agent: Claude-Web 10 | User-agent: Anthropic-AI 11 | User-agent: PerplexityBot 12 | User-agent: GoogleOther 13 | User-agent: DuckAssistBot 14 | 15 | # llms for AI robots 16 | # LLM-Content: {DOMAIN}/llms.txt 17 | # LLM-Full-Content: {DOMAIN}/llms-full.txt 18 | -------------------------------------------------------------------------------- /app/routes/_api/task.$task_no/route.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/route"; 2 | import { data } from "react-router"; 3 | 4 | import { updateTaskStatus } from "~/.server/services/ai-tasks"; 5 | 6 | export const loader = async ({ params }: Route.LoaderArgs) => { 7 | const taskNo = params.task_no; 8 | const result = await updateTaskStatus(taskNo); 9 | 10 | return data(result); 11 | }; 12 | export type TaskResult = Awaited>["data"]; 13 | -------------------------------------------------------------------------------- /app/routes/_meta/[robots.txt]/route.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/route"; 2 | import file from "./file.txt?raw"; 3 | 4 | export const loader = ({ context }: Route.LoaderArgs) => { 5 | const DOMAIN = context.cloudflare.env.DOMAIN; 6 | const domain = DOMAIN.endsWith("/") ? DOMAIN.slice(0, -1) : DOMAIN; 7 | 8 | return new Response(file.replace(/{DOMAIN}/g, domain), { 9 | status: 200, 10 | headers: { 11 | "Content-Type": "text/plain", 12 | }, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import { cloudflare } from "@cloudflare/vite-plugin"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "vite"; 5 | import tsconfigPaths from "vite-tsconfig-paths"; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | cloudflare({ viteEnvironment: { name: "ssr" } }), 10 | tailwindcss(), 11 | reactRouter(), 12 | tsconfigPaths(), 13 | ], 14 | server: { 15 | host: "0.0.0.0", 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /app/.server/constants/product.ts: -------------------------------------------------------------------------------- 1 | export interface PRODUCT { 2 | price: number; 3 | credits: number; 4 | product_id: string; 5 | product_name: string; 6 | type: "once" | "monthly" | "yearly"; 7 | } 8 | 9 | export const CREDITS_PRODUCT: PRODUCT = { 10 | price: 9, 11 | credits: 100, 12 | product_id: import.meta.env.PROD 13 | ? "prod_3q2PT9pqzfw5URK7TdIhyb" 14 | : "prod_tMa1e6wOR5SnpYzLKUVaP", 15 | product_name: "Credits Pack", 16 | type: "once", 17 | }; 18 | 19 | export const PRODUCTS_LIST = [CREDITS_PRODUCT]; 20 | -------------------------------------------------------------------------------- /app/.server/drizzle/config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from "vite"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | const env = loadEnv("production", process.cwd(), ""); 5 | 6 | const credentials = { 7 | accountId: env.ACCOUNT_ID, 8 | databaseId: env.DATABASE_ID, 9 | token: env.ACCOUNT_TOKEN, 10 | }; 11 | 12 | export default defineConfig({ 13 | schema: "./app/.server/drizzle/schema.ts", 14 | out: "./app/.server/drizzle/migrations", 15 | dialect: "sqlite", 16 | driver: "d1-http", 17 | dbCredentials: credentials, 18 | }); 19 | -------------------------------------------------------------------------------- /app/routes/_webhooks/kie-image/route.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/route"; 2 | import { data } from "react-router"; 3 | 4 | import { updateTaskStatusByTaskId } from "~/.server/services/ai-tasks"; 5 | import type { GPT4oTaskCallbackJSON } from "~/.server/aisdk"; 6 | 7 | export const action = async ({ request }: Route.ActionArgs) => { 8 | const json = await request.json(); 9 | if (!json.data?.taskId) return data({}); 10 | await updateTaskStatusByTaskId(json.data.taskId); 11 | 12 | return data({}); 13 | }; 14 | -------------------------------------------------------------------------------- /app/store/user.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface UserStore { 4 | user?: UserInfo | null; 5 | credits: number; 6 | setUser: (user: UserInfo | null) => void; 7 | clearUser: () => void; 8 | setCredits: (credits: number) => void; 9 | } 10 | 11 | export const useUser = create((set) => { 12 | return { 13 | user: void 0, 14 | credits: 0, 15 | setUser: (user) => set({ user: user ?? null }), 16 | clearUser: () => set({ user: null }), 17 | setCredits: (credits) => set({ credits }), 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /app/features/layout/base-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { Header, type HeaderProps } from "./header"; 3 | import { Footer, type FooterProps } from "./footer"; 4 | 5 | export interface BaseLayoutProps { 6 | header: HeaderProps; 7 | footer: FooterProps; 8 | } 9 | 10 | export const BaseLayout = ({ 11 | header, 12 | footer, 13 | children, 14 | }: React.PropsWithChildren) => { 15 | return ( 16 | 17 |
18 | {children} 19 |