diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..70f541d
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,88 @@
+#--------------------------------------------------------------------------------------------------------
+# website URL
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_SITE_URL=http://localhost
+
+#--------------------------------------------------------------------------------------------------------
+# website name
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_WEBSITE_NAME="贴纸网站"
+
+#--------------------------------------------------------------------------------------------------------
+# image alt text
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT="贴纸网站"
+
+#--------------------------------------------------------------------------------------------------------
+# domain name
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_DOMAIN_NAME="Sticker"
+
+#--------------------------------------------------------------------------------------------------------
+# postgres config
+#--------------------------------------------------------------------------------------------------------
+POSTGRES_URL="postgres://sticker:pass1234@127.0.0.1:5432/sticker"
+
+#--------------------------------------------------------------------------------------------------------
+# Google auth config
+# 0 代表不检查登录,则登录相关的按钮也不展示出来,1代表要检查
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_CHECK_GOOGLE_LOGIN=0
+NEXT_PUBLIC_GOOGLE_CLIENT_ID=
+GOOGLE_SECRET_ID=
+
+#--------------------------------------------------------------------------------------------------------
+# NEXTAUTH config create command: openssl rand -base64 32
+#--------------------------------------------------------------------------------------------------------
+NEXTAUTH_URL=http://localhost
+NEXTAUTH_SECRET=
+
+#--------------------------------------------------------------------------------------------------------
+# Google gtag id
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_GOOGLE_TAG_ID=
+
+#--------------------------------------------------------------------------------------------------------
+# Update these with your Stripe credentials from https://dashboard.stripe.com/apikeys
+# 0 代表不检查支付,则支付页面也不展示出来,1代表要检查
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_CHECK_AVAILABLE_TIME=0
+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
+STRIPE_SECRET_KEY=
+STRIPE_WEBHOOK_SECRET=
+# 免费生成次数
+FREE_TIMES=2
+
+#--------------------------------------------------------------------------------------------------------
+# replicate config
+#--------------------------------------------------------------------------------------------------------
+# replicate 生成结束后的回调地址,本地时我才用的 ngrok,线上的话就是你的域名
+REPLICATE_WEBHOOK=
+# replicate 的 API token,需要去你的 replicate 账号里面复制 https://replicate.com/account/api-tokens
+REPLICATE_API_TOKEN=
+# 生成贴纸的API版本 https://replicate.com/fofr/sticker-maker/versions ,最新的那个版本只生成一张图片了,下方这个版本是还会一次生成两张图片的
+REPLICATE_API_VERSION="6443cc831f51eb01333f50b757157411d7cadb6215144cc721e3688b70004ad0"
+
+#--------------------------------------------------------------------------------------------------------
+# cloudflare R2 config
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_STORAGE_URL=
+STORAGE_DOMAIN=
+R2_BUCKET=
+R2_ACCOUNT_ID=
+R2_ENDPOINT=
+R2_TOKEN_VALUE=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+
+#--------------------------------------------------------------------------------------------------------
+# openai config
+#--------------------------------------------------------------------------------------------------------
+OPENAI_API_KEY=
+OPENAI_API_BASE_URL=https://openrouter.ai/api
+OPENAI_API_MODEL="openai/gpt-3.5-turbo"
+
+#--------------------------------------------------------------------------------------------------------
+# stickers 里面每页显示多少条数据
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_PAGES_SIZE=24
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..bc64a4c
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,88 @@
+#--------------------------------------------------------------------------------------------------------
+# website URL
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_SITE_URL=
+
+#--------------------------------------------------------------------------------------------------------
+# website name
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_WEBSITE_NAME=
+
+#--------------------------------------------------------------------------------------------------------
+# image alt text
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT=
+
+#--------------------------------------------------------------------------------------------------------
+# domain name
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_DOMAIN_NAME=
+
+#--------------------------------------------------------------------------------------------------------
+# postgres config
+#--------------------------------------------------------------------------------------------------------
+POSTGRES_URL=
+
+#--------------------------------------------------------------------------------------------------------
+# Google auth config
+# 0 代表不检查登录,则登录相关的按钮也不展示出来,1代表要检查
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_CHECK_GOOGLE_LOGIN=0
+NEXT_PUBLIC_GOOGLE_CLIENT_ID=
+GOOGLE_SECRET_ID=
+
+#--------------------------------------------------------------------------------------------------------
+# NEXTAUTH config create command: openssl rand -base64 32
+#--------------------------------------------------------------------------------------------------------
+NEXTAUTH_URL=
+NEXTAUTH_SECRET=
+
+#--------------------------------------------------------------------------------------------------------
+# Google gtag id
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_GOOGLE_TAG_ID=
+
+#--------------------------------------------------------------------------------------------------------
+# Update these with your Stripe credentials from https://dashboard.stripe.com/apikeys
+# 0 代表不检查支付,则支付页面也不展示出来,1代表要检查
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_CHECK_AVAILABLE_TIME=0
+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
+STRIPE_SECRET_KEY=
+STRIPE_WEBHOOK_SECRET=
+# 免费生成次数
+FREE_TIMES=2
+
+#--------------------------------------------------------------------------------------------------------
+# replicate config
+#--------------------------------------------------------------------------------------------------------
+# replicate 生成结束后的回调地址,本地时我才用的 ngrok,线上的话就是你的域名
+REPLICATE_WEBHOOK=
+# replicate 的 API token,需要去你的 replicate 账号里面复制 https://replicate.com/account/api-tokens
+REPLICATE_API_TOKEN=
+# 生成贴纸的API版本 https://replicate.com/fofr/sticker-maker/versions ,最新的那个版本只生成一张图片了,下方这个版本是还会一次生成两张图片的
+REPLICATE_API_VERSION="6443cc831f51eb01333f50b757157411d7cadb6215144cc721e3688b70004ad0"
+
+#--------------------------------------------------------------------------------------------------------
+# cloudflare R2 config
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_STORAGE_URL=
+STORAGE_DOMAIN=
+R2_BUCKET=
+R2_ACCOUNT_ID=
+R2_ENDPOINT=
+R2_TOKEN_VALUE=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+
+#--------------------------------------------------------------------------------------------------------
+# openai config
+#--------------------------------------------------------------------------------------------------------
+OPENAI_API_KEY=
+OPENAI_API_BASE_URL=https://openrouter.ai/api
+OPENAI_API_MODEL="openai/gpt-3.5-turbo"
+
+#--------------------------------------------------------------------------------------------------------
+# stickers 里面每页显示多少条数据
+#--------------------------------------------------------------------------------------------------------
+NEXT_PUBLIC_PAGES_SIZE=24
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/README.md b/README.md
index a7c9ed7..ec30e27 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,75 @@
# StickerShow
Sticker.Show 代码,仅限哥飞的朋友们社群成员使用
+
+### 1. 右上角 Fork 本项目到你自己的 github 仓库
+
+### 2. Clone你自己的仓库代码到本地
+
+```bash
+git clone (your git url)
+```
+
+### 3. 安装依赖
+
+```bash
+cd StickerShow && yarn
+#or
+cd StickerShow && npm install
+#or
+cd StickerShow && pnpm install
+```
+
+### 4. 复制 .env.example 重命名为 .env.local
+
+修改.env.local其中的配置为你项目的配置,生产环境配置在 .env.production
+
+### 5. 额外的配置
+
+1) 谷歌登录认证配置 👉 [Google-Auth-Help](https://github.com/SoraWebui/SoraWebui/blob/login/help/Google-Auth.md)
+2) 数据库配置 👉 Any PostgreSQL
+3) 目录 /sql 下有需要的数据表,创建数据库并执行这些SQL创建数据表
+4) R2的配置,为了存储生成的图片,需要去 Cloudflare 后台创建 bucket 后进行配置。项目用到了图像转换功能,通过URL来压缩图片,这个需要在 Cloudflare 的 「图像」->「转换」这里面针对具体的域名开启
+5) Stripe 价格配置在 src/configs/stripeConfig.ts ,里面具体的的配置项需要你从 stripe 后台获取。
+6) 对接 stripe 支付参考的是该项目👉 https://github.com/vercel/nextjs-subscription-payments
+上边这个支付项目能单独运行,但它用的 supabase,当前项目是已经改成了支持任何 PostgreSQL 数据库的代码
+
+### 6. 运行
+
+```bash
+yarn dev
+#or
+npm run dev
+#or
+pnpm dev
+```
+
+### 7. 在浏览器打开 [http://localhost](http://localhost)
+
+
+## 注意!!!
+### 1. 贴纸生成结果是通过接收 replicate 的回调,于是本地调试的时候,需要将 REPLICATE_WEBHOOK 这个配置成公网URL,可以配置 [ngrok](https://ngrok.com/) 来接收结果,本地就能处理生成的图片了
+
+类似配置成:https://0123-153-121-77-112.ngrok-free.app,这个是需要你本地运行 ngrok 与 其云端服务对上后生成的,直接用这个是不会通的哈
+
+线上时,REPLICATE_WEBHOOK 这个配置为你的线上域名就可以
+
+### 2. 项目用到 vercel 的定时任务来将贴纸文本翻译为其他语言,线上版本时会生效,配置在 vercel.json 这个文件,设置的一分钟运行一次,免费账户有限制(应该是只能一天调用一次),查看 vercel 的文档 https://vercel.com/docs/cron-jobs
+
+当然,你可以删除 vercel.json 这个文件,那样就不会触发定时任务了。你可以采用别的方式来调用接口触发定时任务,比如你自己的服务器运行一个脚本定时调用接口来翻译
+
+### 3. stripe 不激活的账号就是测试模式,就可以本地调通支付流程;对接支付参考的是这个项目👉 https://github.com/vercel/nextjs-subscription-payments
+
+### 4. 最重要的是熟悉代码且会改代码,本项目对接 stripe 支付只是简易版本对接,各种边界条件没有考虑到。
+
+比如本项目是支付后就无限制使用,没有对某个价格订阅的次数做限制,比如用户重复订阅的判断等等,这些需要你自己去完善。
+
+每个人想给订阅增加的限制,无法做到通用配置,需要你自己去研究代码该怎么添加。
+
+目前配置的是1个月付,1个年付,多个价格也可以,只是界面样式你得自己调整一下。
+
+### 5. 本项目现有代码是能够跑通全流程的,只需将所有配置都做好。
+配置项有点多,需要细心点去进行相关配置,不要遗漏。
+
+
+## 有任何疑问联系 Wechat: GeFei55
+
diff --git a/global.d.ts b/global.d.ts
new file mode 100755
index 0000000..977f992
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,3 @@
+// Use type safe message keys with `next-intl`
+type Messages = typeof import('./messages/en.json');
+declare interface IntlMessages extends Messages {}
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..2a82337
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,128 @@
+{
+ "FileDesc": {
+ "lang": "en",
+ "language": "English",
+ "languageInChineseSimple": "英语"
+ },
+ "IndexPageText": {
+ "title": "英文Title",
+ "description": "英文description",
+ "h1Text": "英文首页 H1",
+ "descriptionBelowH1Text": "英文首页 H1下方的文字"
+ },
+ "CommonText": {
+ "loadingText": "Loading...",
+ "generateText": "Generating...",
+ "placeholderText": "Type or paste text here...",
+ "buttonText": "Generate",
+ "footerDescText": "英文footer左下角文字",
+ "timesLeft": "You can generate",
+ "timesRight": "times",
+ "download": "Download",
+ "result": "Result",
+ "moreWorks": "My Stickers",
+ "generateNew": "Generate New",
+ "displayPublic": "display public",
+ "similarText": "similar",
+ "prompt": "input prompt",
+ "revised": "revised prompt",
+ "exploreMore": "More Stickers",
+ "keyword": "sticker",
+ "searchButtonText": "Search"
+ },
+ "MenuText": {
+ "header0": "Sticker Generator",
+ "header1": "My Stickers",
+ "header2": "Discover Stickers",
+ "header3": "Search Stickers",
+ "footerLegal": "Legal",
+ "footerLegal0": "Privacy Policy",
+ "footerLegal1": "Terms & Conditions",
+ "footerSupport": "Support",
+ "footerSupport0": "Pricing",
+ "footerSupport1": "Manage Subscribe"
+ },
+ "AuthText": {
+ "loginText": "Log in",
+ "loginModalDesc": "Please continue by logging in",
+ "loginModalButtonText": "Login with Google",
+ "logoutModalDesc": "Are you want to log out ?",
+ "confirmButtonText": "Confirm",
+ "cancelButtonText": "Cancel"
+ },
+ "PricingText": {
+ "title": "Pricing",
+ "description": "价格页面description",
+ "h1Text": "Get plan for generate",
+ "basic": "Basic",
+ "essential": "Essential",
+ "growth": "Growth",
+ "buyText": "Get started",
+ "popularText": "Popular",
+ "creditsText": "Generate times",
+ "creditText": "Generate",
+ "free": "Free",
+ "free0": "Free plan",
+ "freeText": "Current plan",
+ "freeIntro0": "%freeTimes% generate per month",
+ "freeIntro1": "Choose not to make the generated results public",
+ "freeIntro2": "Download images with transparent background",
+ "subscriptionIntro0": "Unlimited Generate",
+ "subscriptionIntro1": "Choose not to make the generated results public",
+ "subscriptionIntro2": "Download images with transparent background",
+ "subscriptionIntro3": "Download images in SVG format",
+ "subscriptionIntro4": "Image resolution increased to 1024X1024",
+ "monthText": "month",
+ "monthlyText": "billed monthly",
+ "annualText": "annual",
+ "annuallyText": "billed yearly",
+ "annuallySaveText": "save 50%"
+ },
+ "PrivacyPolicyText": {
+ "title": "英文隐私政策 title",
+ "description": "英文隐私政策description",
+ "h1Text": "英文隐私政策 H1",
+ "detailText": "英文隐私政策详情 markdown 文本"
+ },
+ "TermsOfServiceText": {
+ "title": "英文服务条款 title",
+ "description": "英文服务条款description",
+ "h1Text": "英文服务条款 H1",
+ "detailText": "英文服务条款详情 markdown 文本"
+ },
+ "WorksText": {
+ "title": "英文我的页面 title",
+ "description": "英文我的页面 description",
+ "h1Text": "英文我的页面 H1",
+ "descriptionBelowH1Text": "英文我的页面 H1下方的文字",
+ "descText": "Don't have sticker",
+ "toContinue": "to continue."
+ },
+ "ExploreText": {
+ "title": "Discover %countSticker% Free PNG Stickers for Download",
+ "description": "Discover over %countSticker% free stickers in PNG format for your creative projects. Easily download and add flair to social media, websites, and more!",
+ "h1Text": "Discover Stickers",
+ "descriptionBelowH1Text": "Your Go-To Free Online Custom Sticker Maker & Generator!",
+ "pageText": "Page %pageNumber%",
+ "h2Text": "Discover Our Collection of %countSticker% Free Stickers in PNG Format for Easy Download"
+ },
+ "DetailText": {
+ "title": "%prompt% Sticker, Download in PNG or SVG",
+ "description": "Discover the %prompt% sticker, Download in PNG or SVG format now!",
+ "h1Text": "%prompt% Sticker",
+ "descriptionBelowH1Text": "Discover the %prompt% sticker, Download in PNG or SVG format now!",
+ "numberText": "#%detailId%",
+ "h2Text": "%prompt% Stickers Collection"
+ },
+ "SearchText": {
+ "title": "Search %countSticker% Free PNG Stickers for Download",
+ "description": "Search over %countStickerAll% free stickers in PNG format for your creative projects. Easily download and add flair to social media, websites, and more!",
+ "h1Text": "Search Stickers",
+ "h2Text": "Search Our Collection of %countSticker% Free Stickers in PNG Format for Easy Download",
+ "titleSearch": "Search %countSticker% Free %prompt% Stickers",
+ "h2TextSearch": "Search Our Collection of %countSticker% Free %prompt% Stickers in PNG Format for Easy Download"
+ },
+ "QuestionText": {
+ "detailText": ""
+ }
+}
diff --git a/messages/zh.json b/messages/zh.json
new file mode 100644
index 0000000..e953dba
--- /dev/null
+++ b/messages/zh.json
@@ -0,0 +1,128 @@
+{
+ "FileDesc": {
+ "lang": "en",
+ "language": "简体中文",
+ "languageInChineseSimple": "简体中文"
+ },
+ "IndexPageText": {
+ "title": "中文Title",
+ "description": "中文description",
+ "h1Text": "首页 H1",
+ "descriptionBelowH1Text": "首页 H1下方的文字"
+ },
+ "CommonText": {
+ "loadingText": "加载中...",
+ "generateText": "生成中...",
+ "placeholderText": "在此处输入或粘贴文本...",
+ "buttonText": "生成",
+ "footerDescText": "footer左下角文字",
+ "timesLeft": "您可以生成",
+ "timesRight": "次",
+ "download": "下载",
+ "result": "结果",
+ "moreWorks": "我的贴纸",
+ "generateNew": "生成新的",
+ "displayPublic": "公开展示",
+ "similarText": "相似",
+ "prompt": "输入提示",
+ "revised": "修订提示",
+ "exploreMore": "更多贴纸",
+ "keyword": "贴纸",
+ "searchButtonText": "搜索"
+ },
+ "MenuText": {
+ "header0": "贴纸生成器",
+ "header1": "我的贴纸",
+ "header2": "发现贴纸",
+ "header3": "搜索贴纸",
+ "footerLegal": "法律信息",
+ "footerLegal0": "隐私政策",
+ "footerLegal1": "条款和条件",
+ "footerSupport": "支持",
+ "footerSupport0": "定价",
+ "footerSupport1": "管理订阅"
+ },
+ "AuthText": {
+ "loginText": "登录",
+ "loginModalDesc": "请继续登录",
+ "loginModalButtonText": "使用 Google 登录",
+ "logoutModalDesc": "您想要登出吗?",
+ "confirmButtonText": "确认",
+ "cancelButtonText": "取消"
+ },
+ "PricingText": {
+ "title": "定价",
+ "description": "价格页面description",
+ "h1Text": "获取生成计划",
+ "basic": "基础",
+ "essential": "必要",
+ "growth": "成长",
+ "buyText": "开始",
+ "popularText": "受欢迎的",
+ "creditsText": "生成次数",
+ "creditText": "次",
+ "free": "免费",
+ "free0": "免费计划",
+ "freeText": "当前计划",
+ "freeIntro0": "每月生成 %freeTimes% 次",
+ "freeIntro1": "选择不公开生成结果",
+ "freeIntro2": "下载带有透明背景的图片",
+ "subscriptionIntro0": "无限生成",
+ "subscriptionIntro1": "选择不公开生成结果",
+ "subscriptionIntro2": "下载带有透明背景的图片",
+ "subscriptionIntro3": "以 SVG 格式下载图片",
+ "subscriptionIntro4": "图片分辨率提升至 1024X1024",
+ "monthText": "月",
+ "monthlyText": "按月计费",
+ "annualText": "年",
+ "annuallyText": "按年计费",
+ "annuallySaveText": "节省 50%"
+ },
+ "PrivacyPolicyText": {
+ "title": "隐私政策 title",
+ "description": "隐私政策description",
+ "h1Text": "隐私政策 H1",
+ "detailText": "隐私政策详情 markdown 文本"
+ },
+ "TermsOfServiceText": {
+ "title": "服务条款 title",
+ "description": "服务条款description",
+ "h1Text": "服务条款 H1",
+ "detailText": "服务条款详情 markdown 文本"
+ },
+ "WorksText": {
+ "title": "我的页面 title",
+ "description": "我的页面 description",
+ "h1Text": "我的页面 H1",
+ "descriptionBelowH1Text": "我的页面 H1下方的文字",
+ "descText": "没有贴纸",
+ "toContinue": "继续。"
+ },
+ "ExploreText": {
+ "title": "发现 %countSticker% 张免费 PNG 贴纸下载",
+ "description": "为您的创意项目发现超过 %countSticker% 张免费的 PNG 格式贴纸。轻松下载并为社交媒体、网站等增添风采!",
+ "h1Text": "发现页面 H1",
+ "descriptionBelowH1Text": "发现页面 H1下方的文字",
+ "pageText": "第 %pageNumber% 页",
+ "h2Text": "探索我们的贴纸系列,包含 %countSticker% 张免费的 PNG 格式贴纸,便于下载"
+ },
+ "DetailText": {
+ "title": "%prompt% 贴纸,以 PNG 或 SVG 格式下载",
+ "description": "发现 %prompt% 贴纸,现在就下载 PNG 或 SVG 格式!",
+ "h1Text": "%prompt% 贴纸",
+ "descriptionBelowH1Text": "发现 %prompt% 贴纸,现在就下载 PNG 或 SVG 格式!",
+ "numberText": "#%detailId%",
+ "h2Text": "%prompt% 贴纸系列"
+ },
+ "SearchText": {
+ "title": "搜索 %countSticker% 张免费 PNG 贴纸进行下载",
+ "description": "为您的创意项目搜索超过 %countSticker% 张免费的 %prompt% PNG 格式贴纸。轻松下载并为社交媒体、网站等增添风采!",
+ "h1Text": "搜索贴纸",
+ "h2Text": "搜索我们的 %countSticker% 张免费贴纸,支持 PNG 格式,便于下载",
+ "titleSearch": "搜索 %countSticker% 免费 %prompt% 贴纸",
+ "h2TextSearch": "搜索我们的 %countSticker% 张免费 %prompt% 贴纸,支持 PNG 格式,方便下载"
+ },
+ "QuestionText": {
+ "detailText": ""
+ }
+}
diff --git a/next.config.mjs b/next.config.mjs
new file mode 100644
index 0000000..43efb54
--- /dev/null
+++ b/next.config.mjs
@@ -0,0 +1,18 @@
+import createNextIntlPlugin from 'next-intl/plugin';
+
+const withNextIntl = createNextIntlPlugin();
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ async redirects() {
+ return [
+ {source: '/en', destination: '/', permanent: true},
+ {source: '/stickers/1', destination: '/stickers', permanent: true},
+ {source: '/stickers/0', destination: '/stickers', permanent: true},
+ {source: '/:locale/stickers/1', destination: '/:locale/stickers', permanent: true},
+ {source: '/:locale/stickers/0', destination: '/:locale/stickers', permanent: true},
+ ];
+ }
+};
+
+export default withNextIntl(nextConfig);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..768bfc4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "next-init",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --port 80",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@headlessui/react": "^1.7.18",
+ "@heroicons/react": "^2.1.1",
+ "@next/third-parties": "^14.1.3",
+ "@stripe/stripe-js": "^3.0.7",
+ "@tailwindcss/typography": "^0.5.10",
+ "ahooks": "^3.7.10",
+ "aws-sdk": "^2.1572.0",
+ "clsx": "^2.1.0",
+ "date-fns": "^3.3.1",
+ "google-auth-library": "^9.6.3",
+ "next": "14.1.3",
+ "next-auth": "^4.24.6",
+ "next-intl": "^3.9.2",
+ "pg": "^8.11.3",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-icons": "^5.0.1",
+ "react-markdown": "^9.0.1",
+ "replicate": "^0.27.1",
+ "stripe": "^14.19.0",
+ "uuid": "^9.0.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "14.1.3",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "typescript": "^5"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/public/appicon.svg b/public/appicon.svg
new file mode 100644
index 0000000..6a1f3ff
--- /dev/null
+++ b/public/appicon.svg
@@ -0,0 +1,172 @@
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..97cecde
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100755
index 0000000..601230b
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,8 @@
+User-Agent: *
+Allow: *
+Disallow: /api
+Disallow: /api/
+Disallow: /api*
+Disallow: /*/api
+Disallow: /*/api/
+Disallow: /*/api*
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..7e4032e
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,124 @@
+
+
+
+ https://sticker.show/
+ 2024-03-18
+ daily
+ 1.0
+
+
+ https://sticker.show/stickers
+ 2024-03-18
+ daily
+ 1.0
+
+
+ https://sticker.show/de/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/de/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/es/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/es/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/fr/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/fr/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/ja/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/ja/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/ko/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/ko/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/pt/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/pt/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/tw/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/tw/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/vi/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/vi/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/zh/
+ 2024-03-18
+ daily
+ 0.9
+
+
+ https://sticker.show/zh/stickers
+ 2024-03-18
+ daily
+ 0.9
+
+
+
diff --git a/public/top_blurred.png b/public/top_blurred.png
new file mode 100644
index 0000000..3ab0ad6
Binary files /dev/null and b/public/top_blurred.png differ
diff --git a/public/website.svg b/public/website.svg
new file mode 100644
index 0000000..2d96531
--- /dev/null
+++ b/public/website.svg
@@ -0,0 +1,336 @@
+
+
+
diff --git a/sql/tables/1_user_info.sql b/sql/tables/1_user_info.sql
new file mode 100644
index 0000000..94f03ca
--- /dev/null
+++ b/sql/tables/1_user_info.sql
@@ -0,0 +1,31 @@
+-- auto-generated definition
+create table user_info
+(
+ id bigint generated by default as identity
+ primary key,
+ created_at timestamp with time zone default now() not null,
+ updated_at timestamp with time zone default now() not null,
+ user_id varchar,
+ name varchar,
+ email varchar,
+ image varchar,
+ last_login_ip varchar
+);
+
+comment on table user_info is 'user info table';
+
+comment on column user_info.id is 'sequence id';
+
+comment on column user_info.created_at is 'create time';
+
+comment on column user_info.updated_at is 'update time';
+
+comment on column user_info.user_id is 'user uuid';
+
+comment on column user_info.name is 'user name';
+
+comment on column user_info.email is 'user email';
+
+comment on column user_info.image is 'user avatar path';
+
+comment on column user_info.last_login_ip is 'user last login ip';
diff --git a/sql/tables/2_user_available.sql b/sql/tables/2_user_available.sql
new file mode 100644
index 0000000..1ee54e4
--- /dev/null
+++ b/sql/tables/2_user_available.sql
@@ -0,0 +1,25 @@
+-- auto-generated definition
+create table user_available
+(
+ id bigint generated by default as identity
+ primary key,
+ created_at timestamp with time zone default now() not null,
+ updated_at timestamp with time zone default now() not null,
+ user_id varchar,
+ stripe_customer_id varchar,
+ available_times integer
+);
+
+comment on table user_available is '用户可用点数记录表';
+
+comment on column user_available.id is '自增id';
+
+comment on column user_available.created_at is '创建时间';
+
+comment on column user_available.updated_at is '更新时间';
+
+comment on column user_available.user_id is '用户id';
+
+comment on column user_available.stripe_customer_id is 'stripe用户id';
+
+comment on column user_available.available_times is '可用点数';
diff --git a/sql/tables/3_stripe_customers.sql b/sql/tables/3_stripe_customers.sql
new file mode 100644
index 0000000..5eeb2f7
--- /dev/null
+++ b/sql/tables/3_stripe_customers.sql
@@ -0,0 +1,9 @@
+-- auto-generated definition
+create table stripe_customers
+(
+ user_id varchar not null
+ primary key,
+ stripe_customer_id text
+);
+
+comment on table stripe_customers is 'stripe customers';
diff --git a/sql/tables/4_stripe_subscriptions.sql b/sql/tables/4_stripe_subscriptions.sql
new file mode 100644
index 0000000..fa72974
--- /dev/null
+++ b/sql/tables/4_stripe_subscriptions.sql
@@ -0,0 +1,23 @@
+create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid', 'paused');
+-- auto-generated definition
+create table stripe_subscriptions
+(
+ id text not null
+ primary key,
+ user_id varchar,
+ status subscription_status,
+ metadata jsonb,
+ price_id text,
+ quantity integer,
+ cancel_at_period_end boolean,
+ created timestamp with time zone default timezone('utc'::text, now()) not null,
+ current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,
+ current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,
+ ended_at timestamp with time zone default timezone('utc'::text, now()),
+ cancel_at timestamp with time zone default timezone('utc'::text, now()),
+ canceled_at timestamp with time zone default timezone('utc'::text, now()),
+ trial_start timestamp with time zone default timezone('utc'::text, now()),
+ trial_end timestamp with time zone default timezone('utc'::text, now())
+);
+
+comment on table stripe_subscriptions is 'subscriptions';
diff --git a/sql/tables/5_works.sql b/sql/tables/5_works.sql
new file mode 100644
index 0000000..d9ced6c
--- /dev/null
+++ b/sql/tables/5_works.sql
@@ -0,0 +1,47 @@
+-- auto-generated definition
+create table works
+(
+ id bigint generated by default as identity
+ primary key,
+ created_at timestamp with time zone default now() not null,
+ updated_at timestamp with time zone default now() not null,
+ uid varchar,
+ input_text varchar,
+ revised_text varchar,
+ output_url varchar,
+ is_public boolean default false,
+ status integer,
+ user_id varchar,
+ is_origin boolean,
+ origin_language varchar,
+ current_language varchar,
+ is_delete boolean default false
+);
+
+comment on table works is 'works';
+
+comment on column works.id is 'sequence id';
+
+comment on column works.created_at is 'create time';
+
+comment on column works.updated_at is 'update time';
+
+comment on column works.uid is 'uid';
+
+comment on column works.input_text is 'input_text';
+
+comment on column works.revised_text is 'revised_text';
+
+comment on column works.output_url is 'output_url';
+
+comment on column works.is_public is 'is_public';
+
+comment on column works.status is 'status';
+
+comment on column works.user_id is 'user_id';
+
+comment on column works.is_origin is 'is_origin';
+
+comment on column works.origin_language is 'origin_language';
+
+comment on column works.current_language is 'current_language';
diff --git a/sql/tables/6_key_value.sql b/sql/tables/6_key_value.sql
new file mode 100644
index 0000000..f5966f7
--- /dev/null
+++ b/sql/tables/6_key_value.sql
@@ -0,0 +1,12 @@
+-- auto-generated definition
+create table key_value
+(
+ key varchar not null
+ constraint key_value_pk
+ primary key,
+ value varchar
+);
+
+comment on column key_value.key is 'key';
+
+comment on column key_value.value is 'value';
diff --git a/sql/tables/7_works_translate_task.sql b/sql/tables/7_works_translate_task.sql
new file mode 100644
index 0000000..d601d43
--- /dev/null
+++ b/sql/tables/7_works_translate_task.sql
@@ -0,0 +1,25 @@
+-- auto-generated definition
+create table works_translate_task
+(
+ id bigint generated by default as identity
+ primary key,
+ created_at timestamp with time zone default now() not null,
+ updated_at timestamp with time zone default now() not null,
+ uid varchar,
+ origin_language varchar,
+ status varchar
+);
+
+comment on table works_translate_task is 'works_translate_task';
+
+comment on column works_translate_task.id is '自增id';
+
+comment on column works_translate_task.created_at is '创建时间';
+
+comment on column works_translate_task.updated_at is '更新时间';
+
+comment on column works_translate_task.uid is 'uid';
+
+comment on column works_translate_task.origin_language is 'origin_language';
+
+comment on column works_translate_task.status is 'status: 0未翻译,1已翻译';
diff --git a/sql/tables/8_search_log.sql b/sql/tables/8_search_log.sql
new file mode 100644
index 0000000..38ad544
--- /dev/null
+++ b/sql/tables/8_search_log.sql
@@ -0,0 +1,25 @@
+-- auto-generated definition
+create table search_log
+(
+ id bigint generated by default as identity
+ primary key,
+ created_at timestamp with time zone default now() not null,
+ search_text varchar,
+ result_count varchar,
+ user_agent varchar,
+ search_ip varchar
+);
+
+comment on table search_log is 'search_log';
+
+comment on column search_log.id is '自增id';
+
+comment on column search_log.created_at is 'created_at';
+
+comment on column search_log.search_text is 'search_text';
+
+comment on column search_log.result_count is 'result_count';
+
+comment on column search_log.user_agent is 'user_agent';
+
+comment on column search_log.search_ip is 'search_ip';
diff --git a/sql/tables/9_sensitive_words.sql b/sql/tables/9_sensitive_words.sql
new file mode 100644
index 0000000..396dfb7
--- /dev/null
+++ b/sql/tables/9_sensitive_words.sql
@@ -0,0 +1,16 @@
+-- auto-generated definition
+create table sensitive_words
+(
+ id bigint generated by default as identity
+ primary key,
+ words varchar,
+ level varchar
+);
+
+comment on table sensitive_words is 'sensitive';
+
+comment on column sensitive_words.id is '自增id';
+
+comment on column sensitive_words.words is 'words';
+
+comment on column sensitive_words.level is 'level';
diff --git a/src/app/[locale]/PageComponent.tsx b/src/app/[locale]/PageComponent.tsx
new file mode 100644
index 0000000..e46c485
--- /dev/null
+++ b/src/app/[locale]/PageComponent.tsx
@@ -0,0 +1,397 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import {useCommonContext} from "~/context/common-context";
+import {useEffect, useRef, useState} from "react";
+import {useInterval} from "ahooks";
+import PricingModal from "~/components/PricingModal";
+import Link from "next/link";
+import {Switch} from "@headlessui/react";
+import Markdown from "react-markdown";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink";
+import {useRouter} from "next/navigation";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import TopBlurred from "~/components/TopBlurred";
+import {pinterestSvg} from '~/components/svg'
+
+function classNames(...classes) {
+ return classes.filter(Boolean).join(' ')
+}
+
+const PageComponent = ({
+ locale,
+ indexText,
+ questionText,
+ resultInfoListInit,
+ searchParams,
+ }) => {
+ const router = useRouter();
+ const [pagePath] = useState("");
+
+ const {
+ setShowLoadingModal,
+ setShowLoginModal,
+ setShowPricingModal,
+ setShowGeneratingModal,
+ commonText,
+ userData,
+ pricingText,
+ menuText
+ } = useCommonContext();
+ const [resultInfoList, setResultInfoList] = useState(resultInfoListInit);
+ const [countRefresh, setCountRefresh] = useState(0);
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ getLocalStorage()
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0') {
+ setIntervalAvailableTimes(1000);
+ }
+ setShowLoadingModal(false);
+ // setIntervalLatest(10000);
+ return () => {
+ }
+ }, []);
+
+ const getLocalStorage = () => {
+ const textStr = localStorage.getItem('textStr');
+ if (textStr) {
+ setTextStr(textStr);
+ localStorage.removeItem('textStr');
+ return;
+ }
+ if (searchParams?.prompt) {
+ setTextStr(searchParams.prompt);
+ }
+ }
+
+ const [textStr, setTextStr] = useState('');
+
+ const handleSubmit = async (e: { preventDefault: () => void }) => {
+ e.preventDefault();
+ if (!textStr) {
+ return;
+ }
+ if (!userData && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ setShowLoginModal(true);
+ localStorage.setItem('textStr', textStr);
+ return;
+ }
+ setShowGeneratingModal(true);
+ const requestData = {
+ textStr: textStr,
+ user_id: userData?.user_id,
+ is_public: isPublic
+ }
+ const responseData = await fetch(`/api/generate/handle`, {
+ method: 'POST',
+ body: JSON.stringify(requestData)
+ });
+ const result = await responseData.json();
+ if (result.status == 601 && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ setShowLoginModal(true);
+ localStorage.setItem('textStr', textStr);
+ return;
+ }
+ if (result.status == 602 && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ setShowPricingModal(true);
+ localStorage.setItem('textStr', textStr);
+ return;
+ }
+ const currentUid = result.uid;
+ setUid(currentUid);
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ setIntervalAvailableTimes(1000);
+ }
+ setIntervalResultInfo(6000);
+ }
+
+ const [availableTimes, setAvailableTimes] = useState({
+ available_times: 0,
+ subscribeStatus: '0'
+ });
+ const [resultInfo, setResultInfo] = useState({
+ uid: '',
+ status: 0,
+ input_text: '',
+ output_url: [],
+ revised_text: '',
+ origin_language: '',
+ current_language: ''
+ });
+ const [intervalAvailableTimes, setIntervalAvailableTimes] = useState(undefined);
+ const [uid, setUid] = useState('');
+ const [intervalResultInfo, setIntervalResultInfo] = useState(undefined);
+
+ const getResultInfo = async () => {
+ if (!userData?.user_id && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ return
+ }
+ const userId = userData?.user_id;
+ const response = await fetch(`/api/works/getResultInfo?uid=${uid}&userId=${userId}`);
+ const resultInfo = await response.json();
+ if (resultInfo.status == 1) {
+ setResultInfo(resultInfo);
+ router.push(getLinkHref(locale, `sticker/${resultInfo.uid}`));
+ setShowGeneratingModal(false);
+ setIntervalResultInfo(undefined);
+ setIntervalAvailableTimes(1000);
+ }
+ }
+ useInterval(() => {
+ getResultInfo();
+ }, intervalResultInfo);
+
+ const getAvailableTimes = async () => {
+ if (!userData) {
+ return;
+ }
+ const userId = userData.user_id;
+ if (userId) {
+ const response = await fetch(`/api/user/getAvailableTimes?userId=${userId}`);
+ const availableTimes = await response.json();
+ setAvailableTimes(availableTimes);
+ if (availableTimes.available_times >= 0) {
+ setIntervalAvailableTimes(undefined);
+ }
+ }
+ }
+ useInterval(() => {
+ getAvailableTimes();
+ }, intervalAvailableTimes);
+
+ const downloadResult = (url) => {
+ window.location.href = url;
+ }
+
+ const [isPublic, setIsPublic] = useState(true);
+
+
+ const checkSubscribe = () => {
+ if (availableTimes.subscribeStatus == 'active') {
+ setIsPublic(!isPublic);
+ } else {
+ setShowPricingModal(true);
+ }
+ }
+
+ const [intervalLatest, setIntervalLatest] = useState(undefined);
+ const getLatestList = async () => {
+ if (countRefresh >= 9) {
+ setIntervalLatest(undefined);
+ return;
+ }
+ const requestData = {
+ locale: locale
+ }
+ const response = await fetch(`/api/works/getLatestPublicResultList`, {
+ method: 'POST',
+ body: JSON.stringify(requestData)
+ });
+ const result = await response.json();
+ setCountRefresh(countRefresh + 1);
+ setResultInfoList(result);
+ }
+ useInterval(() => {
+ getLatestList();
+ }, intervalLatest);
+
+ const hasAnyKey = (obj) => {
+ return Object.keys(obj).length > 0;
+ }
+
+ return (
+ <>
+ {
+ hasAnyKey(searchParams) ?
+
+ :
+ null
+ }
+
+
+
+
+
+
+
+
+
{indexText.h1Text}
+
+
{indexText.descriptionBelowH1Text}
+
+
+
+
+
+
+
+
+
{menuText.header2}
+
+
+ {resultInfoList.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+ setShowLoadingModal(true)}
+ className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
+ {commonText.exploreMore} {'>>'}
+
+
+
+
+ {questionText.detailText}
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/[...rest]/page.tsx b/src/app/[locale]/[...rest]/page.tsx
new file mode 100755
index 0000000..6389a47
--- /dev/null
+++ b/src/app/[locale]/[...rest]/page.tsx
@@ -0,0 +1,5 @@
+import {notFound} from 'next/navigation';
+
+export default function CatchAllPage() {
+ notFound();
+}
diff --git a/src/app/[locale]/api/auth/[...nextauth]/route.ts b/src/app/[locale]/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..cdb5882
--- /dev/null
+++ b/src/app/[locale]/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,85 @@
+import NextAuth, {NextAuthOptions} from "next-auth";
+import GoogleProvider from "next-auth/providers/google";
+import CredentialsProvider from 'next-auth/providers/credentials';
+import { OAuth2Client } from 'google-auth-library';
+import {checkAndSaveUser, getUserByEmail} from "~/servers/user";
+import {headers} from "next/headers";
+
+const googleAuthClient = new OAuth2Client(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID)
+const authOptions: NextAuthOptions = {
+ // Configure one or more authentication providers
+ providers: [
+ GoogleProvider({
+ clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_SECRET_ID
+ }),
+ // connect with google api internally
+ CredentialsProvider({
+ // We will use this id later to specify for what Provider we want to trigger the signIn method
+ id: "googleonetap",
+ name: "google-one-tap",
+ // This means that the authentication will be done through a single credential called 'credential'
+ credentials: {
+ credential: { type: "text" },
+ },
+ // @ts-ignore
+ authorize: async (credentials) => {
+ // These next few lines are simply the recommended way to use the Google Auth Javascript API as seen in the Google Auth docs
+ // What is going to happen is that t he Google One Tap UI will make an API call to Google and return a token associated with the user account
+ // This token is then passed to the authorize function and used to retrieve the customer information (payload).
+ // If this doesn't make sense yet, come back to it after having seen the custom hook.
+
+ const token = credentials!.credential;
+ const ticket = await googleAuthClient.verifyIdToken({
+ idToken: token,
+ audience: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
+ });
+
+ const payload = ticket.getPayload();
+ if (!payload) {
+ throw new Error("Cannot extract payload from signin token");
+ }
+ const { email, name, picture: image } = payload;
+ if (!email) {
+ throw new Error("Email not available");
+ }
+ const user = {email, name, image}
+ const headerAll = headers();
+ const userIp = headerAll.get("x-forwarded-for");
+ await checkAndSaveUser(user.name, user.email, user.image, userIp);
+ return user
+ }
+ })
+ ],
+ secret: process.env.NEXTAUTH_SECRET,
+ debug: false,
+ callbacks: {
+ async signIn({user, account, profile, email, credentials}) {
+ const headerAll = headers();
+ const userIp = headerAll.get("x-forwarded-for");
+ await checkAndSaveUser(user.name, user.email, user.image, userIp);
+ return true
+ },
+ async redirect({url, baseUrl}) {
+ // Allows relative callback URLs
+ if (url.startsWith("/")) return `${baseUrl}${url}`
+ // Allows callback URLs on the same origin
+ else if (new URL(url).origin === baseUrl) return url
+ return baseUrl
+ },
+ async session({session}) {
+ if (session) {
+ const email = session?.user?.email;
+ if (email) {
+ session.user = await getUserByEmail(email);
+ return session;
+ }
+ }
+ return session;
+ }
+ }
+};
+
+const handler = NextAuth(authOptions);
+
+export {handler as GET, handler as POST};
diff --git a/src/app/[locale]/api/cron/workTranslate/route.ts b/src/app/[locale]/api/cron/workTranslate/route.ts
new file mode 100644
index 0000000..9763cc9
--- /dev/null
+++ b/src/app/[locale]/api/cron/workTranslate/route.ts
@@ -0,0 +1,78 @@
+import {getDb} from "~/libs/db";
+import {locales} from "~/config";
+import {translateContent} from "~/servers/translate";
+
+export const maxDuration = 300;
+
+export async function GET() {
+ const db = getDb();
+ const timeFlag = 'workTranslate' + '-=->' + new Date().getTime();
+ console.time(timeFlag);
+ const results = await db.query('select * from works_translate_task where status=$1 order by created_at asc limit 3', [0]);
+ const rows = results.rows;
+ if (rows.length <= 0) {
+ console.log('没有任务需要处理');
+ console.timeEnd(timeFlag);
+ return Response.json({message: '没有任务需要处理'});
+ }
+ for (let i = 0; i < rows.length; i++) {
+ const oneTask = rows[i];
+ // 翻译为除了原始语言的其他语言,如果原始语言不在支持列表,就翻译为支持的十种语言
+ const origin_language = oneTask.origin_language;
+ const uid = oneTask.uid;
+
+ const needLanguage = getNeedTranslateLanguage(origin_language);
+
+ if (needLanguage.length <= 0) {
+ // 更新状态为已翻译完成
+ await db.query('update works_translate_task set status=$1 where uid=$2', [1, uid]);
+ console.log('没有需要翻译的语言-=->', uid);
+ console.timeEnd(timeFlag);
+ return Response.json({message: '没有需要翻译的语言'});
+ }
+
+ // 查出原始数据
+ const resultsOrigin = await db.query('select * from works where uid=$1 and is_origin=$2 and is_delete=$3', [uid, true, false]);
+ const rowsOrigin = resultsOrigin.rows;
+ if (rowsOrigin.length <= 0) {
+ console.log('没有原始数据-=->', uid);
+ console.timeEnd(timeFlag);
+ // 更新状态为已翻译完成
+ await db.query('update works_translate_task set status=$1 where uid=$2', [1, uid]);
+ return Response.json({message: '没有原始数据'});
+ }
+ const originData = rowsOrigin[0];
+ if (originData.output_url == '') {
+ // 如果原始数据的url还没生成,就不翻译了
+ console.log('原始数据的url还没生成-=->', uid);
+ console.timeEnd(timeFlag);
+ return Response.json({message: '原始数据的url还没生成'});
+ }
+ const timeFlagCurrent = '翻译' + '-=->' + uid;
+ console.time(timeFlagCurrent);
+ for (let i = 0; i < needLanguage.length; i++) {
+ const toLanguage = needLanguage[i];
+ const translateText = await translateContent(originData.input_text, toLanguage);
+ const sqlStr = 'insert into works(uid, input_text, output_url, is_public, status, user_id, revised_text, is_origin, origin_language, current_language) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)';
+ const data = [uid, translateText, originData.output_url, originData.is_public, originData.status, originData.user_id, originData.revised_text, false, originData.origin_language, toLanguage];
+ await db.query(sqlStr, data);
+ }
+ console.timeEnd(timeFlagCurrent);
+ console.log('翻译完-=->', uid);
+ // 更新状态为已翻译完成
+ await db.query('update works_translate_task set status=$1 where uid=$2', [1, uid]);
+ }
+ console.timeEnd(timeFlag);
+ return Response.json({message: '本次翻译任务翻译完'});
+}
+
+function getNeedTranslateLanguage(origin_language: string) {
+ const needTranslateLanguage = [];
+ // 判断出需要翻译的语言,并调用翻译
+ for (let i = 0; i < locales.length; i++) {
+ if (origin_language != locales[i]) {
+ needTranslateLanguage.push(locales[i]);
+ }
+ }
+ return needTranslateLanguage;
+}
diff --git a/src/app/[locale]/api/generate/callByReplicate/route.ts b/src/app/[locale]/api/generate/callByReplicate/route.ts
new file mode 100644
index 0000000..6057913
--- /dev/null
+++ b/src/app/[locale]/api/generate/callByReplicate/route.ts
@@ -0,0 +1,50 @@
+import {R2, r2Bucket, storageURL} from "~/libs/R2";
+import {getDb} from "~/libs/db";
+import {countSticker} from "~/servers/keyValue";
+import {v4 as uuidv4} from 'uuid';
+
+export const POST = async (req: Request) => {
+ const query = new URL(req.url).searchParams;
+ const uid = query.get("uid")!;
+
+ const json = await req.json();
+
+ const output = json.output;
+ console.log('callByReplicate ==>json.output==>', output);
+
+ // 使用fetch API下载文件,output是一个数组,可能多个
+ const len = output.length;
+ const output_urls = [];
+ for (let i = 0; i < len; i++) {
+ const url = output[i];
+ const currentFileContent = await fetch(url)
+ .then((v) => v.arrayBuffer())
+ .then(Buffer.from);
+
+ const currentKey = `${uuidv4()}.png`;
+ await R2.upload({
+ Bucket: r2Bucket,
+ Key: `${currentKey}`,
+ Body: currentFileContent,
+ }).promise();
+ const currentUrl = `${storageURL}/${currentKey}`;
+ output_urls.push(currentUrl);
+ }
+
+ const output_url = JSON.stringify(output_urls);
+
+ const db = getDb();
+
+ const results = await db.query('select * from works where uid=$1', [uid]);
+ const rows = results.rows;
+ if (rows.length > 0) {
+ const row = rows[0];
+ if (row.is_public) {
+ // 公开的才加
+ await countSticker('countSticker', 1);
+ }
+ await db.query('update works set output_url=$1,status=$2,updated_at=now() where uid=$3', [output_url, 1, uid]);
+ }
+
+ return Response.json({msg: 200});
+}
diff --git a/src/app/[locale]/api/generate/handle/route.ts b/src/app/[locale]/api/generate/handle/route.ts
new file mode 100644
index 0000000..27cbd60
--- /dev/null
+++ b/src/app/[locale]/api/generate/handle/route.ts
@@ -0,0 +1,85 @@
+import {getUserById} from "~/servers/user";
+import {checkUserTimes, countDownUserTimes} from "~/servers/manageUserTimes";
+import {v4 as uuidv4} from 'uuid';
+import {getReplicateClient} from "~/libs/replicateClient";
+import {getInput} from "~/libs/replicate";
+import {getDb} from "~/libs/db";
+import {getLanguage} from "~/servers/language";
+import {checkSubscribe} from "~/servers/subscribe";
+import {checkSensitiveInputText} from "~/servers/checkInput";
+
+
+export async function POST(req: Request, res: Response) {
+ let json = await req.json();
+ let textStr = json.textStr;
+ let user_id = json.user_id;
+ let is_public = json.is_public;
+
+ if (!user_id && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ return Response.json({msg: "Login to continue.", status: 601});
+ }
+
+ // 检查用户在数据库是否存在,不存在则返回需登录
+ const resultsUser = await getUserById(user_id);
+ if (resultsUser.email == '' && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ return Response.json({msg: "Login to continue.", status: 601});
+ }
+
+ const checkSubscribeStatus = await checkSubscribe(user_id);
+ // console.log('checkSubscribeStatus-=-=-', checkSubscribeStatus);
+ if (!is_public) {
+ // 判断用户是否订阅状态,否则返回错误
+ if (!checkSubscribeStatus) {
+ return Response.json({msg: "Pricing to continue.", status: 602});
+ }
+ }
+
+ if (!checkSubscribeStatus) {
+ const check = await checkUserTimes(user_id);
+ if (!check && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0') {
+ return Response.json({msg: "Pricing to continue.", status: 602});
+ }
+ }
+
+ const checkSensitive = await checkSensitiveInputText(textStr);
+ if (!checkSensitive) {
+ // 敏感词没通过,校验是否订阅
+ if (!checkSubscribeStatus) {
+ // 未订阅则返回付费再继续
+ return Response.json({msg: "Pricing to continue.", status: 602});
+ } else {
+ // 订阅强制设置其为用户私有,不公开
+ is_public = false;
+ }
+ }
+
+ const uid = uuidv4();
+
+ const replicateClient = getReplicateClient();
+ const origin_language = await getLanguage(textStr);
+ const input = await getInput(textStr, checkSubscribeStatus);
+ await replicateClient.predictions.create({
+ version: process.env.REPLICATE_API_VERSION,
+ input: input,
+ webhook: `${process.env.REPLICATE_WEBHOOK}/api/generate/callByReplicate?uid=${uid}`,
+ webhook_events_filter: ["completed"],
+ })
+
+ const db = getDb();
+ // 创建新的数据
+ await db.query('insert into works(uid, input_text, output_url, is_public, status, user_id, revised_text, is_origin, origin_language, current_language) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)',
+ [uid, textStr, '', is_public, 0, user_id, input.prompt, true, origin_language, origin_language]);
+ // 创建一条翻译任务
+ await db.query('insert into works_translate_task(uid,origin_language,status) values($1,$2,$3)', [uid, origin_language, 0]);
+
+ // 需要登录,且需要支付时,才操作该项
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' && !checkSubscribeStatus) {
+ // 减少用户次数
+ await countDownUserTimes(user_id);
+ }
+
+ const resultInfo = {
+ uid: uid
+ }
+ return Response.json(resultInfo);
+}
diff --git a/src/app/[locale]/api/stripe/create-checkout-session/route.ts b/src/app/[locale]/api/stripe/create-checkout-session/route.ts
new file mode 100644
index 0000000..6ceeacb
--- /dev/null
+++ b/src/app/[locale]/api/stripe/create-checkout-session/route.ts
@@ -0,0 +1,104 @@
+import {stripe} from '~/libs/stripe';
+import {createOrRetrieveCustomer} from '~/libs/handle-stripe';
+import {getURL} from '~/libs/helpers';
+import Stripe from 'stripe';
+import {getDb} from "~/libs/db";
+
+export async function POST(req: Request) {
+ const db = getDb();
+ if (req.method === 'POST') {
+ // 1. Destructure the price and quantity from the POST body
+ const {price, quantity = 1, metadata = {}, redirectUrl, user_id} = await req.json();
+
+ try {
+ // 2. select user
+ const userInfoRes = await db.query('select * from user_info where user_id = $1', [user_id]);
+ const userInfoRow = userInfoRes.rows;
+ if (userInfoRow.length <= 0) {
+ return new Response(JSON.stringify({message: 'Not have user'}), {
+ status: 200
+ });
+ }
+ const userInfo = userInfoRow[0];
+ const userEmail = userInfo.email;
+
+ // 3. Retrieve or create the customer in Stripe
+ const customer = await createOrRetrieveCustomer({
+ user_id: user_id,
+ email: userEmail
+ });
+
+ let redirectPath = `${getURL()}`;
+ if (redirectUrl) {
+ redirectPath = `${getURL()}${redirectUrl}`;
+ }
+
+ // 4. Create a checkout session in Stripe
+ let session: Stripe.Response;
+ if (price.type === 'recurring') {
+ // @ts-ignore
+ session = await stripe.checkout.sessions.create({
+ payment_method_types: ['card'],
+ billing_address_collection: 'auto',
+ customer,
+ customer_update: {
+ address: 'auto'
+ },
+ line_items: [
+ {
+ price: price.id,
+ quantity
+ }
+ ],
+ mode: 'subscription',
+ allow_promotion_codes: true,
+ subscription_data: {
+ trial_from_plan: true,
+ metadata
+ },
+ success_url: redirectPath,
+ cancel_url: redirectPath
+ });
+ } else if (price.type === 'one_time') {
+ session = await stripe.checkout.sessions.create({
+ payment_method_types: ['card'],
+ billing_address_collection: 'auto',
+ customer,
+ customer_update: {
+ address: 'auto'
+ },
+ line_items: [
+ {
+ price: price.id,
+ quantity
+ }
+ ],
+ mode: 'payment',
+ allow_promotion_codes: true,
+ success_url: redirectPath,
+ cancel_url: redirectPath
+ });
+ }
+
+ if (session) {
+ return new Response(JSON.stringify({sessionId: session.id}), {
+ status: 200
+ });
+ } else {
+ return new Response(
+ JSON.stringify({
+ error: {statusCode: 500, message: 'Session is not defined'}
+ }),
+ {status: 200}
+ );
+ }
+ } catch (err: any) {
+ console.log(err);
+ return new Response(JSON.stringify(err), {status: 500});
+ }
+ } else {
+ return new Response(JSON.stringify({message: 'Method Not Allowed'}), {
+ status: 200
+ });
+ }
+}
diff --git a/src/app/[locale]/api/stripe/create-portal-link/route.ts b/src/app/[locale]/api/stripe/create-portal-link/route.ts
new file mode 100644
index 0000000..66c411a
--- /dev/null
+++ b/src/app/[locale]/api/stripe/create-portal-link/route.ts
@@ -0,0 +1,34 @@
+import {getUserById} from "~/servers/user";
+import {createOrRetrieveCustomer} from "~/libs/handle-stripe";
+import {stripe} from '~/libs/stripe';
+import {getURL} from "~/libs/helpers";
+
+export async function POST(req: Request) {
+ let json = await req.json();
+ let user_id = json.user_id;
+
+ if (!user_id) {
+ return Response.json({msg: "Login to continue.", status: 601});
+ }
+
+ // 检查用户在数据库是否存在,不存在则返回需登录
+ const resultsUser = await getUserById(user_id);
+ if (resultsUser.email == '') {
+ return Response.json({msg: "Login to continue.", status: 601});
+ }
+
+ const customer = await createOrRetrieveCustomer({
+ user_id: user_id,
+ email: resultsUser.email
+ });
+ if (!customer) throw Error('Could not get customer');
+
+ const {url} = await stripe.billingPortal.sessions.create({
+ customer,
+ return_url: `${getURL()}`
+ });
+ return new Response(JSON.stringify({url: url}), {
+ status: 200
+ });
+
+}
diff --git a/src/app/[locale]/api/stripe/webhooks/route.ts b/src/app/[locale]/api/stripe/webhooks/route.ts
new file mode 100644
index 0000000..a1a4a59
--- /dev/null
+++ b/src/app/[locale]/api/stripe/webhooks/route.ts
@@ -0,0 +1,77 @@
+import Stripe from 'stripe';
+import {stripe} from '~/libs/stripe';
+import {
+ manageSubscriptionStatusChange
+} from '~/libs/handle-stripe';
+
+const relevantEvents = new Set([
+ 'checkout.session.completed',
+ 'customer.subscription.created',
+ 'customer.subscription.updated',
+ 'customer.subscription.deleted'
+]);
+
+export async function POST(req: Request) {
+ const body = await req.text();
+ const sig = req.headers.get('stripe-signature') as string;
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
+ let event: Stripe.Event;
+
+ try {
+ if (!sig || !webhookSecret) return;
+ event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
+ } catch (err: any) {
+ console.log(`❌ Error message: ${err.message}`);
+ return new Response(`Webhook Error: ${err.message}`, {status: 200});
+ }
+
+ if (!event) {
+ return new Response(`Webhook Error: event undefined`, {status: 200});
+ }
+
+ if (!event.type) {
+ return new Response(`Webhook Error: event.type undefined`, {status: 200});
+ }
+
+ if (relevantEvents.has(event.type)) {
+ try {
+ switch (event.type) {
+ case 'customer.subscription.created':
+ case 'customer.subscription.updated':
+ case 'customer.subscription.deleted':
+ const subscription = event.data.object as Stripe.Subscription;
+ await manageSubscriptionStatusChange(
+ subscription.id,
+ subscription.customer as string,
+ event.type === 'customer.subscription.created'
+ );
+ break;
+ case 'checkout.session.completed':
+ const checkoutSession = event.data.object as Stripe.Checkout.Session;
+ // console.log('checkoutSession==>', checkoutSession);
+ if (checkoutSession.mode === 'subscription') {
+ const subscriptionId = checkoutSession.subscription;
+ await manageSubscriptionStatusChange(
+ subscriptionId as string,
+ checkoutSession.customer as string,
+ true
+ );
+ } else {
+
+ }
+ break;
+ default:
+ throw new Error('Unhandled relevant event!');
+ }
+ } catch (error) {
+ console.log(error);
+ return new Response(
+ 'Webhook handler failed. View your nextjs function logs.',
+ {
+ status: 200
+ }
+ );
+ }
+ }
+ return new Response(JSON.stringify({received: true}));
+}
diff --git a/src/app/[locale]/api/user/getAvailableTimes/route.ts b/src/app/[locale]/api/user/getAvailableTimes/route.ts
new file mode 100644
index 0000000..fe30d93
--- /dev/null
+++ b/src/app/[locale]/api/user/getAvailableTimes/route.ts
@@ -0,0 +1,34 @@
+import {getDb} from "~/libs/db";
+
+export const revalidate = 0;
+export const GET = async (req: Request) => {
+ const query = new URL(req.url).searchParams;
+ const userId = query.get("userId");
+
+ const db = getDb();
+
+ const result = {
+ userId: userId,
+ available_times: 0,
+ subscribeStatus: ''
+ };
+
+ if (userId) {
+
+ const resultsSubscribe = await db.query('SELECT * FROM stripe_subscriptions where user_id=$1', [userId]);
+ const originSubscribe = resultsSubscribe.rows;
+ if (originSubscribe.length > 0) {
+ if (originSubscribe[0].status == 'active') {
+ result.subscribeStatus = originSubscribe[0].status
+ }
+ }
+
+ const results = await db.query('SELECT * FROM user_available where user_id=$1', [userId]);
+ const origin = results.rows;
+
+ if (origin.length !== 0) {
+ result.available_times = origin[0].available_times;
+ }
+ }
+ return Response.json(result);
+}
diff --git a/src/app/[locale]/api/user/getUserByEmail/route.ts b/src/app/[locale]/api/user/getUserByEmail/route.ts
new file mode 100644
index 0000000..aeead9a
--- /dev/null
+++ b/src/app/[locale]/api/user/getUserByEmail/route.ts
@@ -0,0 +1,10 @@
+import {getUserByEmail} from "~/servers/user";
+
+export async function POST(req: Request, res: Response) {
+ let json = await req.json();
+ let email = json.email;
+
+ const result = await getUserByEmail(email);
+ return Response.json(result);
+
+}
diff --git a/src/app/[locale]/api/user/getUserByUserId/route.ts b/src/app/[locale]/api/user/getUserByUserId/route.ts
new file mode 100644
index 0000000..1b253ff
--- /dev/null
+++ b/src/app/[locale]/api/user/getUserByUserId/route.ts
@@ -0,0 +1,10 @@
+import {getUserById} from "~/servers/user";
+
+export async function POST(req: Request, res: Response) {
+ let json = await req.json();
+ let user_id = json.user_id;
+
+ const result = await getUserById(user_id);
+ return Response.json(result);
+
+}
diff --git a/src/app/[locale]/api/works/getLatestPublicResultList/route.ts b/src/app/[locale]/api/works/getLatestPublicResultList/route.ts
new file mode 100644
index 0000000..59048e7
--- /dev/null
+++ b/src/app/[locale]/api/works/getLatestPublicResultList/route.ts
@@ -0,0 +1,16 @@
+import {getLatestPublicResultList} from "~/servers/works";
+
+export const revalidate = 10;
+
+export async function POST(req: Request, res: Response) {
+
+ const json = await req.json();
+ const locale = json.locale;
+
+ const works = await getLatestPublicResultList(locale, 1);
+
+ return new Response(JSON.stringify(works), {
+ headers: {"Content-Type": "application/json"},
+ status: 200
+ });
+}
diff --git a/src/app/[locale]/api/works/getResultInfo/route.ts b/src/app/[locale]/api/works/getResultInfo/route.ts
new file mode 100644
index 0000000..055f2f7
--- /dev/null
+++ b/src/app/[locale]/api/works/getResultInfo/route.ts
@@ -0,0 +1,51 @@
+import {getDb} from "~/libs/db";
+import {getArrayUrlResult} from "~/configs/buildLink";
+
+export const revalidate = 0;
+
+export const GET = async (req: Request) => {
+ const query = new URL(req.url).searchParams;
+
+ const userId = query.get("userId");
+ const uid = query.get("uid");
+
+ const result = {
+ status: 404,
+ uid: uid,
+ input_text: '',
+ output_url: [],
+ is_public: false,
+ message: 'error',
+ user_id: userId,
+ revised_text: ''
+ }
+
+ if ((!userId || userId === 'undefined') && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ return Response.json(result);
+ }
+
+ const db = getDb();
+
+ let results;
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ results = await db.query('select * from works where uid=$1 and user_id=$2 and is_origin=$3 and is_delete=$4', [uid, userId, true, false]);
+ } else {
+ results = await db.query('select * from works where uid=$1 and is_origin=$2 and is_delete=$3', [uid, true, false]);
+ }
+
+ const resultData = results.rows;
+ if (resultData.length <= 0) {
+ return Response.json(result);
+ }
+
+ const data = resultData[0];
+ result.status = data.status;
+ result.input_text = data.input_text;
+ result.output_url = getArrayUrlResult(data.output_url);
+ result.is_public = data.is_public;
+ result.user_id = data.user_id;
+ result.uid = data.uid;
+ result.revised_text = data.revised_text;
+
+ return Response.json(result);
+}
diff --git a/src/app/[locale]/api/works/getWorkList/route.ts b/src/app/[locale]/api/works/getWorkList/route.ts
new file mode 100644
index 0000000..d1ed1b7
--- /dev/null
+++ b/src/app/[locale]/api/works/getWorkList/route.ts
@@ -0,0 +1,17 @@
+import {getWorkListByUserId} from "~/servers/works";
+
+export const revalidate = 0;
+
+export async function POST(req: Request, res: Response) {
+
+ const json = await req.json();
+ const user_id = json.user_id;
+ const current_page = json.current_page;
+
+ const works = await getWorkListByUserId(user_id, current_page);
+
+ return new Response(JSON.stringify(works), {
+ headers: {"Content-Type": "application/json"},
+ status: 200
+ });
+}
diff --git a/src/app/[locale]/api/works/updateWork/route.ts b/src/app/[locale]/api/works/updateWork/route.ts
new file mode 100644
index 0000000..48ba6fe
--- /dev/null
+++ b/src/app/[locale]/api/works/updateWork/route.ts
@@ -0,0 +1,19 @@
+import {getDb} from "~/libs/db";
+
+export const revalidate = 0;
+
+export async function POST(req: Request, res: Response) {
+ const db = getDb();
+
+ const json = await req.json();
+ const uid = json.uid;
+
+ // 更新数据为删除状态
+ await db.query('update works set is_delete=$1,updated_at=now() where uid=$2', [true, uid]);
+
+ return new Response(JSON.stringify({message: 'success'}), {
+ headers: {"Content-Type": "application/json"},
+ status: 200
+ });
+
+}
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
new file mode 100644
index 0000000..230db3e
--- /dev/null
+++ b/src/app/[locale]/layout.tsx
@@ -0,0 +1,57 @@
+import clsx from 'clsx';
+import {Inter} from 'next/font/google';
+import {notFound} from 'next/navigation';
+import {unstable_setRequestLocale} from 'next-intl/server';
+import {ReactNode} from 'react';
+import {locales} from '~/config';
+import {CommonProvider} from '~/context/common-context';
+import {NextAuthProvider} from '~/context/next-auth-context';
+import {getAuthText, getCommonText, getMenuText, getPricingText} from "~/configs/languageText";
+
+const inter = Inter({subsets: ['latin']});
+
+type Props = {
+ children: ReactNode;
+ params: { locale: string };
+};
+
+export function generateStaticParams() {
+ return locales.map((locale) => ({locale}));
+}
+
+export default async function LocaleLayout({
+ children,
+ params: {locale}
+ }: Props) {
+
+ // Validate that the incoming `locale` parameter is valid
+ if (!locales.includes(locale as any)) notFound();
+
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const commonText = await getCommonText();
+ const authText = await getAuthText();
+ const menuText = await getMenuText();
+ const pricingText = await getPricingText();
+
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/my/PageComponent.tsx b/src/app/[locale]/my/PageComponent.tsx
new file mode 100644
index 0000000..bf65f6e
--- /dev/null
+++ b/src/app/[locale]/my/PageComponent.tsx
@@ -0,0 +1,164 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import {useEffect, useRef, useState} from "react";
+import {useCommonContext} from "~/context/common-context";
+import {useInterval} from "ahooks";
+import Link from "next/link";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import TopBlurred from "~/components/TopBlurred";
+import {pinterestSvg} from "~/components/svg";
+
+
+const PageComponent = ({
+ locale,
+ worksText
+ }) => {
+ const [pagePath] = useState('my');
+
+ const [resultInfoList, setResultInfoList] = useState([]);
+ const {
+ setShowLoadingModal,
+ userData,
+ commonText,
+ } = useCommonContext();
+ const [intervalWorkList, setIntervalWorkList] = useState(1000);
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(true);
+ return () => {
+ }
+ }, []);
+
+ const getCurrentWorkList = async (newPage) => {
+ if (!userData) {
+
+ } else {
+ setCurrentPage(newPage);
+ setIntervalWorkList(undefined);
+ const requestData = {
+ user_id: userData.user_id,
+ current_page: newPage
+ }
+ // setShowLoadingModal(true);
+ const response = await fetch(`/api/works/getWorkList`, {
+ method: 'POST',
+ body: JSON.stringify(requestData)
+ });
+ const result = await response.json();
+ setShowLoadingModal(false);
+ if (result.length == 0) {
+ setAlreadyLoadAll(true);
+ } else {
+ setResultInfoList([...resultInfoList, ...result]);
+ }
+ // setResultInfoList(result);
+ }
+ }
+ useInterval(() => {
+ getCurrentWorkList(currentPage)
+ }, intervalWorkList);
+
+ const [currentPage, setCurrentPage] = useState(1);
+ const [alreadyLoadAll, setAlreadyLoadAll] = useState(false);
+
+ const myRef = useRef(null);
+ const handleScroll = async () => {
+ const {scrollHeight, clientHeight, scrollTop} = myRef.current;
+ if (scrollHeight - clientHeight <= scrollTop + 1) {
+ console.log('Reached bottom');
+ if (!alreadyLoadAll) {
+ await getCurrentWorkList(currentPage + 1);
+ }
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
{worksText.h1Text}
+
+
+ setShowLoadingModal(true)}
+ className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
+ {commonText.generateNew} {'>>'}
+
+
+
+
+ {resultInfoList?.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/my/page.tsx b/src/app/[locale]/my/page.tsx
new file mode 100644
index 0000000..33eb58b
--- /dev/null
+++ b/src/app/[locale]/my/page.tsx
@@ -0,0 +1,22 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getWorksText
+} from "~/configs/languageText";
+
+export default async function IndexPage({params: {locale = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const worksText = await getWorksText();
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
new file mode 100644
index 0000000..2de2f4d
--- /dev/null
+++ b/src/app/[locale]/page.tsx
@@ -0,0 +1,31 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getIndexPageText,
+ getQuestionText
+} from "~/configs/languageText";
+import {getLatestPublicResultList} from "~/servers/works";
+
+export const revalidate = 120;
+export default async function IndexPage({params: {locale = ''}, searchParams: searchParams}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const indexText = await getIndexPageText();
+ const questionText = await getQuestionText();
+
+ const resultInfoListInit = await getLatestPublicResultList(locale, 1);
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/pricing/PageComponent.tsx b/src/app/[locale]/pricing/PageComponent.tsx
new file mode 100644
index 0000000..f07db59
--- /dev/null
+++ b/src/app/[locale]/pricing/PageComponent.tsx
@@ -0,0 +1,65 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Pricing from "~/components/PricingComponent";
+import {useEffect, useRef, useState} from "react";
+import TopBlurred from "~/components/TopBlurred";
+import {useCommonContext} from "~/context/common-context";
+
+
+const PageComponent = ({
+ locale,
+ }) => {
+ const [pagePath] = useState('pricing');
+
+ const {
+ setShowLoadingModal,
+ pricingText
+ } = useCommonContext();
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ return () => {
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/pricing/page.tsx b/src/app/[locale]/pricing/page.tsx
new file mode 100644
index 0000000..1225b48
--- /dev/null
+++ b/src/app/[locale]/pricing/page.tsx
@@ -0,0 +1,15 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+export default async function IndexPage({params: {locale = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/privacy-policy/PageComponent.tsx b/src/app/[locale]/privacy-policy/PageComponent.tsx
new file mode 100644
index 0000000..699e56c
--- /dev/null
+++ b/src/app/[locale]/privacy-policy/PageComponent.tsx
@@ -0,0 +1,66 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Markdown from "react-markdown";
+import TopBlurred from "~/components/TopBlurred";
+import {useEffect, useRef, useState} from "react";
+import {useCommonContext} from "~/context/common-context";
+
+const PageComponent = ({
+ locale,
+ privacyPolicyText,
+ }) => {
+
+ const [pagePath] = useState("privacy-policy");
+ const {setShowLoadingModal} = useCommonContext();
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ return () => {
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {privacyPolicyText.detailText}
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/privacy-policy/page.tsx b/src/app/[locale]/privacy-policy/page.tsx
new file mode 100644
index 0000000..9b93483
--- /dev/null
+++ b/src/app/[locale]/privacy-policy/page.tsx
@@ -0,0 +1,23 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getPrivacyPolicyText
+} from "~/configs/languageText";
+
+export default async function IndexPage({params: {locale = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const privacyPolicyText = await getPrivacyPolicyText();
+
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/search/PageComponent.tsx b/src/app/[locale]/search/PageComponent.tsx
new file mode 100644
index 0000000..87a5130
--- /dev/null
+++ b/src/app/[locale]/search/PageComponent.tsx
@@ -0,0 +1,187 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import TopBlurred from "~/components/TopBlurred";
+import {useEffect, useRef, useState} from "react";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink";
+import Link from "next/link";
+import {useCommonContext} from "~/context/common-context";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import {useRouter} from "next/navigation";
+import {useInterval} from "ahooks";
+import {pinterestSvg} from "~/components/svg";
+
+const PageComponent = ({
+ locale,
+ searchText,
+ resultInfoListInit,
+ sticker,
+ }) => {
+ const router = useRouter();
+ const [pagePath] = useState(sticker ? ('search?sticker=' + encodeURIComponent(sticker)) : 'search');
+ const [resultInfoList, setResultInfoList] = useState(resultInfoListInit);
+ const {
+ setShowLoadingModal,
+ userData,
+ commonText
+ } = useCommonContext();
+ const [intervalCheckUser, setIntervalCheckUser] = useState(undefined);
+
+ const [textStr, setTextStr] = useState(sticker);
+ const handleSubmit = async (e: { preventDefault: () => void }) => {
+ e.preventDefault();
+ if (!textStr) {
+ window.location.reload();
+ return;
+ }
+ setShowLoadingModal(true);
+ const pageNew = `search?sticker=${textStr}`;
+ const resultStr = getLinkHref(locale, pageNew);
+ window.location.href = process.env.NEXT_PUBLIC_SITE_URL + '/' + resultStr;
+ setShowLoadingModal(false);
+ }
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ setIntervalCheckUser(500)
+ return () => {
+ }
+ }, []);
+
+ const updateWork = async (uid) => {
+ setShowLoadingModal(true);
+ const response = await fetch(`/api/works/updateWork`, {
+ method: 'POST',
+ body: JSON.stringify({uid: uid})
+ });
+ const result = await response.json();
+ console.log('result->', result);
+ setShowLoadingModal(false);
+ window.location.reload();
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {
+ resultInfoList.length <= 0 ?
+
+
+ {commonText.generateNew} {'>>'}
+
+
+ :
+ null
+ }
+
+ {
+ resultInfoList.length > 0 ?
+ <>
+
+
+ {resultInfoList.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+ setShowLoadingModal(true)}
+ className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
+ {commonText.exploreMore} {'>>'}
+
+
+ >
+ :
+ null
+ }
+
+
+
+ >
+ )
+
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/search/page.tsx b/src/app/[locale]/search/page.tsx
new file mode 100644
index 0000000..a6e7a4a
--- /dev/null
+++ b/src/app/[locale]/search/page.tsx
@@ -0,0 +1,36 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+import {getSearchText} from "~/configs/languageText";
+import {getLatestPublicResultList} from "~/servers/works";
+import {getCountSticker} from "~/servers/keyValue";
+import {searchByWords, addSearchLog} from "~/servers/search";
+
+export const revalidate = 0;
+
+export default async function SearchPage({params: {locale = ''}, searchParams: {sticker = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+
+ const countSticker = await getCountSticker();
+
+ let resultInfoListInit = [];
+ let searchText;
+ if (sticker) {
+ resultInfoListInit = await searchByWords(locale, sticker);
+ searchText = await getSearchText(resultInfoListInit.length, sticker, countSticker);
+ addSearchLog(sticker, resultInfoListInit.length);
+ } else {
+ resultInfoListInit = await getLatestPublicResultList(locale, 1);
+ searchText = await getSearchText(countSticker, sticker, countSticker);
+ }
+
+ return (
+
+ )
+}
diff --git a/src/app/[locale]/sticker/[uid]/PageComponent.tsx b/src/app/[locale]/sticker/[uid]/PageComponent.tsx
new file mode 100644
index 0000000..939a19d
--- /dev/null
+++ b/src/app/[locale]/sticker/[uid]/PageComponent.tsx
@@ -0,0 +1,259 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Link from "next/link";
+import {useEffect, useRef, useState} from "react";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest, getTotalLinkHref} from "~/configs/buildLink";
+import {useCommonContext} from "~/context/common-context";
+import {useInterval} from "ahooks";
+import PricingModal from "~/components/PricingModal";
+import TopBlurred from "~/components/TopBlurred";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import {pinterestSvg} from "~/components/svg";
+
+
+const PageComponent = ({
+ locale,
+ detailText,
+ workDetail,
+ similarList,
+ }) => {
+ const [pagePath] = useState(`sticker/${workDetail.uid}`);
+
+ const {
+ setShowLoadingModal,
+ userData,
+ setShowPricingModal,
+ setShowLoginModal,
+ commonText
+ } = useCommonContext();
+ const [availableTimes, setAvailableTimes] = useState({
+ available_times: 0,
+ subscribeStatus: '0'
+ });
+ const [intervalAvailableTimes, setIntervalAvailableTimes] = useState(undefined);
+
+ const getAvailableTimes = async () => {
+ if (!userData) {
+ return;
+ }
+ const userId = userData.user_id;
+ if (userId) {
+ const response = await fetch(`/api/user/getAvailableTimes?userId=${userId}`);
+ const availableTimes = await response.json();
+ setAvailableTimes(availableTimes);
+ if (availableTimes.available_times >= 0) {
+ setIntervalAvailableTimes(undefined);
+ }
+ }
+ }
+ useInterval(() => {
+ getAvailableTimes();
+ }, intervalAvailableTimes);
+
+ const downloadResult = (url, index) => {
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') {
+ if (!userData) {
+ setShowLoginModal(true);
+ return;
+ }
+ if (index == 0) {
+ window.location.href = url;
+ } else {
+ if (process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0') {
+ if (availableTimes.subscribeStatus != 'active') {
+ setShowPricingModal(true);
+ } else {
+ window.location.href = url;
+ }
+ }
+ }
+ } else {
+ window.location.href = url;
+ }
+ }
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0') {
+ setIntervalAvailableTimes(1000);
+ }
+ return () => {
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ {/*
*/}
+
+
+
+
{detailText.h1Text}
+
+
+
+ setShowLoadingModal(true)}
+ rel={"nofollow"}
+ className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
+ {commonText.generateNew} {'>>'}
+
+
+
+
+
+
+ {
+ workDetail?.output_url?.length > 0 ?
+
+
+
+
})
+
+ {pinterestSvg}
+
+
+
+
+
+
+
+
+
+ :
+ null
+ }
+ {
+ workDetail?.output_url?.length > 1 ?
+
+
+
+
})
+
+ {pinterestSvg}
+
+
+
+
+
+
+
+
+
+ :
+ null
+ }
+
+
+
+
+
+
{detailText.h2Text}
+
+
+ {similarList?.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/sticker/[uid]/page.tsx b/src/app/[locale]/sticker/[uid]/page.tsx
new file mode 100644
index 0000000..0e3aa1e
--- /dev/null
+++ b/src/app/[locale]/sticker/[uid]/page.tsx
@@ -0,0 +1,36 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getDetailText,
+} from "~/configs/languageText";
+import {getSimilarList, getWorkDetailByUid} from "~/servers/works";
+import {notFound} from "next/navigation";
+
+// export const revalidate = 86400;
+export const dynamicParams = true
+export const dynamic = 'error';
+
+export default async function IndexPage({params: {locale = '', uid = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const workDetail = await getWorkDetailByUid(locale, uid);
+ if (workDetail.status == 404) {
+ notFound();
+ }
+ const detailText = await getDetailText(workDetail);
+
+ const similarList = await getSimilarList(workDetail.revised_text, uid, locale)
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/stickers/PageComponent.tsx b/src/app/[locale]/stickers/PageComponent.tsx
new file mode 100644
index 0000000..5125660
--- /dev/null
+++ b/src/app/[locale]/stickers/PageComponent.tsx
@@ -0,0 +1,211 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Link from "next/link";
+import {useEffect, useRef, useState} from "react";
+import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/20/solid'
+import {useCommonContext} from "~/context/common-context";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import TopBlurred from "~/components/TopBlurred";
+import {pinterestSvg} from "~/components/svg";
+
+const PageComponent = ({
+ locale,
+ exploreText,
+ resultInfoData,
+ page = 1,
+ pageData = {
+ totalPage: 0,
+ pagination: []
+ },
+ }) => {
+ const [pagePath] = useState('stickers');
+ const [resultInfoList, setResultInfoList] = useState(resultInfoData);
+ const {
+ setShowLoadingModal,
+ commonText
+ } = useCommonContext();
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ return () => {
+ }
+ }, []);
+
+ const checkShowLoading = (toPage) => {
+ if (page != toPage) {
+ setShowLoadingModal(true);
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
{exploreText.h1Text}
+
+
{exploreText.h2Text}
+
+
+
+
+ {commonText.generateNew} {'>>'}
+
+
+
+
+ {resultInfoList?.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/stickers/[page]/PageComponent.tsx b/src/app/[locale]/stickers/[page]/PageComponent.tsx
new file mode 100644
index 0000000..90983fb
--- /dev/null
+++ b/src/app/[locale]/stickers/[page]/PageComponent.tsx
@@ -0,0 +1,213 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Link from "next/link";
+import {useEffect, useRef, useState} from "react";
+import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/20/solid'
+import {useCommonContext} from "~/context/common-context";
+import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink";
+import {getResultStrAddSticker} from "~/configs/buildStr";
+import TopBlurred from "~/components/TopBlurred";
+import {pinterestSvg} from "~/components/svg";
+
+const PageComponent = ({
+ locale,
+ exploreText,
+ resultInfoData,
+ page = 1,
+ pageData = {
+ totalPage: 0,
+ pagination: []
+ },
+ }) => {
+ const [pagePath] = useState(`stickers/${page}`);
+
+ const [resultInfoList, setResultInfoList] = useState(resultInfoData);
+ const {
+ setShowLoadingModal,
+ commonText
+ } = useCommonContext();
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ return () => {
+ }
+ }, []);
+
+
+ const checkShowLoading = (toPage) => {
+ if (page != toPage) {
+ setShowLoadingModal(true);
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
{exploreText.h1Text}
+
+
{exploreText.h2Text}
+
+
+
+
+ {commonText.generateNew} {'>>'}
+
+
+
+
+ {resultInfoList?.map((file, index) => (
+
+
+
setShowLoadingModal(true)}
+ className={"cursor-pointer"}
+ >
+
})
+
+
+ {pinterestSvg}
+
+
+
+
{getResultStrAddSticker(file.input_text, commonText.keyword)}
+
+
+ ))}
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/stickers/[page]/page.tsx b/src/app/[locale]/stickers/[page]/page.tsx
new file mode 100644
index 0000000..081f961
--- /dev/null
+++ b/src/app/[locale]/stickers/[page]/page.tsx
@@ -0,0 +1,43 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getExploreText,
+} from "~/configs/languageText";
+import {getPagination, getPublicResultList} from "~/servers/works";
+import {notFound} from "next/navigation";
+import {getCountSticker} from "~/servers/keyValue";
+
+export const revalidate = 300;
+export const dynamic = "force-static";
+
+export default async function IndexPage({params: {locale = '', page = 2}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const countSticker = await getCountSticker();
+
+ if (page == 0) {
+ page = 1;
+ }
+ const exploreText = await getExploreText(countSticker, page);
+
+ const resultInfoData = await getPublicResultList(locale, page);
+ const pageData = await getPagination(locale, page);
+
+ if (page > pageData.totalPage) {
+ notFound();
+ }
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/stickers/page.tsx b/src/app/[locale]/stickers/page.tsx
new file mode 100644
index 0000000..149e4fc
--- /dev/null
+++ b/src/app/[locale]/stickers/page.tsx
@@ -0,0 +1,35 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getExploreText,
+} from "~/configs/languageText";
+import {getPagination, getPublicResultList} from "~/servers/works";
+import {getCountSticker} from "~/servers/keyValue";
+
+export const revalidate = 300;
+
+export default async function IndexPage({params: {locale = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const countSticker = await getCountSticker();
+
+ const exploreText = await getExploreText(countSticker, 1);
+
+ const resultInfoData = await getPublicResultList(locale, 1);
+ const pageData = await getPagination(locale, 1);
+
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/[locale]/terms-of-service/PageComponent.tsx b/src/app/[locale]/terms-of-service/PageComponent.tsx
new file mode 100644
index 0000000..bda1db1
--- /dev/null
+++ b/src/app/[locale]/terms-of-service/PageComponent.tsx
@@ -0,0 +1,66 @@
+'use client'
+import HeadInfo from "~/components/HeadInfo";
+import Header from "~/components/Header";
+import Footer from "~/components/Footer";
+import Markdown from "react-markdown";
+import TopBlurred from "~/components/TopBlurred";
+import {useEffect, useRef, useState} from "react";
+import {useCommonContext} from "~/context/common-context";
+
+
+const PageComponent = ({
+ locale,
+ termsOfServiceText,
+ }) => {
+ const [pagePath] = useState("terms-of-service");
+ const {setShowLoadingModal} = useCommonContext();
+
+ const useCustomEffect = (effect, deps) => {
+ const isInitialMount = useRef(true);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'production' || isInitialMount.current) {
+ isInitialMount.current = false;
+ return effect();
+ }
+ }, deps);
+ };
+
+ useCustomEffect(() => {
+ setShowLoadingModal(false);
+ return () => {
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {termsOfServiceText.detailText}
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PageComponent
diff --git a/src/app/[locale]/terms-of-service/page.tsx b/src/app/[locale]/terms-of-service/page.tsx
new file mode 100644
index 0000000..bd733a6
--- /dev/null
+++ b/src/app/[locale]/terms-of-service/page.tsx
@@ -0,0 +1,23 @@
+import PageComponent from "./PageComponent";
+import {unstable_setRequestLocale} from 'next-intl/server';
+
+import {
+ getTermsOfServiceText
+} from "~/configs/languageText";
+
+export default async function IndexPage({params: {locale = ''}}) {
+ // Enable static rendering
+ unstable_setRequestLocale(locale);
+
+ const termsOfServiceText = await getTermsOfServiceText();
+
+
+ return (
+
+ )
+
+
+}
diff --git a/src/app/error.tsx b/src/app/error.tsx
new file mode 100755
index 0000000..cbad08c
--- /dev/null
+++ b/src/app/error.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import {useEffect} from 'react';
+
+type Props = {
+ error: Error;
+ reset(): void;
+};
+
+export default function Error({error, reset}: Props) {
+
+ useEffect(() => {
+ console.error(error);
+ }, [error]);
+
+ return (
+ not found
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..2ab5551
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,63 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* styles.css */
+.div-markdown-color h1,
+.div-markdown-color h2,
+.div-markdown-color h3,
+.div-markdown-color h4,
+.div-markdown-color h5,
+.div-markdown-color h6 {
+ @apply text-gray-50;
+}
+
+.div-markdown-color a {
+ @apply text-gray-200;
+}
+
+.div-markdown-color strong {
+ @apply text-gray-50;
+}
+
+.custom-textarea:focus {
+ border: none !important;
+ outline: none !important;
+}
+
+
+.background-div {
+ background-image: linear-gradient(to left top, #14171f, #151829, #181831, #201739, #2b133e);
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.background-header {
+ background-image: linear-gradient(to left top, #14171f, #151829, #181831, #201739, #2b133e);
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.background-footer {
+ background-image: linear-gradient(to left top, #14171f, #151829, #181831, #201739, #2b133e);
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.line-clamp-2 {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
+/* 定义一个棋盘格背景的样式 */
+.checkerboard {
+ /* 创建两个线性渐变,45度角,模拟棋盘格效果 */
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0),
+ linear-gradient(45deg, #f0f0f0 25%, #ffffff 25%, #ffffff 75%, #f0f0f0 75%, #f0f0f0);
+ /* 定义每个方格的大小为20px */
+ background-size: 20px 20px;
+ /* 偏移其中一个渐变,以产生交错效果 */
+ background-position: 0 0, 10px 10px;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..e68a5a0
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,12 @@
+import {ReactNode} from 'react';
+import './globals.css';
+
+type Props = {
+ children: ReactNode;
+};
+
+// Since we have a `not-found.tsx` page on the root, a layout file
+// is required, even if it's just passing children through.
+export default function RootLayout({children}: Props) {
+ return children;
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100755
index 0000000..c5da17b
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+// Render the default Next.js 404 page when a route
+// is requested that doesn't match the middleware and
+// therefore doesn't have a locale associated with it.
+
+export default function NotFound() {
+ return (
+
+
+
+
+
404
+
Page not found
+
Sorry, we couldn’t find the page you’re looking for.
+
+
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..d5d37cc
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,6 @@
+import {redirect} from 'next/navigation';
+
+// This page only renders when the app is built statically (output: 'export')
+export default function RootPage() {
+ redirect('/en');
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..537dc26
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,160 @@
+import Link from "next/link";
+import {getLinkHref} from "~/configs/buildLink";
+import {useCommonContext} from "~/context/common-context";
+import {GoogleAnalytics} from "@next/third-parties/google";
+
+export default function Footer({
+ locale,
+ page,
+ }) {
+ const {
+ userData,
+ setShowLoadingModal,
+ commonText,
+ menuText,
+ } = useCommonContext();
+
+ const manageSubscribe = async () => {
+ if (!userData?.user_id) {
+ return
+ }
+ const user_id = userData?.user_id;
+ const requestData = {
+ user_id: user_id
+ }
+ setShowLoadingModal(true);
+ const responseData = await fetch(`/api/stripe/create-portal-link`, {
+ method: 'POST',
+ body: JSON.stringify(requestData)
+ });
+ const result = await responseData.json();
+ setShowLoadingModal(false);
+ if (result.url) {
+ window.location.href = result.url;
+ }
+ }
+
+ const checkPageAndLoading = (toPage) => {
+ if (page != toPage) {
+ setShowLoadingModal(true);
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/GeneratingModal.tsx b/src/components/GeneratingModal.tsx
new file mode 100644
index 0000000..7a11f75
--- /dev/null
+++ b/src/components/GeneratingModal.tsx
@@ -0,0 +1,61 @@
+import {Fragment, useRef, useState} from 'react'
+import {Dialog, Transition} from '@headlessui/react'
+import {useCommonContext} from "~/context/common-context";
+
+export default function GeneratingModal({
+ generatingText,
+ }) {
+
+ const cancelButtonRef = useRef(null);
+ const {showGeneratingModal, setShowGeneratingModal} = useCommonContext();
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/HeadInfo.tsx b/src/components/HeadInfo.tsx
new file mode 100644
index 0000000..c4e62a3
--- /dev/null
+++ b/src/components/HeadInfo.tsx
@@ -0,0 +1,60 @@
+import {languages} from "~/config";
+
+const HeadInfo = ({
+ locale,
+ page,
+ title,
+ description,
+ }) => {
+ return (
+ <>
+ {title}
+
+ {
+ languages.map((item) => {
+ const currentPage = page;
+ let hrefLang = item.code;
+ if (item.lang == 'en') {
+ hrefLang = 'x-default';
+ }
+ let href: string;
+ if (currentPage) {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${item.lang}/${currentPage}`;
+ if (item.lang == 'en') {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${currentPage}`;
+ }
+ } else {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${item.lang}`;
+ if (item.lang == 'en') {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/`;
+ }
+ }
+ return
+ })
+ }
+ {
+ languages.map((item) => {
+ const currentPage = page;
+ let hrefLang = item.code;
+ let href: string;
+ if (currentPage) {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${item.lang}/${currentPage}`;
+ if (item.lang == 'en') {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${currentPage}`;
+ }
+ } else {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/${item.lang}`;
+ if (item.lang == 'en') {
+ href = `${process.env.NEXT_PUBLIC_SITE_URL}/`;
+ }
+ }
+ if (locale == item.lang) {
+ return
+ }
+ })
+ }
+ >
+ )
+}
+
+export default HeadInfo
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 0000000..913d64b
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,291 @@
+'use client'
+import {useState} from 'react'
+import {Dialog} from '@headlessui/react'
+import {Bars3Icon, XMarkIcon} from '@heroicons/react/24/outline'
+import {GlobeAltIcon} from '@heroicons/react/24/outline'
+import {Fragment} from 'react'
+import {Menu, Transition} from '@headlessui/react'
+import {ChevronDownIcon} from '@heroicons/react/20/solid'
+import Link from "next/link";
+import {languages} from "~/config";
+import {useCommonContext} from '~/context/common-context'
+import LoadingModal from "./LoadingModal";
+import GeneratingModal from "~/components/GeneratingModal";
+import LoginButton from './LoginButton';
+import LoginModal from './LoginModal';
+import LogoutModal from "./LogoutModal";
+import {getLinkHref} from "~/configs/buildLink";
+
+export default function Header({
+ locale,
+ page
+ }) {
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
+ const {
+ setShowLoadingModal,
+ userData,
+ commonText,
+ authText,
+ menuText
+ } = useCommonContext();
+
+ const [pageResult] = useState(getLinkHref(locale, page))
+
+ const checkLocalAndLoading = (lang) => {
+ setMobileMenuOpen(false);
+ if (locale != lang) {
+ setShowLoadingModal(true);
+ }
+ }
+
+ const checkPageAndLoading = (toPage) => {
+ setMobileMenuOpen(false);
+ if (page != toPage) {
+ setShowLoadingModal(true);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/LoadingDots/LoadingDots.module.css b/src/components/LoadingDots/LoadingDots.module.css
new file mode 100644
index 0000000..aa36bed
--- /dev/null
+++ b/src/components/LoadingDots/LoadingDots.module.css
@@ -0,0 +1,32 @@
+.root {
+ @apply inline-flex text-center items-center leading-7;
+}
+
+.root span {
+ @apply bg-zinc-200 rounded-full h-2 w-2;
+ animation-name: blink;
+ animation-duration: 1.4s;
+ animation-iteration-count: infinite;
+ animation-fill-mode: both;
+ margin: 0 2px;
+}
+
+.root span:nth-of-type(2) {
+ animation-delay: 0.2s;
+}
+
+.root span:nth-of-type(3) {
+ animation-delay: 0.4s;
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 0.2;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.2;
+ }
+}
diff --git a/src/components/LoadingDots/LoadingDots.tsx b/src/components/LoadingDots/LoadingDots.tsx
new file mode 100644
index 0000000..505025b
--- /dev/null
+++ b/src/components/LoadingDots/LoadingDots.tsx
@@ -0,0 +1,13 @@
+import s from './LoadingDots.module.css';
+
+const LoadingDots = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default LoadingDots;
diff --git a/src/components/LoadingDots/index.ts b/src/components/LoadingDots/index.ts
new file mode 100644
index 0000000..a8744cf
--- /dev/null
+++ b/src/components/LoadingDots/index.ts
@@ -0,0 +1 @@
+export { default } from './LoadingDots';
diff --git a/src/components/LoadingModal.tsx b/src/components/LoadingModal.tsx
new file mode 100644
index 0000000..99c9560
--- /dev/null
+++ b/src/components/LoadingModal.tsx
@@ -0,0 +1,61 @@
+import {Fragment, useRef} from 'react'
+import {Dialog, Transition} from '@headlessui/react'
+import {useCommonContext} from "~/context/common-context";
+
+export default function LoadingModal({
+ loadingText,
+ }) {
+
+ const cancelButtonRef = useRef(null);
+ const {showLoadingModal, setShowLoadingModal} = useCommonContext();
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/LoginButton.tsx b/src/components/LoginButton.tsx
new file mode 100644
index 0000000..dc792eb
--- /dev/null
+++ b/src/components/LoginButton.tsx
@@ -0,0 +1,94 @@
+'use client'
+import React, {useState} from 'react'
+import {useRouter} from 'next/navigation'
+import {whiteLoadingSvg} from './svg';
+import {useCommonContext} from '~/context/common-context';
+import {useSession} from "next-auth/react";
+
+const LoginButton = ({
+ buttonType = 0,
+ loginText = 'Log in'
+ }) => {
+
+ const router = useRouter();
+ const {data: session, status} = useSession();
+
+ const {
+ userData,
+ setUserData,
+ setShowLoginModal,
+ setShowLogoutModal
+ } = useCommonContext()
+ const [loading, setLoading] = useState(false)
+
+ async function login(event) {
+ event.preventDefault();
+ setLoading(true)
+ let _userData;
+ if (userData == null || Object.keys(userData).length == 0) {
+ if (status == 'authenticated') {
+ setUserData(session?.user)
+ _userData = session?.user
+ }
+ } else {
+ _userData = userData
+ }
+
+ if (_userData != null && Object.keys(_userData).length != 0) {
+ router.refresh();
+ } else {
+ setShowLoginModal(true)
+ setLoading(false)
+ }
+ }
+
+ async function logout() {
+ setShowLogoutModal(true);
+ }
+
+ return (
+ <>
+ {
+ buttonType == 0 && (
+ <>
+ {
+ loading ? (
+
+ ) :
+ (
+
+ )
+ }
+ >
+ )
+ }
+ {
+ buttonType == 1 && (
+ <>
+ {
+
+ }
+ >
+ )
+ }
+ >
+ )
+}
+
+export default LoginButton
diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx
new file mode 100644
index 0000000..524d01e
--- /dev/null
+++ b/src/components/LoginModal.tsx
@@ -0,0 +1,113 @@
+'use client'
+import React from 'react'
+import {Fragment, useState} from 'react'
+import {Dialog, Transition} from '@headlessui/react'
+import {FcGoogle} from 'react-icons/fc'
+import {blackLoadingSvg} from './svg'
+import {useCommonContext} from "~/context/common-context";
+import {signInUseAuth} from "~/libs/nextAuthClient";
+import Image from "next/image";
+
+const style = {
+ loginGoogleBtn: 'inline-flex w-full justify-center items-center space-x-3 rounded-md px-3 py-2 text-sm font-semibold shadow-sm hover:border-indigo-400 border-2 border-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
+}
+
+const LoginModal = ({
+ loadingText,
+ redirectPath,
+ loginModalDesc,
+ loginModalButtonText,
+ }) => {
+
+ const [loadGoogle, setLoadGoogle] = useState(false)
+ const {showLoginModal, setShowLoginModal} = useCommonContext();
+
+
+ return (
+
+
+
+ )
+}
+
+export default LoginModal
diff --git a/src/components/LogoutModal.tsx b/src/components/LogoutModal.tsx
new file mode 100644
index 0000000..81d6595
--- /dev/null
+++ b/src/components/LogoutModal.tsx
@@ -0,0 +1,80 @@
+import {Fragment, useRef} from 'react'
+import {Dialog, Transition} from '@headlessui/react'
+import {useCommonContext} from "~/context/common-context";
+import {signOut} from "next-auth/react";
+
+export default function LogoutModal({
+ logoutModalDesc,
+ confirmButtonText,
+ cancelButtonText,
+ redirectPath
+ }) {
+
+ const cancelButtonRef = useRef(null);
+ const {showLogoutModal, setShowLogoutModal} = useCommonContext();
+
+ const confirmButton = () => {
+ sessionStorage.removeItem("user_id");
+ signOut({callbackUrl: redirectPath}).then(r => console.log(r))
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/OneTapComponent.tsx b/src/components/OneTapComponent.tsx
new file mode 100644
index 0000000..3b64840
--- /dev/null
+++ b/src/components/OneTapComponent.tsx
@@ -0,0 +1,12 @@
+import useOneTapSignin from "~/libs/useOneTapSignin";
+
+const OneTapComponent = () => {
+ const { showLoadingModal: oneTapIsLoading } = useOneTapSignin({
+ redirect: false,
+ parentContainerId: "oneTap",
+ });
+
+ return ; // This is done with tailwind. Update with system of choice
+};
+
+export default OneTapComponent;
diff --git a/src/components/PricingComponent.tsx b/src/components/PricingComponent.tsx
new file mode 100644
index 0000000..31536b0
--- /dev/null
+++ b/src/components/PricingComponent.tsx
@@ -0,0 +1,349 @@
+import {getStripe} from '~/libs/stripeClient';
+import React, {useState} from 'react';
+import {useCommonContext} from "~/context/common-context";
+import LoadingDots from "./LoadingDots";
+import {priceList} from "~/configs/stripeConfig";
+
+export default function Pricing({
+ redirectUrl,
+ isPricing = false
+ }) {
+ const [priceIdLoading, setPriceIdLoading] = useState();
+ const {
+ setShowLoginModal,
+ userData,
+ pricingText
+ } = useCommonContext();
+
+ const handleCheckout = async (price) => {
+ setPriceIdLoading(price.id);
+ if (!userData || !userData.user_id) {
+ setShowLoginModal(true);
+ return
+ }
+ const user_id = userData.user_id;
+ try {
+ const data = {
+ price,
+ redirectUrl,
+ user_id
+ }
+ const url = `/api/stripe/create-checkout-session`;
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: new Headers({'Content-Type': 'application/json'}),
+ credentials: 'same-origin',
+ body: JSON.stringify(data)
+ });
+ const res = await response.json();
+ const sessionId = res.sessionId;
+ const stripe = await getStripe();
+ stripe?.redirectToCheckout({sessionId});
+ } catch (error) {
+ return alert((error as Error)?.message);
+ } finally {
+ setPriceIdLoading(undefined);
+ }
+ }
+
+ if (!priceList?.length)
+ return (
+
+ );
+
+ return (
+
+
+
+
+
{pricingText.h1Text}
+
+
+
+
+
+
{pricingText.free0}
+
+ {pricingText.buyText}
+
+
+
+
+
+
+ {pricingText.freeIntro0}
+
+
+
+
+
+ {pricingText.freeIntro1}
+
+
+
+
+
+ {pricingText.freeIntro2}
+
+
+
+
+
+ {pricingText.subscriptionIntro4}
+
+
+
+
+ {
+ priceList?.map((price, index) => {
+ if (!price) return null;
+ const priceString = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: price.currency!,
+ minimumFractionDigits: 0
+ }).format((price?.unit_amount || 0) / 100);
+
+
+ const perDownloadPrice = (price?.unit_amount || 0) / 100 / 12;
+ const pricePerString = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: price.currency!,
+ minimumFractionDigits: 0
+ }).format(perDownloadPrice);
+
+ if (index != 0) {
+ return (
+
+
+
+
+
+
+ {pricingText.subscriptionIntro0}
+
+
+
+
+
+ {pricingText.subscriptionIntro1}
+
+
+
+
+
+ {pricingText.subscriptionIntro2}
+
+
+
+
+
+ {pricingText.subscriptionIntro4}
+
+
+
+
+ )
+ } else {
+ return (
+
+
+
+
+
+
+ {pricingText.subscriptionIntro0}
+
+
+
+
+
+ {pricingText.subscriptionIntro1}
+
+
+
+
+
+ {pricingText.subscriptionIntro2}
+
+
+
+
+
+ {pricingText.subscriptionIntro4}
+
+
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+ {/*
*/}
+ {/* {pricingText.subscriptionIntro3}*/}
+ {/*
*/}
+ {/*
*/}
+
+
+ )
+ }
+ })
+ }
+
+
+
+
+ );
+
+
+}
diff --git a/src/components/PricingModal.tsx b/src/components/PricingModal.tsx
new file mode 100644
index 0000000..3a12aec
--- /dev/null
+++ b/src/components/PricingModal.tsx
@@ -0,0 +1,53 @@
+import {Fragment, useRef, useState} from 'react'
+import {Dialog, Transition} from '@headlessui/react'
+import Pricing from "~/components/PricingComponent";
+import {useCommonContext} from "~/context/common-context";
+
+export default function PricingModal({
+ locale,
+ page
+ }) {
+
+ const [redirectUrl] = useState(`${locale}/${page}`);
+
+ const cancelButtonRef = useRef(null)
+ const {showPricingModal, setShowPricingModal} = useCommonContext();
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/TopBlurred.tsx b/src/components/TopBlurred.tsx
new file mode 100644
index 0000000..e5ec9ca
--- /dev/null
+++ b/src/components/TopBlurred.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+
+const TopBlurred = () => {
+ return (
+
+

+
+ )
+}
+
+export default TopBlurred
diff --git a/src/components/svg.tsx b/src/components/svg.tsx
new file mode 100644
index 0000000..d3c8b6e
--- /dev/null
+++ b/src/components/svg.tsx
@@ -0,0 +1,32 @@
+export const whiteLoadingSvg = (
+
+)
+
+export const blackLoadingSvg = (
+
+)
+
+export const pinterestSvg = (
+
+
+
+)
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..6778561
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,34 @@
+import {Pathnames} from 'next-intl/navigation';
+
+export const locales = ['en', 'zh'] as const;
+
+export const languages = [
+ {
+ code: "en-US",
+ lang: "en",
+ language: "English",
+ },
+ {
+ code: "zh-CN",
+ lang: "zh",
+ language: "简体中文",
+ },
+]
+
+export const pathnames = {
+ '/': '/',
+} satisfies Pathnames;
+
+// Use the default: `always`,设置为 as-needed可不显示默认路由
+export const localePrefix = 'as-needed';
+
+export type AppPathnames = keyof typeof pathnames;
+
+
+export const getLanguageByLang = (lang) => {
+ for (let i = 0; i < languages.length; i++) {
+ if (lang == languages[i].lang) {
+ return languages[i];
+ }
+ }
+}
diff --git a/src/configs/buildLink.ts b/src/configs/buildLink.ts
new file mode 100644
index 0000000..062a71c
--- /dev/null
+++ b/src/configs/buildLink.ts
@@ -0,0 +1,49 @@
+import {proc} from "vfile/do-not-use-conditional-minproc";
+
+export const getLinkHref = (locale = 'en', page = '') => {
+ if (page == '') {
+ if (locale == 'en') {
+ return '/';
+ }
+ return `/${locale}/`;
+ }
+ if (locale == 'en') {
+ return `/${page}`;
+ }
+ return `/${locale}/${page}`;
+}
+
+
+export const getCompressionImageLink = (url) => {
+ const beginUrl = process.env.NEXT_PUBLIC_STORAGE_URL + '/cdn-cgi/image/width=512,quality=85/';
+ return beginUrl + url;
+}
+
+
+export const getArrayUrlResult = (origin) => {
+ if (origin) {
+ const jsonResult = JSON.parse(origin);
+ if (jsonResult.length > 0) {
+ return jsonResult;
+ }
+ }
+ return []
+}
+
+export const getTotalLinkHref = (locale = 'en', page = '') => {
+ if (page == '') {
+ if (locale == 'en') {
+ return process.env.NEXT_PUBLIC_SITE_URL + '/';
+ }
+ return process.env.NEXT_PUBLIC_SITE_URL + `/${locale}/`;
+ }
+ if (locale == 'en') {
+ return process.env.NEXT_PUBLIC_SITE_URL + `/${page}`;
+ }
+ return process.env.NEXT_PUBLIC_SITE_URL + `/${locale}/${page}`;
+}
+
+export const getShareToPinterest = (locale = 'en', page = '', sticker:string) => {
+ const pinterestUrl = 'https://pinterest.com/pin/create/button/';
+ return pinterestUrl + `?description=${encodeURIComponent(sticker)}` + `&url=` + encodeURIComponent(getTotalLinkHref(locale, page));
+}
diff --git a/src/configs/buildStr.ts b/src/configs/buildStr.ts
new file mode 100644
index 0000000..1d70351
--- /dev/null
+++ b/src/configs/buildStr.ts
@@ -0,0 +1,10 @@
+export const getResultStrAddSticker = (input_text, keyword) => {
+ // 检查字符串是否以 keyword 结尾
+ if (input_text.endsWith(keyword)) {
+ // 如果是,直接返回原始字符串
+ return input_text;
+ } else {
+ // 如果不是,添加 keyword 后缀
+ return input_text + ' ' + keyword;
+ }
+}
diff --git a/src/configs/languageText.ts b/src/configs/languageText.ts
new file mode 100644
index 0000000..4239e21
--- /dev/null
+++ b/src/configs/languageText.ts
@@ -0,0 +1,260 @@
+import {getTranslations} from "next-intl/server";
+
+export const getIndexPageText = async () => {
+ const tIndex = await getTranslations('IndexPageText');
+ return {
+ title: tIndex('title'),
+ description: tIndex('description'),
+ h1Text: tIndex('h1Text'),
+ descriptionBelowH1Text: tIndex('descriptionBelowH1Text'),
+ }
+}
+
+export const getCommonText = async () => {
+ const tCommon = await getTranslations('CommonText');
+ return {
+ loadingText: tCommon('loadingText'),
+ generateText: tCommon('generateText'),
+ placeholderText: tCommon('placeholderText'),
+ buttonText: tCommon('buttonText'),
+ footerDescText: tCommon('footerDescText'),
+ timesLeft: tCommon('timesLeft'),
+ timesRight: tCommon('timesRight'),
+ download: tCommon('download'),
+ result: tCommon('result'),
+ moreWorks: tCommon('moreWorks'),
+ generateNew: tCommon('generateNew'),
+ displayPublic: tCommon('displayPublic'),
+ similarText: tCommon('similarText'),
+ prompt: tCommon('prompt'),
+ revised: tCommon('revised'),
+ exploreMore: tCommon('exploreMore'),
+ keyword: tCommon('keyword'),
+ searchButtonText: tCommon('searchButtonText'),
+ }
+}
+
+export const getAuthText = async () => {
+ const tAuth = await getTranslations('AuthText');
+ return {
+ loginText: tAuth('loginText'),
+ loginModalDesc: tAuth('loginModalDesc'),
+ loginModalButtonText: tAuth('loginModalButtonText'),
+ logoutModalDesc: tAuth('logoutModalDesc'),
+ confirmButtonText: tAuth('confirmButtonText'),
+ cancelButtonText: tAuth('cancelButtonText'),
+ }
+}
+
+
+export const getPricingText = async () => {
+ const tPricing = await getTranslations('PricingText');
+ const title = tPricing('title') + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+ const description = tPricing('description');
+ const h1Text = tPricing('h1Text');
+ const basic = tPricing('basic');
+ const essential = tPricing('essential');
+ const growth = tPricing('growth');
+ const buyText= tPricing('buyText');
+ const popularText = tPricing('popularText');
+ const creditsText = tPricing('creditsText');
+ const creditText = tPricing('creditText');
+ const free = tPricing('free');
+ const free0 = tPricing('free0');
+ const freeText = tPricing('freeText');
+ let freeIntro0 = tPricing('freeIntro0');
+ const freeIntro1 = tPricing('freeIntro1');
+ const freeIntro2 = tPricing('freeIntro2');
+ const subscriptionIntro0 = tPricing('subscriptionIntro0');
+ const subscriptionIntro1 = tPricing('subscriptionIntro1');
+ const subscriptionIntro2 = tPricing('subscriptionIntro2');
+ const subscriptionIntro3 = tPricing('subscriptionIntro3');
+ const subscriptionIntro4 = tPricing('subscriptionIntro4');
+ const monthText = tPricing('monthText');
+ const monthlyText = tPricing('monthlyText');
+ const annualText = tPricing('annualText');
+ const annuallyText = tPricing('annuallyText');
+ const annuallySaveText = tPricing('annuallySaveText');
+
+ // 免费生成次数
+ const freeTimes = process.env.FREE_TIMES;
+ freeIntro0 = freeIntro0.replace(/%freeTimes%/g, freeTimes);
+
+ return {
+ title: title,
+ description: description,
+ h1Text: h1Text,
+ basic: basic,
+ essential: essential,
+ growth: growth,
+ buyText: buyText,
+ popularText: popularText,
+ creditsText: creditsText,
+ creditText: creditText,
+ free: free,
+ free0: free0,
+ freeText: freeText,
+ freeIntro0: freeIntro0,
+ freeIntro1: freeIntro1,
+ freeIntro2: freeIntro2,
+ subscriptionIntro0: subscriptionIntro0,
+ subscriptionIntro1: subscriptionIntro1,
+ subscriptionIntro2: subscriptionIntro2,
+ subscriptionIntro3: subscriptionIntro3,
+ subscriptionIntro4: subscriptionIntro4,
+ monthText: monthText,
+ monthlyText: monthlyText,
+ annualText: annualText,
+ annuallyText: annuallyText,
+ annuallySaveText: annuallySaveText,
+ }
+}
+
+export const getPrivacyPolicyText = async () => {
+ const tPrivacyPolicy = await getTranslations('PrivacyPolicyText');
+ return {
+ title: tPrivacyPolicy('title') + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME,
+ description: tPrivacyPolicy('description'),
+ h1Text: tPrivacyPolicy('h1Text'),
+ detailText: tPrivacyPolicy('detailText'),
+ }
+}
+
+
+export const getTermsOfServiceText = async () => {
+ const tTermsOfService = await getTranslations('TermsOfServiceText');
+ return {
+ title: tTermsOfService('title') + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME,
+ description: tTermsOfService('description'),
+ h1Text: tTermsOfService('h1Text'),
+ detailText: tTermsOfService('detailText'),
+ }
+}
+
+export const getWorksText = async () => {
+ const tWorks = await getTranslations('WorksText');
+ return {
+ title: tWorks('title') + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME,
+ description: tWorks('description'),
+ h1Text: tWorks('h1Text'),
+ descriptionBelowH1Text: tWorks('descriptionBelowH1Text'),
+ descText: tWorks('descText'),
+ toContinue: tWorks('toContinue'),
+ }
+}
+
+export const getExploreText = async (countSticker: string, page) => {
+ const tExplore = await getTranslations('ExploreText');
+ let title = tExplore('title');
+ let description = tExplore('description');
+ let h1Text = tExplore('h1Text');
+ let descriptionBelowH1Text = tExplore('descriptionBelowH1Text');
+ let pageText = tExplore('pageText');
+ let h2Text = tExplore('h2Text');
+
+ title = title.replace(/%countSticker%/g, countSticker);
+ description = description.replace(/%countSticker%/g, countSticker);
+ pageText = pageText.replace(/%pageNumber%/g, page);
+
+ if (page != '1') {
+ title = title + ", " + pageText + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+ } else {
+ title = title + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+ }
+ h2Text = h2Text.replace(/%countSticker%/g, countSticker);
+
+ return {
+ title: title,
+ description: description,
+ h1Text: h1Text,
+ descriptionBelowH1Text: descriptionBelowH1Text,
+ h2Text: h2Text
+ }
+}
+
+export const getDetailText = async (workDetail) => {
+ let promptResult = workDetail.input_text.slice(0, 20);
+ const tDetail = await getTranslations('DetailText');
+ let title = tDetail('title');
+ let description = tDetail('description');
+ let h1Text = tDetail('h1Text');
+ let descriptionBelowH1Text = tDetail('descriptionBelowH1Text');
+ let numberText = tDetail('numberText');
+ let h2Text = tDetail('h2Text');
+
+ title = title.replace(/%prompt%/g, promptResult);
+ description = description.replace(/%prompt%/g, promptResult);
+ h1Text = h1Text.replace(/%prompt%/g, promptResult);
+ descriptionBelowH1Text = descriptionBelowH1Text.replace(/%prompt%/g, promptResult);
+
+ numberText = numberText.replace(/%detailId%/g, workDetail.id);
+ title = title + ', ' + numberText + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+
+ h2Text = h2Text.replace(/%prompt%/g, promptResult);
+
+ return {
+ title: title,
+ description: description,
+ h1Text: h1Text,
+ descriptionBelowH1Text: descriptionBelowH1Text,
+ h2Text: h2Text
+ }
+}
+
+export const getQuestionText = async () => {
+ const tQuestion = await getTranslations('QuestionText');
+ return {
+ detailText: tQuestion('detailText')
+ }
+}
+
+export const getMenuText = async () => {
+ const tMenu = await getTranslations('MenuText');
+ return {
+ header0: tMenu('header0'),
+ header1: tMenu('header1'),
+ header2: tMenu('header2'),
+ header3: tMenu('header3'),
+ footerLegal: tMenu('footerLegal'),
+ footerLegal0: tMenu('footerLegal0'),
+ footerLegal1: tMenu('footerLegal1'),
+ footerSupport: tMenu('footerSupport'),
+ footerSupport0: tMenu('footerSupport0'),
+ footerSupport1: tMenu('footerSupport1'),
+ }
+}
+
+export const getSearchText = async (countSticker, sticker: string, countStickerAll) => {
+ const tSearch = await getTranslations('SearchText');
+ let title = tSearch('title');
+ let description = tSearch('description');
+ let h1Text = tSearch('h1Text');
+ let h2Text = tSearch('h2Text');
+ let titleSearch = tSearch('titleSearch');
+ let h2TextSearch = tSearch('h2TextSearch');
+
+ title = title.replace(/%countSticker%/g, countSticker);
+ description = description.replace(/%countStickerAll%/g, countStickerAll);
+ h2Text = h2Text.replace(/%countSticker%/g, countSticker);
+
+ title = title + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+
+ if (sticker) {
+ titleSearch = titleSearch.replace(/%countSticker%/g, countSticker);
+ let promptResult = sticker.slice(0, 20);
+ titleSearch = titleSearch.replace(/%prompt%/g, promptResult);
+ title = titleSearch + ' | ' + process.env.NEXT_PUBLIC_WEBSITE_NAME;
+
+ h2TextSearch = h2TextSearch.replace(/%countSticker%/g, countSticker);
+ h2TextSearch = h2TextSearch.replace(/%prompt%/g, promptResult);
+ h2Text = h2TextSearch;
+ }
+
+ return {
+ title: title,
+ description: description,
+ h1Text: h1Text,
+ h2Text: h2Text,
+ }
+
+}
diff --git a/src/configs/openaiConfig.ts b/src/configs/openaiConfig.ts
new file mode 100644
index 0000000..97e0206
--- /dev/null
+++ b/src/configs/openaiConfig.ts
@@ -0,0 +1,129 @@
+export const apiKey = process.env.OPENAI_API_KEY
+export const model = process.env.OPENAI_API_MODEL
+export const baseUrl = process.env.OPENAI_API_BASE_URL
+export const temperature = 0
+
+
+export const getCurrentLanguage = (to = '') => {
+
+ if (to !== '') {
+ for (let i = 0; i < translateLanguageList.length; i++) {
+ if (to === translateLanguageList[i].lang) {
+ return translateLanguageList[i]
+ }
+ }
+ }
+
+ return {
+ lang: to,
+ language: to,
+ systemPrompt: "You are a professional and authentic translation engine. You only provide translations, without any explanations, and the translation results must retain all markdown formatting symbols.",
+ systemPrompt2: "if content like this: \"[](https://XXXXXXXX)\", only translate text middle of \"[\" and \"]\" other text should not change",
+ systemPrompt3: "if content contain '#',the result must also '#'",
+ systemPrompt4: `Please translate into ${to} (avoid explaining the original text). Return to me in json format {'text':'Place the translated text here'} `,
+ userPrompt: `here is the content that needs to be translated `
+ }
+}
+
+export const translateLanguageList = [
+ {
+ lang: "en",
+ language: "English",
+ languageInChineseSimple: "英语",
+ systemPrompt: "You are a professional and authentic translation engine. You only provide translations, without any explanations, and the translation results must retain all markdown formatting symbols. ",
+ systemPrompt2: "if content like this: \"[](https://XXXXXXXX)\", only translate text middle of \"[\" and \"]\" other text should not change. ",
+ systemPrompt3: "if content contain '#',the result must also '#' ",
+ systemPrompt4: "Please translate into English (avoid explaining the original text). Return to me in json format {'text':'Place the translated text here'} ",
+ userPrompt: "here is the content that needs to be translated "
+ },
+ {
+ lang: "zh",
+ language: "简体中文",
+ languageInChineseSimple: "简体中文",
+ systemPrompt: "你是一个专业、地道的翻译引擎,你只返回译文,不含任何解释,翻译结果必须保留 markdown 的所有格式标记符号 ",
+ systemPrompt2: "如果原文类似这样: '[](https://XXXXXXXX)', 只翻译 '[' 和 ']' 中间的文字,其他的文字保留原样 ",
+ systemPrompt3: "如果原文有'#',则翻译结果必须保留原文的'#' ",
+ systemPrompt4: "请翻译为简体中文(避免解释原文)。以json格式{'text':'这里放翻译好的文字'}返回给我 ",
+ userPrompt: "以下是需要翻译的内容 "
+ },
+ {
+ lang: "tw",
+ language: "繁體中文",
+ languageInChineseSimple: "繁体中文",
+ systemPrompt: "您是一個專業、道地的翻譯引擎,您僅返回譯文,不含任何解釋,翻譯結果必須保留 markdown 的所有格式標記符號。 ",
+ systemPrompt2: "如果原文類似這樣: '[](https://XXXXXXXX)', 只翻譯 '[' 和 ']' 中間的文字,其他的文字保留原樣 ",
+ systemPrompt3: "如果原文有'#',則翻譯結果必須保留原文的'#' ",
+ systemPrompt4: "請翻譯為繁體中文(避免解釋原文)。以json格式{'text':'這裡放翻譯好的文字'}返回給我 ",
+ userPrompt: "以下是需要翻譯的內容 "
+ },
+ {
+ lang: "ko",
+ language: "한국어",
+ languageInChineseSimple: "韩语",
+ systemPrompt: "당신은 전문적이고 진정한 번역 엔진입니다. 당신은 번역 텍스트만을 반환하며, 어떠한 설명도 포함하지 않습니다. 번역 결과는 markdown의 모든 형식 표시 기호를 유지해야 합니다. ",
+ systemPrompt2: "원본 텍스트가 '[](https://XXXXXXXX)'와 유사한 경우 '['와 ']' 사이의 텍스트만 번역되고 다른 텍스트는 그대로 유지됩니다. ",
+ systemPrompt3: "원문에 '#'이 있는 경우 번역 결과는 원문의 '#'을 유지해야 합니다. ",
+ systemPrompt4: "한국어로 번역해주세요(원문 설명을 피하세요)。json 형식으로 {'text':'여기에 번역된 텍스트를 넣으세요'}로 반환해 주세요 ",
+ userPrompt: "다음은 번역이 필요한 내용입니다 "
+ },
+ {
+ lang: "ja",
+ language: "日本語",
+ languageInChineseSimple: "日语",
+ systemPrompt: "あなたはプロフェッショナルで本格的な翻訳エンジンで、翻訳のみを提供し、いかなる説明も含まず、翻訳結果はすべてのmarkdownのフォーマット記号を保持しなければなりません。 ",
+ systemPrompt2: "元のテキストが「[](https://XXXXXXXX)」のような場合、「[」と「]」の間のテキストのみが翻訳され、他のテキストはそのまま残ります。 ",
+ systemPrompt3: "原文に「#」が含まれている場合、翻訳結果は原文の「#」を保持する必要があります。 ",
+ systemPrompt4: "日本語に翻訳してください(原文の説明は避けてください)。json形式で{'text':'翻訳されたテキストをここに置く'}として返してください。 ",
+ userPrompt: "以下が翻訳が必要な内容です "
+ },
+ {
+ lang: "pt",
+ language: "Português",
+ languageInChineseSimple: "葡萄牙语",
+ systemPrompt: "Você é um motor de tradução profissional e autêntico. Você retorna apenas traduções, sem quaisquer explicações, e os resultados da tradução devem manter todos os símbolos de formatação do markdown. ",
+ systemPrompt2: "Se o texto original for semelhante a este: '[](https://XXXXXXXX)', apenas o texto entre '[' e ']' será traduzido, e o outro texto permanecerá como está. ",
+ systemPrompt3: "Se o texto original contiver '#', o resultado da tradução deverá reter o '#' do texto original ",
+ systemPrompt4: "Por favor, traduza para Português (evite explicar o texto original). Retorne para mim no formato json {'text':'Coloque o texto traduzido aqui'} ",
+ userPrompt: "a seguir está o conteúdo que precisa ser traduzido "
+ },
+ {
+ lang: "es",
+ language: "Español",
+ languageInChineseSimple: "西班牙语",
+ systemPrompt: "Eres un motor de traducción profesional y auténtico. Solo devuelves textos traducidos, sin ninguna explicación, y los resultados de la traducción deben conservar todos los símbolos de formato de markdown. ",
+ systemPrompt2: "Si el texto original es similar a este: '[](https://XXXXXXXXX)', solo se traducirá el texto entre '[' y ']' y el resto del texto permanecerá como está. ",
+ systemPrompt3: "Si el texto original tiene '#', el resultado de la traducción debe conservar el '#' del texto original ",
+ systemPrompt4: "Por favor, traduce al español (evita explicar el texto original). Devuélvemelo en formato json {'text':'coloca aquí el texto traducido'} ",
+ userPrompt: "a continuación está el contenido que necesita ser traducido "
+ },
+ {
+ lang: "de",
+ language: "Deutsch",
+ languageInChineseSimple: "德语",
+ systemPrompt: "Du bist ein professioneller und authentischer Übersetzungsmotor. Du lieferst nur Übersetzungen, ohne jegliche Erklärungen, und die Übersetzungsergebnisse müssen alle Markdown-Formatierungssymbole beibehalten. ",
+ systemPrompt2: "Wenn der Originaltext etwa so aussieht: „[](https://XXXXXXXX)“, wird nur der Text zwischen „[“ und „]“ übersetzt und der andere Text bleibt unverändert. ",
+ systemPrompt3: "Wenn der Originaltext ein „#“ enthält, muss das Übersetzungsergebnis das „#“ des Originaltexts beibehalten ",
+ systemPrompt4: "Bitte übersetzen Sie in Deutsch (ohne Erklärung des Originaltextes). Geben Sie mir die Übersetzung im JSON-Format {'text':'Hier den übersetzten Text einfügen'} zurück. ",
+ userPrompt: "Hier ist der zu übersetzende Inhalt "
+ },
+ {
+ lang: "fr",
+ language: "Français",
+ languageInChineseSimple: "法语",
+ systemPrompt: "Vous êtes un moteur de traduction professionnel et authentique. Vous ne fournissez que des traductions, sans aucune explication, et les résultats de la traduction doivent conserver tous les symboles de formatage markdown. ",
+ systemPrompt2: "Si le texte original ressemble à ceci : '[](https://XXXXXXXX)', seul le texte entre '[' et ']' sera traduit, et l'autre texte restera tel quel. ",
+ systemPrompt3: "Si le texte original contient un « # », le résultat de la traduction doit conserver le « # » du texte original. ",
+ systemPrompt4: "Veuillez traduire en français (éviter d'expliquer le texte original). Renvoyez-moi au format json {'text':'Insérez le texte traduit ici'} ",
+ userPrompt: "voici le contenu à traduire "
+ },
+ {
+ lang: "vi",
+ language: "Tiếng Việt",
+ languageInChineseSimple: "越南语",
+ systemPrompt: "Bạn là một công cụ dịch thuật chuyên nghiệp và đích thực, bạn chỉ trả lại bản dịch, không bao gồm bất kỳ giải thích nào, kết quả dịch phải giữ nguyên tất cả các dấu hiệu định dạng của markdown. ",
+ systemPrompt2: "Nếu văn bản gốc tương tự như sau: '[](https://XXXXXXXX)' thì chỉ văn bản nằm giữa '[' và ']' mới được dịch và văn bản còn lại sẽ giữ nguyên. ",
+ systemPrompt3: "Nếu văn bản gốc có “#” thì kết quả dịch phải giữ lại “#” của văn bản gốc ",
+ systemPrompt4: "Vui lòng dịch sang Tiếng Việt (tránh giải thích văn bản gốc). Trả lại cho tôi dưới dạng json {'text':'đặt văn bản đã dịch ở đây'} ",
+ userPrompt: "đây là nội dung cần được dịch "
+ },
+];
diff --git a/src/configs/stripeConfig.ts b/src/configs/stripeConfig.ts
new file mode 100644
index 0000000..67bddce
--- /dev/null
+++ b/src/configs/stripeConfig.ts
@@ -0,0 +1,31 @@
+const priceProd = [
+ {
+ currency: "usd",
+ type: "recurring",
+ unit_amount: 11880,
+ id: "price_"
+ },
+ {
+ currency: "usd",
+ type: "recurring",
+ unit_amount: 1990,
+ id: "price_"
+ }
+];
+
+const priceTest = [
+ {
+ currency: "usd",
+ type: "recurring",
+ unit_amount: 11880,
+ id: "price_"
+ },
+ {
+ currency: "usd",
+ type: "recurring",
+ unit_amount: 1990,
+ id: "price_"
+ }
+];
+
+export const priceList = (process.env.NODE_ENV === 'production' ? priceProd: priceTest);
diff --git a/src/context/common-context.tsx b/src/context/common-context.tsx
new file mode 100644
index 0000000..e6ab437
--- /dev/null
+++ b/src/context/common-context.tsx
@@ -0,0 +1,72 @@
+'use client';
+import {createContext, useContext, useState} from "react";
+import {useSession} from "next-auth/react";
+import {useInterval} from "ahooks";
+
+
+const CommonContext = createContext(undefined);
+export const CommonProvider = ({
+ children,
+ commonText,
+ authText,
+ menuText,
+ pricingText
+ }) => {
+
+ const {data: session, status} = useSession();
+ const [userData, setUserData] = useState({});
+ const [intervalUserData, setIntervalUserData] = useState(1000);
+ const [showLoginModal, setShowLoginModal] = useState(false);
+ const [showLogoutModal, setShowLogoutModal] = useState(false);
+ const [showLoadingModal, setShowLoadingModal] = useState(false);
+ const [showGeneratingModal, setShowGeneratingModal] = useState(false);
+ const [showPricingModal, setShowPricingModal] = useState(false);
+
+
+ useInterval(() => {
+ init();
+ }, intervalUserData);
+
+ async function init() {
+ if (status == 'authenticated') {
+ const userData = {
+ // @ts-ignore
+ user_id: session?.user?.user_id,
+ name: session?.user?.name,
+ email: session?.user?.email,
+ image: session?.user?.image,
+ }
+ setUserData(userData);
+ setShowLoginModal(false);
+ setIntervalUserData(undefined);
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+
+}
+
+export const useCommonContext = () => useContext(CommonContext)
diff --git a/src/context/next-auth-context.tsx b/src/context/next-auth-context.tsx
new file mode 100644
index 0000000..33e597a
--- /dev/null
+++ b/src/context/next-auth-context.tsx
@@ -0,0 +1,7 @@
+'use client'
+
+import {SessionProvider} from "next-auth/react";
+
+export function NextAuthProvider({children}: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100755
index 0000000..4841382
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,10 @@
+import {getRequestConfig} from 'next-intl/server';
+
+export default getRequestConfig(async ({locale}) => ({
+ messages: (
+ await (locale === 'en'
+ ? // When using Turbopack, this will enable HMR for `default`
+ import('../messages/en.json')
+ : import(`../messages/${locale}.json`))
+ ).default
+}));
diff --git a/src/libs/R2.ts b/src/libs/R2.ts
new file mode 100644
index 0000000..3a613cb
--- /dev/null
+++ b/src/libs/R2.ts
@@ -0,0 +1,18 @@
+import {S3} from "aws-sdk";
+
+export const storageDomain = process.env.STORAGE_DOMAIN
+export const storageURL = "https://" + storageDomain
+export const r2Bucket = process.env.R2_BUCKET
+export const r2AccountId = process.env.R2_ACCOUNT_ID
+export const r2Endpoint = process.env.R2_ENDPOINT
+export const r2AccessKeyId = process.env.R2_ACCESS_KEY_ID
+export const r2SecretAccessKey = process.env.R2_SECRET_ACCESS_KEY
+
+
+export const R2 = new S3({
+ endpoint: r2Endpoint,
+ credentials: {
+ accessKeyId: r2AccessKeyId,
+ secretAccessKey: r2SecretAccessKey,
+ },
+});
diff --git a/src/libs/db.ts b/src/libs/db.ts
new file mode 100644
index 0000000..3ff90c8
--- /dev/null
+++ b/src/libs/db.ts
@@ -0,0 +1,15 @@
+import {Pool} from 'pg'
+
+let globalPool: Pool
+
+export function getDb() {
+ if (!globalPool) {
+ const connectionString = process.env.POSTGRES_URL;
+ // console.log("connectionString", connectionString);
+
+ globalPool = new Pool({
+ connectionString,
+ });
+ }
+ return globalPool;
+}
diff --git a/src/libs/handle-stripe.ts b/src/libs/handle-stripe.ts
new file mode 100644
index 0000000..544ea46
--- /dev/null
+++ b/src/libs/handle-stripe.ts
@@ -0,0 +1,138 @@
+import {toDateTime} from './helpers';
+import {stripe} from './stripe';
+import Stripe from 'stripe';
+import {getDb} from "~/libs/db";
+
+
+
+const createOrRetrieveCustomer = async ({
+ email,
+ user_id
+ }: {
+ email: string;
+ user_id: string;
+}) => {
+
+ const db = getDb();
+
+ const results = await db.query('SELECT * FROM stripe_customers where user_id=$1 limit 1', [user_id]);
+ const existUser = results.rows;
+
+ if (existUser.length <= 0) {
+ // 创建
+ const customerData: { metadata: { user_id: string }; email?: string } =
+ {
+ metadata: {
+ user_id: user_id
+ }
+ };
+ if (email) customerData.email = email;
+ const customer = await stripe.customers.create(customerData);
+ await db.query('insert into stripe_customers(user_id,stripe_customer_id) values($1, $2)', [user_id, customer.id]);
+ return customer.id;
+ }
+ return existUser[0].stripe_customer_id;
+};
+
+
+/**
+ * Copies the billing details from the payment method to the customer object.
+ */
+const copyBillingDetailsToCustomer = async (
+ user_id: string,
+ payment_method: Stripe.PaymentMethod
+) => {
+ //Todo: check this assertion
+ const customer = payment_method.customer as string;
+ const {name, phone, address} = payment_method.billing_details;
+ if (!name || !phone || !address) return;
+ //@ts-ignore
+ await stripe.customers.update(customer, {name, phone, address});
+ const db = getDb();
+ await db.query('update user_info set billing_address=$1,payment_method=$2 where user_id=$3',
+ [{...address}, {...payment_method[payment_method.type], user_id}]);
+};
+
+const manageSubscriptionStatusChange = async (
+ subscriptionId: string,
+ customerId: string,
+ createAction = false
+) => {
+ const db = getDb();
+ // Get customer's UUID from mapping table.
+ const results = await db.query('SELECT * FROM stripe_customers where stripe_customer_id=$1 limit 1', [customerId]);
+ const existCustomer = results.rows;
+ if (existCustomer.length <= 0) {
+ return;
+ }
+ const customerData = existCustomer[0];
+
+ const {user_id: user_id} = customerData!;
+
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
+ expand: ['default_payment_method']
+ });
+ // Upsert the latest status of the subscription object.
+ const subscriptionData =
+ {
+ id: subscription.id,
+ user_id: user_id,
+ metadata: subscription.metadata,
+ status: subscription.status,
+ price_id: subscription.items.data[0].price.id,
+ //TODO check quantity on subscription
+ // @ts-ignore
+ quantity: subscription.quantity,
+ cancel_at_period_end: subscription.cancel_at_period_end,
+ cancel_at: subscription.cancel_at
+ ? toDateTime(subscription.cancel_at).toISOString()
+ : null,
+ canceled_at: subscription.canceled_at
+ ? toDateTime(subscription.canceled_at).toISOString()
+ : null,
+ current_period_start: toDateTime(
+ subscription.current_period_start
+ ).toISOString(),
+ current_period_end: toDateTime(
+ subscription.current_period_end
+ ).toISOString(),
+ created: toDateTime(subscription.created).toISOString(),
+ ended_at: subscription.ended_at
+ ? toDateTime(subscription.ended_at).toISOString()
+ : null,
+ trial_start: subscription.trial_start
+ ? toDateTime(subscription.trial_start).toISOString()
+ : null,
+ trial_end: subscription.trial_end
+ ? toDateTime(subscription.trial_end).toISOString()
+ : null
+ };
+
+ const resultSubs = await db.query('SELECT * FROM stripe_subscriptions where user_id=$1 limit 1', [subscriptionData.user_id]);
+ const existSubs = resultSubs.rows;
+ if (existSubs.length <= 0) {
+ // 没有,新增
+ await db.query('insert into stripe_subscriptions(id,user_id,metadata,status,price_id,quantity,cancel_at_period_end,cancel_at,canceled_at,current_period_start,current_period_end,created,ended_at,trial_start,trial_end) values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)',
+ [subscriptionData.id, subscriptionData.user_id, subscriptionData.metadata, subscriptionData.status, subscriptionData.price_id, subscriptionData.quantity, subscriptionData.cancel_at_period_end, subscriptionData.cancel_at, subscriptionData.canceled_at, subscriptionData.current_period_start, subscriptionData.current_period_end, subscriptionData.created, subscriptionData.ended_at, subscriptionData.trial_start, subscriptionData.trial_end]);
+ } else {
+ // 有,更新
+ await db.query('update stripe_subscriptions set user_id=$1,metadata=$2,status=$3,price_id=$4,quantity=$5,cancel_at_period_end=$6,cancel_at=$7,canceled_at=$8,current_period_start=$9,current_period_end=$10,created=$11,ended_at=$12,trial_start=$13,trial_end=$14 where user_id=$15',
+ [subscriptionData.user_id, subscriptionData.metadata, subscriptionData.status, subscriptionData.price_id, subscriptionData.quantity, subscriptionData.cancel_at_period_end, subscriptionData.cancel_at, subscriptionData.canceled_at, subscriptionData.current_period_start, subscriptionData.current_period_end, subscriptionData.created, subscriptionData.ended_at, subscriptionData.trial_start, subscriptionData.trial_end, subscriptionData.user_id]);
+ }
+
+ // For a new subscription copy the billing details to the customer object.
+ // NOTE: This is a costly operation and should happen at the very end.
+ if (createAction && subscription.default_payment_method && user_id)
+ //@ts-ignore
+ await copyBillingDetailsToCustomer(
+ user_id,
+ subscription.default_payment_method as Stripe.PaymentMethod
+ );
+};
+
+
+
+export {
+ createOrRetrieveCustomer,
+ manageSubscriptionStatusChange,
+};
diff --git a/src/libs/helpers.ts b/src/libs/helpers.ts
new file mode 100644
index 0000000..55d2bae
--- /dev/null
+++ b/src/libs/helpers.ts
@@ -0,0 +1,17 @@
+export const getURL = () => {
+ let url =
+ process?.env?.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
+ process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
+ 'http://localhost/';
+ // Make sure to include `https://` when not localhost.
+ url = url.includes('http') ? url : `https://${url}`;
+ // Make sure to including trailing `/`.
+ url = url.charAt(url.length - 1) === '/' ? url : `${url}/`;
+ return url;
+};
+
+export const toDateTime = (secs: number) => {
+ var t = new Date('1970-01-01T00:30:00Z'); // Unix epoch start.
+ t.setSeconds(secs);
+ return t;
+};
diff --git a/src/libs/nextAuthClient.ts b/src/libs/nextAuthClient.ts
new file mode 100644
index 0000000..461d40c
--- /dev/null
+++ b/src/libs/nextAuthClient.ts
@@ -0,0 +1,7 @@
+import {signIn} from "next-auth/react";
+
+export async function signInUseAuth({redirectPath}) {
+ const result = await signIn('google', {
+ callbackUrl: redirectPath
+ })
+}
diff --git a/src/libs/replicate.ts b/src/libs/replicate.ts
new file mode 100644
index 0000000..589b7de
--- /dev/null
+++ b/src/libs/replicate.ts
@@ -0,0 +1,25 @@
+import {translateContent} from "~/servers/translate";
+
+
+export const getInput = async (textStr, checkSubscribeStatus) => {
+ // 翻译成英语后返回
+ const revised_text = await translateContent(textStr, 'en');
+
+ let width = 512;
+ let height = 512;
+ let upscale = false;
+ if (checkSubscribeStatus) {
+ width = 1024;
+ height = 1024
+ }
+
+ return {
+ steps: 20,
+ width: width,
+ height: height,
+ prompt: revised_text,
+ upscale: upscale,
+ upscale_steps: 2,
+ negative_prompt: "NSFW. No nudity or explicit content.No violence or gore.No sexual themes.Suitable for all ages.No illegal activities or substances.General audience appropriate.No offensive material.No hate speech or discrimination.Nothing disturbing or shocking.Respectful, non-exploitative content."
+ }
+}
diff --git a/src/libs/replicateClient.ts b/src/libs/replicateClient.ts
new file mode 100644
index 0000000..361c385
--- /dev/null
+++ b/src/libs/replicateClient.ts
@@ -0,0 +1,12 @@
+import Replicate from "replicate";
+
+let replicateClient: Replicate;
+
+export const getReplicateClient = () => {
+ if (!replicateClient) {
+ replicateClient = new Replicate({
+ auth: process.env.REPLICATE_API_TOKEN,
+ });
+ }
+ return replicateClient;
+}
diff --git a/src/libs/stripe.ts b/src/libs/stripe.ts
new file mode 100644
index 0000000..b5ac6a1
--- /dev/null
+++ b/src/libs/stripe.ts
@@ -0,0 +1,15 @@
+import Stripe from 'stripe';
+
+export const stripe = new Stripe(
+ process.env.STRIPE_SECRET_KEY_LIVE ?? process.env.STRIPE_SECRET_KEY ?? '',
+ {
+ // https://github.com/stripe/stripe-node#configuration
+ apiVersion: '2023-10-16',
+ // Register this as an official Stripe plugin.
+ // https://stripe.com/docs/building-plugins#setappinfo
+ appInfo: {
+ name: 'Next.js Subscription Starter',
+ version: '0.1.0'
+ }
+ }
+);
diff --git a/src/libs/stripeClient.ts b/src/libs/stripeClient.ts
new file mode 100644
index 0000000..dc758f1
--- /dev/null
+++ b/src/libs/stripeClient.ts
@@ -0,0 +1,15 @@
+import {loadStripe, Stripe} from '@stripe/stripe-js';
+
+let stripePromise: Promise;
+
+export const getStripe = () => {
+ if (!stripePromise) {
+ stripePromise = loadStripe(
+ process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ??
+ process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ??
+ ''
+ );
+ }
+
+ return stripePromise;
+};
diff --git a/src/libs/useOneTapSignin.ts b/src/libs/useOneTapSignin.ts
new file mode 100644
index 0000000..e5c3f9f
--- /dev/null
+++ b/src/libs/useOneTapSignin.ts
@@ -0,0 +1,54 @@
+'use client';
+import { useSession, signIn, SignInOptions } from "next-auth/react";
+import {useCommonContext} from "~/context/common-context";
+
+interface OneTapSigninOptions {
+ parentContainerId?: string;
+}
+
+const useOneTapSignin = (options?: OneTapSigninOptions & Pick) => {
+ const { parentContainerId } = options || {};
+ const { showLoadingModal, setShowLoadingModal } = useCommonContext();
+
+ // Taking advantage in recent development of useSession hook.
+ // If user is unauthenticated, google one tap ui is initialized and rendered
+ const { status } = useSession({
+ required: true,
+ onUnauthenticated() {
+ if (!showLoadingModal) {
+ const { google } = window;
+ if (google) {
+ google.accounts.id.initialize({
+ client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
+ callback: async (response: any) => {
+ setShowLoadingModal(true);
+ // Here we call our Provider with the token provided by google
+ await signIn("googleonetap", {
+ credential: response.credential,
+ redirect: true,
+ ...options,
+ });
+ setShowLoadingModal(false);
+ },
+ prompt_parent_id: parentContainerId,
+ });
+ // Here we just console.log some error situations and reason why the Google one tap
+ // is not displayed. You may want to handle it depending on your application
+ google.accounts.id.prompt((notification: any) => {
+ if (notification.isNotDisplayed()) {
+ console.log("getNotDisplayedReason ::", notification.getNotDisplayedReason());
+ } else if (notification.isSkippedMoment()) {
+ console.log("getSkippedReason ::", notification.getSkippedReason());
+ } else if (notification.isDismissedMoment()) {
+ console.log("getDismissedReason ::", notification.getDismissedReason());
+ }
+ });
+ }
+ }
+ },
+ });
+
+ return { showLoadingModal };
+};
+
+export default useOneTapSignin;
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..76d9caa
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,25 @@
+import createMiddleware from 'next-intl/middleware';
+import {pathnames, locales, localePrefix} from './config';
+
+export default createMiddleware({
+ defaultLocale: 'en',
+ locales,
+ pathnames,
+ localePrefix,
+ localeDetection: false
+});
+
+export const config = {
+ matcher: [
+ // Enable a redirect to a matching locale at the root
+ '/',
+
+ // Set a cookie to remember the previous locale for
+ // all requests that have a locale prefix
+ '/(en|zh)/:path*',
+
+ // Enable redirects that add missing locales
+ // (e.g. `/pathnames` -> `/en/pathnames`)
+ '/((?!_next|_vercel|.*\\..*).*)'
+ ]
+};
diff --git a/src/navigation.ts b/src/navigation.ts
new file mode 100644
index 0000000..44f4374
--- /dev/null
+++ b/src/navigation.ts
@@ -0,0 +1,9 @@
+import {createLocalizedPathnamesNavigation} from 'next-intl/navigation';
+import {locales, pathnames, localePrefix} from './config';
+
+export const {Link, redirect, usePathname, useRouter} =
+ createLocalizedPathnamesNavigation({
+ locales,
+ pathnames,
+ localePrefix
+ });
diff --git a/src/servers/checkInput.ts b/src/servers/checkInput.ts
new file mode 100644
index 0000000..d9fe0fe
--- /dev/null
+++ b/src/servers/checkInput.ts
@@ -0,0 +1,17 @@
+import {getDb} from "~/libs/db";
+
+
+const db = getDb();
+export const checkSensitiveInputText = async (input_text:string) => {
+ const {rows: sensitiveWords} = await db.query('select * from sensitive_words');
+ if (sensitiveWords.length > 0) {
+ for (let i = 0; i < sensitiveWords.length; i++) {
+ const currentSensitive = sensitiveWords[i];
+ const currentWords = currentSensitive.words;
+ if (input_text.indexOf(currentWords) != -1) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
diff --git a/src/servers/keyValue.ts b/src/servers/keyValue.ts
new file mode 100644
index 0000000..da79fe1
--- /dev/null
+++ b/src/servers/keyValue.ts
@@ -0,0 +1,30 @@
+import {getDb} from "~/libs/db";
+
+
+export const countSticker = async (key, addCount) => {
+ const db = getDb();
+
+ const results = await db.query('select * from key_value where key=$1 limit 1', [key]);
+ const rows = results.rows;
+ if (rows.length <= 0) {
+ // 新增
+ await db.query('insert into key_value(key, value) values($1,$2)', [key, addCount]);
+ }
+ // 更新
+ const origin = rows[0];
+ const newCount = Number(origin.value) + addCount
+ await db.query('update key_value set value=$1 where key=$2', [newCount, key]);
+
+}
+
+export const getCountSticker = async () => {
+ const db = getDb();
+
+ const results = await db.query('select * from key_value where key=$1 limit 1', ['countSticker']);
+ const rows = results.rows;
+ if (rows.length > 0) {
+ const origin = rows[0];
+ return origin.value;
+ }
+ return '';
+}
diff --git a/src/servers/language.ts b/src/servers/language.ts
new file mode 100644
index 0000000..b937876
--- /dev/null
+++ b/src/servers/language.ts
@@ -0,0 +1,41 @@
+import {apiKey, baseUrl} from "~/configs/openaiConfig";
+
+export const model = 'openai/gpt-4o';
+export const temperature = 0
+export const getLanguage = async (content: string) => {
+ let body = {
+ messages: [
+ {
+ role: 'system',
+ content: `你是一个语言分析专家,能够直接识别文本是什么语言,并且区分繁体中文和简体中文,如果是繁体中文则返回tw,简体中文返回zh。`
+ },
+ {
+ role: 'system',
+ content: `识别这段文字的语言,只返回语言的英文缩写,不含任何解释!`
+ },
+ {
+ role: 'user',
+ content: `需要识别的内容: ${content}`
+ }
+ ],
+ model: model,
+ temperature: temperature,
+ stream: false
+ }
+ let languageResult = await fetch(`${baseUrl}/v1/chat/completions`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: {
+ 'content-type': 'application/json',
+ authorization: `Bearer ${apiKey}`
+ }
+ })
+ .then(v => v.json()).catch(err => console.log(err));
+
+ // console.log('content-=->', content);
+ // console.log('languageResult-=->', languageResult);
+ // console.log('languageResult?.choices[0]?.message-=->', languageResult?.choices[0]?.message);
+ const lang = languageResult?.choices[0]?.message?.content.substring(0, 2) || 'en';
+ // console.log('lang->', lang);
+ return lang;
+}
diff --git a/src/servers/manageUserTimes.ts b/src/servers/manageUserTimes.ts
new file mode 100644
index 0000000..b04c898
--- /dev/null
+++ b/src/servers/manageUserTimes.ts
@@ -0,0 +1,23 @@
+import {getDb} from "~/libs/db";
+
+const db = getDb();
+export const checkUserTimes = async (user_id) => {
+ const results = await db.query(`select * from user_available where user_id=$1;`, [user_id]);
+ const result = results.rows;
+ if (result.length <= 0) {
+ return false;
+ }
+ const available = result[0];
+ const available_times = available.available_times;
+ return available_times > 0;
+}
+
+export const countDownUserTimes = async (user_id) => {
+ const results = await db.query(`select * from user_available where user_id=$1;`, [user_id]);
+ const result = results.rows;
+ if (result.length > 0) {
+ const available = result[0];
+ const resultTimes = available.available_times - 1;
+ await db.query('update user_available set available_times=$1,updated_at=now() where user_id=$2', [resultTimes, user_id]);
+ }
+}
diff --git a/src/servers/search.ts b/src/servers/search.ts
new file mode 100644
index 0000000..9ffffaa
--- /dev/null
+++ b/src/servers/search.ts
@@ -0,0 +1,96 @@
+import {getDb} from "~/libs/db";
+import {getArrayUrlResult} from "~/configs/buildLink";
+import {headers} from "next/headers";
+
+export const searchByWords = async (locale, textStr) => {
+ const db = getDb();
+ const worksList = await searchDatabaseWithoutLocale(textStr);
+ if (worksList.length > 0) {
+ // 得到所有的 uid,查询对应语言下的数据返回
+ const uidArray = [];
+ let searchTerms = textStr.split(" ");
+ for (let i = 0; i < worksList.length; i++) {
+ const currentRow = worksList[i];
+ const currentText = currentRow.input_text?.split(' ');
+ let checkExist = false;
+ for (let j = 0; j < currentText.length; j++) {
+ for (let k = 0; k < searchTerms.length; k++) {
+ if (currentText[j]?.toLowerCase() == searchTerms[k]?.toLowerCase()) {
+ // 单词转小写后全匹配过滤
+ checkExist = true;
+ break;
+ }
+ }
+ }
+ if (checkExist) {
+ uidArray.push(currentRow.uid);
+ }
+ }
+ // console.log('uidArray-=-=-->', uidArray);
+ if (uidArray.length <= 0) {
+ return [];
+ }
+ let uidStr = uidArray.map(item => `'${item}'`).join(",");
+ // console.log('uidStr-=-=-->', uidStr);
+ // 查询对应语言的数据
+ const queryStr = `select * from works where uid in (${uidStr}) and current_language = $1 and is_delete=$2`;
+ const resultRows = await db.query(queryStr, [locale, false]);
+ const rows = resultRows.rows;
+ if (rows.length > 0) {
+ const resultInfoList = [];
+ for (let i = 0; i < rows.length; i++) {
+ const currentRow = rows[i];
+ if (resultInfoList.length > 23) {
+ break;
+ }
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ resultInfoList.push(currentRow);
+ }
+ return resultInfoList;
+ }
+ }
+ return [];
+}
+
+async function searchDatabaseWithoutLocale(inputString) {
+ const db = getDb();
+ // 分割输入字符串
+ let searchTerms = inputString.split(" ");
+
+ // 构建SQL查询
+ let query = `SELECT *,
+ (
+ ${searchTerms.map((_, index) => `CASE WHEN input_text ILIKE $${index + 1} and is_delete=false THEN 1 ELSE 0 END`).join(' + ')}
+ ) AS relevance
+ FROM works
+ WHERE ${searchTerms.map((_, index) => `input_text ILIKE $${index + 1} and is_delete=false`).join(' OR ')}
+ ORDER BY relevance DESC limit 100;`;
+
+ // 准备参数数组,为每个term包装成带有通配符的字符串
+ let queryParams = searchTerms.map(term => `%${term}%`);
+
+ // console.log("query-=>", query);
+ // console.log("queryParams-=>", queryParams);
+
+ try {
+ // 执行查询
+ const {rows} = await db.query(query, queryParams);
+ // console.log(rows);
+ return rows;
+ } catch (error) {
+ console.error('Error executing query', error.stack);
+ throw error;
+ }
+}
+
+
+export const addSearchLog = async (search_text, result_count) => {
+ const db = getDb();
+
+ const headerAll = headers();
+ const search_ip = headerAll.get("x-forwarded-for");
+ const user_agent = headerAll.get("user-agent");
+
+ await db.query('insert into search_log(search_text, result_count, user_agent, search_ip) values($1,$2,$3,$4)',
+ [search_text, result_count, user_agent, search_ip])
+}
diff --git a/src/servers/subscribe.ts b/src/servers/subscribe.ts
new file mode 100644
index 0000000..281e10c
--- /dev/null
+++ b/src/servers/subscribe.ts
@@ -0,0 +1,16 @@
+import {getDb} from "~/libs/db";
+
+export const checkSubscribe = async (user_id: string) => {
+ const db = getDb();
+
+ const results = await db.query('SELECT * FROM stripe_subscriptions where user_id=$1', [user_id]);
+ const origin = results.rows;
+
+ if (origin.length > 0) {
+ const data = origin[0].status;
+ if (data == 'active') {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/src/servers/translate.ts b/src/servers/translate.ts
new file mode 100644
index 0000000..0ac15a9
--- /dev/null
+++ b/src/servers/translate.ts
@@ -0,0 +1,69 @@
+import {apiKey, baseUrl, getCurrentLanguage, model, temperature} from "~/configs/openaiConfig";
+
+export const translateContent = async (userContent: string, to: string) => {
+
+ let currentLanguage = getCurrentLanguage(to)
+ const body = {
+ messages: [
+ {
+ role: 'system',
+ content: `${currentLanguage.systemPrompt}`
+ },
+ {
+ role: 'system',
+ content: `${currentLanguage.systemPrompt2}`
+ },
+ {
+ role: 'system',
+ content: `${currentLanguage.systemPrompt3}`
+ },
+ {
+ role: 'system',
+ content: `${currentLanguage.systemPrompt4}`
+ },
+ {
+ role: 'user',
+ content: `${currentLanguage.userPrompt}: '${userContent}'`
+ }
+ ],
+ model: model,
+ temperature: temperature,
+ stream: false,
+ response_format: {
+ type: 'json_object'
+ }
+ }
+
+ // console.log('requestBody->>>>', body);
+ const translateResult = await fetch(`${baseUrl}/v1/chat/completions`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: {
+ 'content-type': 'application/json',
+ authorization: `Bearer ${apiKey}`
+ }
+ })
+ .then(v => v.json()).catch(err => console.log(err)) || undefined;
+ // console.log('translateResult->>>>', translateResult);
+ if (!translateResult) {
+ return userContent;
+ }
+
+ // console.log('translateResult.choices[0].message-->>>>', translateResult.choices[0].message);
+ // console.log('translateResult.choices[0].message?.content-->>>>', translateResult.choices[0].message?.content);
+ let translateResultText = userContent;
+ try {
+ if (translateResult?.choices[0]?.message?.content) {
+ translateResultText = JSON.parse(translateResult?.choices[0]?.message?.content).text || '';
+ }
+ return translateResultText;
+ } catch (e) {
+ console.log(e);
+ return userContent;
+ }
+
+}
+
+
+
+
diff --git a/src/servers/user.ts b/src/servers/user.ts
new file mode 100644
index 0000000..38f37ac
--- /dev/null
+++ b/src/servers/user.ts
@@ -0,0 +1,82 @@
+import {v4 as uuidv4} from 'uuid';
+import {getDb} from "~/libs/db";
+
+export const checkAndSaveUser = async (name: string, email: string, image: string, last_login_ip: string) => {
+ const db = getDb();
+ const results = await db.query(`select * from user_info where email=$1;`, [email]);
+ const users = results.rows;
+ if (users.length <= 0) {
+ const result = {
+ user_id: '',
+ name: '',
+ email: '',
+ image: '',
+ }
+ // 新增
+ const strUUID = uuidv4();
+ await db.query('insert into user_info(user_id,name,email,image,last_login_ip) values($1,$2,$3,$4,$5)',
+ [strUUID, name, email, image, last_login_ip]);
+
+ // 免费生成次数
+ const freeTimes = Number(process.env.FREE_TIMES);
+ await db.query('insert into user_available(user_id,stripe_customer_id,available_times) values($1, $2, $3)', [strUUID, '', freeTimes]);
+
+ result.user_id = strUUID;
+ result.name = name;
+ result.email = email;
+ result.image = image;
+ return result;
+ } else {
+ // 更新
+ const user = users[0];
+ await db.query('update user_info set name=$1,image=$2,last_login_ip=$3,updated_at=now() where id=$4',
+ [name, image, last_login_ip, user.id]);
+ return user;
+ }
+}
+
+export const getUserById = async (user_id) => {
+ const db = getDb();
+ const results = await db.query('select * from user_info where user_id=$1', [user_id]);
+ const users = results.rows;
+ if (users.length > 0) {
+ const user = users[0];
+ return {
+ user_id: user_id,
+ name: user.name,
+ email: user.email,
+ image: user.image,
+ status: 1
+ }
+ }
+ return {
+ user_id: user_id,
+ name: '',
+ email: '',
+ image: '',
+ status: 0
+ }
+}
+
+export const getUserByEmail = async (email) => {
+ const db = getDb();
+ const results = await db.query('select * from user_info where email=$1', [email]);
+ const users = results.rows;
+ if (users.length > 0) {
+ const user = users[0];
+ return {
+ user_id: user.user_id,
+ name: user.name,
+ email: email,
+ image: user.image,
+ status: 1
+ }
+ }
+ return {
+ user_id: '',
+ name: '',
+ email: email,
+ image: '',
+ status: 0
+ }
+}
diff --git a/src/servers/works.ts b/src/servers/works.ts
new file mode 100644
index 0000000..0a81d20
--- /dev/null
+++ b/src/servers/works.ts
@@ -0,0 +1,200 @@
+import {getDb} from "~/libs/db";
+import {getArrayUrlResult} from "~/configs/buildLink";
+
+const db = getDb();
+
+export const getWorkDetailByUid = async (locale:string, uid:string) => {
+ // 先查指定语言的是否有,没有则返回原始数据
+ const resultsCurrent = await db.query('select * from works where uid=$1 and current_language=$2 and is_delete=$3 order by updated_at desc', [uid, locale, false]);
+ const currentRows = resultsCurrent.rows;
+ if (currentRows.length > 0) {
+ const currentRow = currentRows[0];
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ return currentRow;
+ }
+
+ const results = await db.query('select * from works where uid=$1 and is_origin=$2 and is_delete=$3', [uid, true, false]);
+ const works = results.rows;
+ if (works.length > 0) {
+ const currentRow = works[0];
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ return currentRow;
+ }
+ return {
+ status: 404
+ }
+}
+
+export const getSimilarList = async (revised_text, uid, locale) => {
+ const worksList = await searchDatabase(revised_text, locale);
+ let searchTerms = revised_text.split(" ");
+ if (worksList.length > 0) {
+ const resultInfoList = [];
+ for (let i = 0; i < worksList.length; i++) {
+ const currentRow = worksList[i];
+ if (currentRow.uid == uid) {
+ continue;
+ }
+ const currentText = currentRow.input_text?.split(' ');
+ let checkExist = false;
+ for (let j = 0; j < currentText.length; j++) {
+ for (let k = 0; k < searchTerms.length; k++) {
+ if (currentText[j]?.toLowerCase() == searchTerms[k]?.toLowerCase()) {
+ checkExist = true;
+ break;
+ }
+ }
+ }
+ if (resultInfoList.length > 11) {
+ break;
+ }
+ if (checkExist) {
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ resultInfoList.push(currentRow);
+ }
+ }
+ return resultInfoList;
+ }
+ return [];
+}
+
+async function searchDatabase(inputString, locale) {
+ // 分割输入字符串
+ let searchTerms = inputString.split(" ");
+
+ // 构建SQL查询
+ let query = `SELECT *, (
+ ${searchTerms.map((_,index) => `CASE WHEN is_delete=false and current_language='${locale}' and input_text ILIKE $${index + 1} THEN 1 ELSE 0 END`).join(' + ')}
+ ) AS relevance
+ FROM works
+ WHERE ${searchTerms.map((_, index) => `is_delete=false and current_language='${locale}' and input_text ILIKE $${index + 1}`).join(' OR ')}
+ ORDER BY relevance DESC limit 100;`;
+
+ // 准备参数数组,为每个term包装成带有通配符的字符串
+ let queryParams = searchTerms.map(term => `%${term}%`);
+
+ // console.log("query-=>", query);
+ // console.log("queryParams-=>", queryParams);
+
+ try {
+ // 执行查询
+ const { rows } = await db.query(query, queryParams);
+ // console.log(rows);
+ return rows;
+ } catch (error) {
+ console.error('Error executing query', error.stack);
+ throw error;
+ }
+}
+
+export const getWorkListByUserId = async (user_id: string, current_page:string) => {
+ const pageSize = Number(process.env.NEXT_PUBLIC_PAGES_SIZE);
+ const skipSize = pageSize * (Number(current_page) - 1);
+
+ const results = await db.query('select * from works where user_id=$1 and is_origin=$2 and is_delete=$3 order by updated_at desc limit $4 offset $5', [user_id, true, false, pageSize, skipSize]);
+ const works = results.rows;
+
+ const resultInfoList = [];
+ if (works.length > 0) {
+ for (let i = 0; i < works.length; i++) {
+ const currentRow = works[i];
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ resultInfoList.push(currentRow)
+ }
+ return resultInfoList;
+ }
+
+ return [];
+}
+
+export const getPublicResultList = async (locale, current_page) => {
+ const pageSize = Number(process.env.NEXT_PUBLIC_PAGES_SIZE);
+ const skipSize = pageSize * (Number(current_page) - 1);
+
+ const results = await db.query('select * from works where is_public=$1 and current_language=$2 and output_url != $3 and is_delete=$4 order by updated_at desc limit $5 offset $6', [true, locale, '', false, pageSize, skipSize]);
+ const works = results.rows;
+
+ const resultInfoList = [];
+ if (works.length > 0) {
+ for (let i = 0; i < works.length; i++) {
+ const currentRow = works[i];
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ resultInfoList.push(currentRow)
+ }
+ return resultInfoList;
+ }
+
+ return [];
+}
+
+export const getLatestPublicResultList = async (locale, current_page) => {
+ // 首页数据
+ const pageSize = 8;
+ const skipSize = pageSize * (Number(current_page) - 1);
+
+ const results = await db.query('select * from works where is_public=$1 and current_language=$2 and output_url != $3 and is_delete=$4 order by updated_at desc limit $5 offset $6', [true, locale, '', false, pageSize, skipSize]);
+ const works = results.rows;
+
+ const resultInfoList = [];
+ if (works.length > 0) {
+ for (let i = 0; i < works.length; i++) {
+ const currentRow = works[i];
+ currentRow.output_url = getArrayUrlResult(currentRow.output_url);
+ resultInfoList.push(currentRow)
+ }
+ return resultInfoList;
+ }
+
+ return [];
+}
+
+export const getPagination = async (locale:string, page: number) => {
+
+ const pageSize = Number(process.env.NEXT_PUBLIC_PAGES_SIZE);
+ const results = await db.query('select count(1) from works where is_public=$1 and current_language=$2 and is_delete=$3', [true, locale, false]);
+ const countTotal = results.rows;
+
+ const total = countTotal[0].count;
+ const totalPage = Math.ceil(total / pageSize)
+
+ const result = {
+ totalPage: totalPage,
+ pagination: createPagination(totalPage, Number(page), 6),
+ }
+ return result
+}
+
+function createPagination(totalPages, currentPage, maxPagesToShow) {
+ const pages = [];
+ let startPage, endPage;
+
+ if (totalPages <= maxPagesToShow) {
+ // 总页数少于或等于最大显示页数,显示所有页码
+ startPage = 1;
+ endPage = totalPages;
+ } else {
+ // 确定页码的开始和结束位置
+ const maxPagesBeforeCurrentPage = Math.floor(maxPagesToShow / 2);
+ const maxPagesAfterCurrentPage = Math.ceil(maxPagesToShow / 2) - 1;
+ if (currentPage <= maxPagesBeforeCurrentPage) {
+ // 当前页码靠近开始
+ startPage = 1;
+ endPage = maxPagesToShow;
+ } else if (currentPage + maxPagesAfterCurrentPage >= totalPages) {
+ // 当前页码靠近结束
+ startPage = totalPages - maxPagesToShow + 1;
+ endPage = totalPages;
+ } else {
+ // 当前页码在中间
+ startPage = currentPage - maxPagesBeforeCurrentPage;
+ endPage = currentPage + maxPagesAfterCurrentPage;
+ }
+ }
+
+ // 生成页码
+ for (let i = startPage; i <= endPage; i++) {
+ pages.push(i);
+ }
+ return pages;
+}
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..619f544
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,34 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/**/*.{tsx,css}'],
+ theme: {
+ fontFamily: {
+ sans: ['Inter', 'sans-serif'],
+ mono: [
+ 'Monaco',
+ 'ui-monospace',
+ 'SFMono-Regular',
+ 'Menlo',
+ 'Consolas',
+ 'Liberation Mono',
+ 'Courier New',
+ 'monospace'
+ ]
+ },
+ container: {
+ center: true,
+ screens: {
+ sm: '50rem'
+ }
+ },
+ extend: {
+ colors: {
+ slate: {
+ 850: 'hsl(222deg 47% 16%)'
+ },
+ primary: '#5fc3e7'
+ }
+ }
+ },
+ plugins: [require('@tailwindcss/typography')]
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8084b20
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "baseUrl": "src",
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "strict": false,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "~/*": [
+ "./*"
+ ]
+ },
+ "isolatedModules": true
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules/*"
+ ]
+}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..f8c271a
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,8 @@
+{
+ "crons": [
+ {
+ "path": "/api/cron/workTranslate",
+ "schedule": "*/1 * * * *"
+ }
+ ]
+}