Sticker.Show

This commit is contained in:
littletry 2024-07-08 18:37:12 +08:00
parent 31c8f7bd7a
commit c897ef82eb
114 changed files with 7258 additions and 0 deletions

88
.env.example Normal file
View File

@ -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

88
.env.production Normal file
View File

@ -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

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View File

@ -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

View File

@ -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

3
global.d.ts vendored Executable file
View File

@ -0,0 +1,3 @@
// Use type safe message keys with `next-intl`
type Messages = typeof import('./messages/en.json');
declare interface IntlMessages extends Messages {}

128
messages/en.json Normal file
View File

@ -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": ""
}
}

128
messages/zh.json Normal file
View File

@ -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": ""
}
}

18
next.config.mjs Normal file
View File

@ -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);

44
package.json Normal file
View File

@ -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"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

172
public/appicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

8
public/robots.txt Executable file
View File

@ -0,0 +1,8 @@
User-Agent: *
Allow: *
Disallow: /api
Disallow: /api/
Disallow: /api*
Disallow: /*/api
Disallow: /*/api/
Disallow: /*/api*

124
public/sitemap.xml Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://sticker.show/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://sticker.show/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://sticker.show/de/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/de/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/es/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/es/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/fr/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/fr/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/ja/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/ja/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/ko/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/ko/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/pt/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/pt/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/tw/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/tw/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/vi/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/vi/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/zh/</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://sticker.show/zh/stickers</loc>
<lastmod>2024-03-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
</urlset>

BIN
public/top_blurred.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

336
public/website.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 423 KiB

View File

@ -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';

View File

@ -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 '可用点数';

View File

@ -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';

View File

@ -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';

47
sql/tables/5_works.sql Normal file
View File

@ -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';

View File

@ -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';

View File

@ -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已翻译';

View File

@ -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';

View File

@ -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';

View File

@ -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) ?
<meta name="robots" content="noindex"/>
:
null
}
<HeadInfo
locale={locale}
page={pagePath}
title={indexText.title}
description={indexText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<PricingModal
locale={locale}
page={pagePath}
/>
<div className="mt-4 my-auto">
<TopBlurred/>
<div className="block overflow-hidden text-white">
<div className="mx-auto w-full px-5 mb-5">
<div
className="mx-auto flex max-w-4xl flex-col items-center text-center py-10">
<h2 className="mb-4 text-4xl font-bold md:text-6xl">{indexText.h1Text}</h2>
<div className="mb-5 max-w-[628px] lg:mb-8">
<h1 className="text-[#7c8aaa] text-xl">{indexText.descriptionBelowH1Text}</h1>
</div>
</div>
<div className={"max-w-7xl px-9 mx-auto"}>
<div
className={"mx-auto rounded-tl-[30px] rounded-tr-[30px] border-[12px] border-[#ffffff1f] object-fill"}>
<form onSubmit={handleSubmit} className="relative shadow-lg">
<div
className="overflow-hidden rounded-tl-[20px] rounded-tr-[20px]">
<textarea
rows={5}
name="description"
id="description"
className="custom-textarea block w-full resize-none text-gray-900 placeholder:text-gray-400 text-lg p-4 pl-4"
placeholder={commonText.placeholderText}
value={textStr}
onChange={(e) => {
setTextStr(e.target.value);
}}
maxLength={400}
/>
</div>
{
userData?.user_id ?
<div
className="flex flex-col justify-start items-center md:flex-row md:justify-center md:items-center bg-white text-black md:pb-2">
{
process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' && availableTimes.subscribeStatus == '' ?
<>
<p>{commonText.timesLeft} <span
className={"text-red-400"}>{availableTimes.available_times}</span> {commonText.timesRight}
<span className={"font-bold hidden md:inline-flex"}>&nbsp;|&nbsp;</span>
</p>
</>
:
process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' && availableTimes.subscribeStatus == 'active' ?
<>
<span className={"text-red-400"}>{pricingText.subscriptionIntro0}</span>
<span className={"font-bold hidden md:inline-flex"}>&nbsp;|&nbsp;</span>
</>
:
null
}
{
process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' ?
<div className={"inline-flex mb-2 md:mb-0"}>
<span className={"text-black mr-1"}>{commonText.displayPublic}</span>
<Switch
checked={isPublic}
onChange={checkSubscribe}
className={classNames(
isPublic ? 'bg-[#f05011]' : 'bg-gray-200',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out'
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
isPublic ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
)}
/>
</Switch>
</div>
:null
}
</div>
:
null
}
<div className="inset-x-px bottom-1 bg-white">
<div
className="flex justify-center items-center space-x-3 border-t border-gray-200 px-2 py-2">
<div className="pt-2 w-1/4">
<button
type="submit"
className="w-full inline-flex justify-center items-center rounded-md bg-[#ffa11b] px-3 py-2 text-xs md:text-lg font-semibold text-white shadow-sm hover:bg-[#f05011]"
>
{commonText.buttonText}
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div className={"w-[90%] mx-auto mb-10 mt-8"}>
<div className={"flex justify-center items-start"}>
<h2 className="text-white text-3xl">{menuText.header2}</h2>
</div>
<div
role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{resultInfoList.map((file, index) => (
<div key={file.input_text + index} className={"mt-6"}>
<div
className="rounded-xl flex justify-center items-start checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, file.input_text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
<div key={"more"} className={"px-4"}>
<Link
href={getLinkHref(locale, 'stickers')}
onClick={() => setShowLoadingModal(true)}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.exploreMore} {'>>'}
</Link>
</div>
<div className="prose w-full max-w-2xl mx-auto mt-8 text-gray-300 div-markdown-color">
<Markdown>
{questionText.detailText}
</Markdown>
</div>
</div>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -0,0 +1,5 @@
import {notFound} from 'next/navigation';
export default function CatchAllPage() {
notFound();
}

View File

@ -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};

View File

@ -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;
}

View File

@ -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});
}

View File

@ -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);
}

View File

@ -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<Stripe.Checkout.Session>;
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
});
}
}

View File

@ -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
});
}

View File

@ -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}));
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
});
}

View File

@ -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);
}

View File

@ -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
});
}

View File

@ -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
});
}

View File

@ -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 (
<html lang={locale}>
<head>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body suppressHydrationWarning={true} className={clsx(inter.className, 'flex flex-col background-div')}>
<NextAuthProvider>
<CommonProvider
commonText={commonText}
authText={authText}
menuText={menuText}
pricingText={pricingText}
>
{children}
</CommonProvider>
</NextAuthProvider>
</body>
</html>
);
}

View File

@ -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 (
<>
<meta name="robots" content="noindex"/>
<HeadInfo
locale={locale}
page={pagePath}
title={worksText.title}
description={worksText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className={"my-auto h-screen overflow-y-auto"} ref={myRef} onScroll={handleScroll}>
<TopBlurred/>
<div className="block overflow-hidden text-white">
<div className="mx-auto w-full px-5 mb-5">
<div
className="mx-auto flex max-w-4xl flex-col items-center text-center py-6">
<h1 className="text-4xl font-bold md:text-6xl">{worksText.h1Text}</h1>
</div>
<div key={"more"} className={"px-6"}>
<Link
href={getLinkHref(locale, '')}
onClick={() => setShowLoadingModal(true)}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.generateNew} {'>>'}
</Link>
</div>
<div className={"w-[90%] mx-auto mb-20"}>
<div role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{resultInfoList?.map((file, index) => (
<div key={file.input_text + index}>
<div
className="rounded-xl flex justify-center items-start mt-6 checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, file.input_text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
worksText={worksText}
/>
)
}

31
src/app/[locale]/page.tsx Normal file
View File

@ -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 (
<PageComponent
locale={locale}
indexText={indexText}
questionText={questionText}
resultInfoListInit={resultInfoListInit}
searchParams={searchParams}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={pricingText.title}
description={pricingText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className={"mt-8 my-auto min-h-[90vh]"}>
<TopBlurred/>
<Pricing
redirectUrl={`${locale}/pricing`}
isPricing={true}
/>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={privacyPolicyText.title}
description={privacyPolicyText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className="mt-6 my-auto min-h-[80vh]">
<TopBlurred/>
<main className="w-[95%] md:w-[65%] lg:w-[55%] 2xl:w-[45%] mx-auto h-full my-8">
<div className="p-6 prose mx-auto text-gray-300 div-markdown-color">
<Markdown>
{privacyPolicyText.detailText}
</Markdown>
</div>
</main>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
privacyPolicyText={privacyPolicyText}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={searchText.title}
description={searchText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className={"my-auto mt-4 min-h-[90vh]"}>
<TopBlurred/>
<div className="mx-auto w-full max-w-7xl px-5 py-4 md:px-10 md:py-12 lg:py-18">
<div className="mx-auto max-w-3xl text-center">
<h2 className="mb-4 text-4xl font-bold md:text-6xl text-white">{searchText.h1Text}</h2>
<h1 className="mx-auto mb-6 text-sm md:text-xl text-[#7c8aaa]">
{searchText.h2Text}
</h1>
<div className="mx-auto mb-4 flex max-w-xl justify-center">
<form onSubmit={handleSubmit} className="relative w-full max-w-[80%]">
<input type="text"
className="rounded-lg h-9 w-full bg-white px-3 py-6 text-sm text-[#333333] custom-textarea"
placeholder={commonText.placeholderText}
value={textStr}
onChange={(e) => {
setTextStr(e.target.value);
}}
maxLength={100}
/>
<input type="submit"
value={commonText.searchButtonText}
className="rounded-r-md rounded-l-md md:rounded-l-none relative top-[5px] md:top-0 md:h-full w-full cursor-pointer bg-[#f05011] px-6 py-2 text-center font-semibold text-white sm:absolute md:right-0 sm:w-auto"/>
</form>
</div>
</div>
</div>
{
resultInfoList.length <= 0 ?
<div className={"px-6 py-4"}>
<Link href={getLinkHref(locale, '')}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.generateNew} {'>>'}
</Link>
</div>
:
null
}
{
resultInfoList.length > 0 ?
<>
<div className={"w-[90%] mx-auto mb-10"}>
<div role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{resultInfoList.map((file, index) => (
<div key={file.input_text + index} className={"mt-6"}>
<div
className="rounded-xl flex justify-center items-start checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, file.input_text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
<div key={"more"} className={"px-4"}>
<Link
href={getLinkHref(locale, 'stickers')}
onClick={() => setShowLoadingModal(true)}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.exploreMore} {'>>'}
</Link>
</div>
</>
:
null
}
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
searchText={searchText}
resultInfoListInit={resultInfoListInit}
sticker={sticker}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={detailText.title}
description={detailText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<PricingModal
locale={locale}
page={pagePath}
/>
<div className="mt-4 my-auto min-h-[90vh]">
{/*<TopBlurred/>*/}
<div className="block overflow-hidden bg-cover bg-center text-white">
<div className="mx-auto w-full max-w-7xl px-5 mb-5">
<div
className="mx-auto flex max-w-4xl flex-col items-center text-center py-10">
<h1 className="mb-4 text-2xl font-bold md:text-4xl">{detailText.h1Text}</h1>
</div>
<div key={"more"} className={"px-6 py-4"}>
<Link
href={getLinkHref(locale, '') + '?prompt=' + workDetail.input_text}
onClick={() => setShowLoadingModal(true)}
rel={"nofollow"}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.generateNew} {'>>'}
</Link>
</div>
<div className={"mx-auto"}>
<div className={"flex-col px-8 py-4 justify-center items-center mx-auto"}>
<div className={"grid grid-cols-1 md:grid-cols-2 gap-2"}>
{
workDetail?.output_url?.length > 0 ?
<div className={"rounded-lg flex justify-center items-start"}>
<div className={"flex-col items-center"}>
<div className="relative">
<img
title={workDetail.input_text}
src={getCompressionImageLink(workDetail?.output_url[0])}
alt={workDetail.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
className={"rounded-lg checkerboard z-20"}
width={400}
height={400}
/>
<Link
href={`${getShareToPinterest(locale, 'sticker/' + workDetail.uid, detailText.h1Text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div
className="flex justify-center items-center space-x-3 px-2 py-2">
<div className="pt-2">
<button
type="submit"
onClick={() => downloadResult(workDetail?.output_url[0], 0)}
className="w-full inline-flex justify-center items-center rounded-md bg-[#ffa11b] px-3 py-2 text-xs md:text-lg font-semibold text-white shadow-sm hover:bg-[#f05011]"
>
{commonText.download}
</button>
</div>
</div>
</div>
</div>
:
null
}
{
workDetail?.output_url?.length > 1 ?
<div className={"rounded-lg flex justify-center items-start"}>
<div className={"flex-col items-center"}>
<div className="relative">
<img
title={workDetail.input_text}
src={getCompressionImageLink(workDetail?.output_url[1])}
alt={workDetail.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
className={"rounded-lg checkerboard"}
width={400}
height={400}
/>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + workDetail.uid, detailText.h1Text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div
className="flex justify-center items-center space-x-3 px-2 py-2">
<div className="pt-2">
<button
type="submit"
onClick={() => downloadResult(workDetail?.output_url[1], 1)}
className="w-full inline-flex justify-center items-center rounded-md bg-[#ffa11b] px-3 py-2 text-xs md:text-lg font-semibold text-white shadow-sm hover:bg-[#f05011]"
>
{commonText.download} PNG
</button>
</div>
</div>
</div>
</div>
:
null
}
</div>
</div>
</div>
</div>
</div>
<h2
className={"w-[80%] mx-auto text-white pt-4 text-xl md:text-4xl flex justify-center items-center"}>{detailText.h2Text}</h2>
<div className={"w-[90%] mx-auto mb-20"}>
<div role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{similarList?.map((file, index) => (
<div key={file.input_text + index} className={"mt-6"}>
<div
className="rounded-xl flex justify-center items-start checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, detailText.h1Text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
detailText={detailText}
workDetail={workDetail}
similarList={similarList}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={exploreText.title}
description={exploreText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className={"my-auto mt-4 min-h-[90vh]"}>
<TopBlurred/>
<div className={"mb-8"}>
<h2
className={"text-white pt-4 text-3xl md:text-4xl flex justify-center items-center"}>{exploreText.h1Text}</h2>
<div className="mb-5 w-[80%] md:max-w-[628px] lg:mb-8 mx-auto mt-4">
<h1 className="text-[#7c8aaa] text-md md:text-xl">{exploreText.h2Text}</h1>
</div>
</div>
<div key={"more"} className={"px-6 py-4"}>
<Link href={getLinkHref(locale, '')}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.generateNew} {'>>'}
</Link>
</div>
<div className={"w-[90%] mx-auto mb-20"}>
<div role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{resultInfoList?.map((file, index) => (
<div key={file.input_text + index} className={"mt-6"}>
<div
className="rounded-xl flex justify-center items-start checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, file.input_text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
<div className={"flex justify-center items-center"}>
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{
pageData?.pagination?.length > 0 ?
page == 2 ?
<Link
key={2}
href={getLinkHref(locale, `stickers`)}
className="no-underline relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(page)}
>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
<Link
key={1}
href={getLinkHref(locale, `stickers`)}
className="no-underline relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(page)}
>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
null
}
{
pageData?.pagination?.map((pa, index) => {
let href;
if (pa == 1) {
href = getLinkHref(locale, `stickers`);
} else if (pa == '...') {
href = `#`;
} else {
href = getLinkHref(locale, `stickers/${pa}`);
}
if (pa == page) {
return (
<Link
key={pa}
href={href}
aria-current="page"
className="no-underline relative z-10 inline-flex items-center bg-[#de5c2d] px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={() => checkShowLoading(pa)}
>
{pa}
</Link>
)
} else {
return (
<Link
key={pa}
href={href}
className="no-underline relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(pa)}
>
{pa}
</Link>
)
}
})
}
{
pageData?.pagination?.length > 0 ?
page == pageData?.totalPage ?
<Link
key={page}
href={getLinkHref(locale, `stickers/${page}`)}
className="no-underline relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(page)}
>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
<Link
key={2}
href={getLinkHref(locale, `stickers/${Number(page) + 1}`)}
className="no-underline relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(Number(page) + 1)}
>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
null
}
</nav>
</div>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={exploreText.title}
description={exploreText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className={"my-auto mt-4 min-h-[90vh]"}>
<TopBlurred/>
<div className={"mb-8"}>
<h2
className={"text-white pt-4 text-3xl md:text-4xl flex justify-center items-center"}>{exploreText.h1Text}</h2>
<div className="mb-5 w-[80%] md:max-w-[628px] lg:mb-8 mx-auto mt-4">
<h1 className="text-[#7c8aaa] text-md md:text-xl">{exploreText.h2Text}</h1>
</div>
</div>
<div key={"more"} className={"px-6 py-4"}>
<Link href={getLinkHref(locale, ``)}
className={"flex justify-center items-center text-xl text-red-400 hover:text-blue-600"}>
{commonText.generateNew} {'>>'}
</Link>
</div>
<div className={"w-[90%] mx-auto mb-20"}>
<div role="list"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{resultInfoList?.map((file, index) => (
<div key={file.input_text + index}>
<div
className="rounded-xl flex justify-center items-start mt-6 checkerboard relative">
<Link
href={getLinkHref(locale, `sticker/${file.uid}`)}
onClick={() => setShowLoadingModal(true)}
className={"cursor-pointer"}
>
<img
src={getCompressionImageLink(file.output_url[1])}
alt={file.input_text + ' ' + process.env.NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT}
width={400}
height={400}
className={"rounded-lg"}
/>
</Link>
<Link
href={`https://pinterest.com/pin/create/button/?url=${getShareToPinterest(locale, 'sticker/' + file.uid, file.input_text)}`}
target={"_blank"}
className={"absolute top-1 left-1"}>
{pinterestSvg}
</Link>
</div>
<div className={"flex justify-center items-center"}>
<p
className="pointer-events-none mt-2 block text-sm font-medium text-white w-[90%] line-clamp-2">{getResultStrAddSticker(file.input_text, commonText.keyword)}</p>
</div>
</div>
))}
</div>
</div>
<div className={"flex justify-center items-center"}>
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{
pageData?.pagination?.length > 0 ?
page == 2 ?
<Link
key={2}
href={getLinkHref(locale, `stickers`)}
className="no-underline relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(page)}
>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
<Link
key={24}
href={getLinkHref(locale, `stickers/${Number(page) - 1}`)}
className="no-underline relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(Number(page) - 1)}
>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
null
}
{
pageData?.pagination?.map((pa, index) => {
let href;
if (pa == 1) {
href = getLinkHref(locale, `stickers`);
} else if (pa == '...') {
href = `#`;
} else {
href = getLinkHref(locale, `stickers/${pa}`);
}
if (pa == page) {
return (
<Link
key={pa}
href={href}
aria-current="page"
className="no-underline relative z-10 inline-flex items-center bg-[#de5c2d] px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={() => checkShowLoading(pa)}
>
{pa}
</Link>
)
} else {
return (
<Link
key={pa}
href={href}
className="no-underline relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(pa)}
>
{pa}
</Link>
)
}
})
}
{
pageData?.pagination?.length > 0 ?
page == pageData?.totalPage ?
<Link
key={page}
href={getLinkHref(locale, `stickers/${page}`)}
className="no-underline relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(page)}
>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
<Link
key={2}
href={getLinkHref(locale, `stickers/${Number(page) + 1}`)}
className="no-underline relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 bg-gray-100 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
onClick={() => checkShowLoading(Number(page) + 1)}
>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true"/>
</Link>
:
null
}
</nav>
</div>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
exploreText={exploreText}
resultInfoData={resultInfoData}
page={page}
pageData={pageData}
/>
)
}

View File

@ -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 (
<PageComponent
locale={locale}
exploreText={exploreText}
resultInfoData={resultInfoData}
page={1}
pageData={pageData}
/>
)
}

View File

@ -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 (
<>
<HeadInfo
locale={locale}
page={pagePath}
title={termsOfServiceText.title}
description={termsOfServiceText.description}
/>
<Header
locale={locale}
page={pagePath}
/>
<div className="mt-6 my-auto min-h-[90vh]">
<TopBlurred/>
<main className="w-[95%] md:w-[65%] lg:w-[55%] 2xl:w-[45%] mx-auto h-full my-8">
<div className="p-6 prose mx-auto text-gray-300 div-markdown-color">
<Markdown>
{termsOfServiceText.detailText}
</Markdown>
</div>
</main>
</div>
<Footer
locale={locale}
page={pagePath}
/>
</>
)
}
export default PageComponent

View File

@ -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 (
<PageComponent
locale={locale}
termsOfServiceText={termsOfServiceText}
/>
)
}

19
src/app/error.tsx Executable file
View File

@ -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 (
<div>not found</div>
);
}

63
src/app/globals.css Normal file
View File

@ -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;
}

12
src/app/layout.tsx Normal file
View File

@ -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;
}

29
src/app/not-found.tsx Executable file
View File

@ -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 (
<html lang="en">
<body>
<main className="grid min-h-full place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
<div className="text-center">
<p className="text-base font-semibold text-[#ffa11b]">404</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-gray-900 sm:text-5xl">Page not found</h1>
<p className="mt-6 text-base leading-7 text-gray-600">Sorry, we couldnt find the page youre looking for.</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href={process.env.NEXT_PUBLIC_SITE_URL}
className="rounded-md bg-[#ffa11b] px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-[#f05011] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Go back home
</a>
</div>
</div>
</main>
</body>
</html>
);
}

6
src/app/page.tsx Normal file
View File

@ -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');
}

160
src/components/Footer.tsx Normal file
View File

@ -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 (
<footer aria-labelledby="footer-heading">
<div id="footer-heading" className="sr-only">
Footer
</div>
<div className="mx-auto max-w-7xl px-6 py-4">
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
<div className="space-y-8">
<Link
href={getLinkHref(locale, '')}
>
<img
className="h-10"
src="/website.svg"
alt={process.env.NEXT_PUBLIC_DOMAIN_NAME}
/>
</Link>
<p className="text-sm text-gray-300">
{commonText.footerDescText}
</p>
</div>
<div className="mt-2 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
<div className="md:grid md:grid-cols-2 md:gap-8">
<div>
<div className="text-sm font-semibold leading-6 text-white"></div>
<ul role="list" className="mt-6 space-y-4">
</ul>
</div>
<div className="mt-10 md:mt-0">
<div className="text-sm font-semibold leading-6 text-white"></div>
<ul role="list" className="mt-6 space-y-4">
<a
className="text-sm leading-6 footer-link text-white"
target={"_blank"}
href={"https://sticker.show/"}
title={"Free Online AI Sticker Maker & Generator!"}
>
Source by Sticker.Show
</a>
</ul>
</div>
</div>
<div className="md:grid md:grid-cols-2 md:gap-8">
<div>
<div
className="text-sm font-semibold leading-6 text-white">
{process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME == '0' ? '' : menuText.footerSupport}
</div>
<ul role="list" className="mt-6 space-y-4">
{
process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' ?
<li>
<Link
href={getLinkHref(locale, 'pricing')}
className="text-sm leading-6 text-gray-300 hover:text-[#2d6ae0]"
onClick={()=>checkPageAndLoading('pricing')}
>
{menuText.footerSupport0}
</Link>
</li>
:
null
}
{
userData && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0' ?
<li>
<a
onClick={() => manageSubscribe()}
className="cursor-pointer text-sm leading-6 text-gray-300 hover:text-[#2d6ae0]">
{menuText.footerSupport1}
</a>
</li>
:
null
}
</ul>
</div>
<div className="mt-10 md:mt-0">
<div className="text-sm font-semibold leading-6 text-white">{menuText.footerLegal}</div>
<ul role="list" className="mt-6 space-y-4">
<li>
<Link
href={getLinkHref(locale, 'privacy-policy')}
className="text-sm leading-6 text-gray-300 hover:text-[#2d6ae0]"
onClick={()=>checkPageAndLoading('privacy-policy')}
>
{menuText.footerLegal0}
</Link>
</li>
<li>
<Link
href={getLinkHref(locale, 'terms-of-service')}
className="text-sm leading-6 text-gray-300 hover:text-[#2d6ae0]"
onClick={()=>checkPageAndLoading('terms-of-service')}
>
{menuText.footerLegal1}
</Link>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<>
{
process.env.NEXT_PUBLIC_GOOGLE_TAG_ID ?
<>
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_TAG_ID}/>
</>
:
null
}
</>
</footer>
)
}

View File

@ -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 (
<Transition.Root show={showGeneratingModal} as={Fragment}>
<Dialog as="div" className="relative z-40" initialFocus={cancelButtonRef} onClose={setShowGeneratingModal} onClick={() => setShowGeneratingModal(true)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl">
<div className="flex items-center justify-center">
<div
className="inline-flex items-center px-4 py-2 font-semibold leading-6 text-xl transition ease-in-out duration-150"
style={{color: '#f05011'}}>
<svg className="animate-spin -ml-1 mr-3 h-10 w-10" style={{color: '#f05011'}}
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{generatingText}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@ -0,0 +1,60 @@
import {languages} from "~/config";
const HeadInfo = ({
locale,
page,
title,
description,
}) => {
return (
<>
<title>{title}</title>
<meta name="description" content={description}/>
{
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 <link key={href} rel="alternate" hrefLang={hrefLang} href={href}/>
})
}
{
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 <link key={href + 'canonical'} rel="canonical" hrefLang={hrefLang} href={href}/>
}
})
}
</>
)
}
export default HeadInfo

291
src/components/Header.tsx Normal file
View File

@ -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 (
<header className="top-0 z-20 w-full">
<LoadingModal loadingText={commonText.loadingText}/>
<GeneratingModal generatingText={commonText.generateText}/>
<LoginModal
loadingText={commonText.loadingText}
redirectPath={pageResult}
loginModalDesc={authText.loginModalDesc}
loginModalButtonText={authText.loginModalButtonText}
/>
<LogoutModal
logoutModalDesc={authText.logoutModalDesc}
confirmButtonText={authText.confirmButtonText}
cancelButtonText={authText.cancelButtonText}
redirectPath={pageResult}
/>
<nav className="mx-auto flex items-center justify-between p-6 lg:px-8" aria-label="Global">
<div className="flex">
<Link
href={getLinkHref(locale, '')}
className="-m-1.5 ml-0.5 p-1.5"
onClick={() => checkLocalAndLoading(locale)}>
<img
className="h-8 w-auto"
src="/website.svg"
width={32}
height={24}
alt={process.env.NEXT_PUBLIC_DOMAIN_NAME}
/>
</Link>
</div>
<div className="flex lg:hidden">
<button
type="button"
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-white"
onClick={() => setMobileMenuOpen(true)}
>
<span className="sr-only">Open main menu</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
<div className="hidden lg:ml-14 lg:flex lg:flex-1 lg:gap-x-6">
<Link
href={getLinkHref(locale, '')}
onClick={() => checkPageAndLoading('')}
className="text-sm font-semibold leading-6 text-white hover:text-blue-500">
{menuText.header0}
</Link>
<Link
href={getLinkHref(locale, 'stickers')}
onClick={() => checkPageAndLoading('stickers')}
className="text-sm font-semibold leading-6 text-white hover:text-blue-500">
{menuText.header2}
</Link>
<Link
href={getLinkHref(locale, 'search')}
onClick={() => checkPageAndLoading('search')}
className="text-sm font-semibold leading-6 text-white hover:text-blue-500">
{menuText.header3}
</Link>
{
userData.email ?
<Link
href={getLinkHref(locale, 'my')}
onClick={() => checkPageAndLoading('my')}
className="text-sm font-semibold leading-6 text-white hover:text-blue-500">
{menuText.header1}
</Link>
:
null
}
</div>
<Menu as="div" className="hidden lg:relative lg:inline-block lg:text-left z-30">
<div>
<Menu.Button
className="inline-flex w-full justify-center gap-x-1.5 border border-[rgba(255,255,255,0.5)] rounded-md px-3 py-2 text-sm font-semibold text-white hover:border-[rgba(255,255,255,0.9)]">
<GlobeAltIcon className="w-5 h-5 text-white"/>{locale == 'default' ? 'EN' : locale.toUpperCase()}
<ChevronDownIcon className="-mr-1 h-5 w-5 text-white" aria-hidden="true"/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-0 z-30 mt-2 w-26 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1 z-30">
{
languages.map((item) => {
let hrefValue = `/${item.lang}`;
if (page) {
hrefValue = `/${item.lang}/${page}`;
}
return (
<Menu.Item key={item.lang}>
<Link href={hrefValue} onClick={() => checkLocalAndLoading(item.lang)} className={"z-30"}>
<span
className={'text-gray-700 block px-4 py-2 text-sm hover:text-[#2d6ae0] z-30'}
>
{item.language}
</span>
</Link>
</Menu.Item>
)
})
}
</div>
</Menu.Items>
</Transition>
</Menu>
{
process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' ?
<div className="hidden lg:ml-2 lg:relative lg:inline-block lg:text-left lg:text-white">
<LoginButton buttonType={userData.email ? 1 : 0} loginText={authText.loginText}/>
</div>
:
null
}
</nav>
<Dialog as="div" className="lg:hidden" open={mobileMenuOpen} onClose={setMobileMenuOpen}>
<div className="fixed inset-0 z-30"/>
<Dialog.Panel
className="fixed inset-y-0 right-0 z-30 w-full overflow-y-auto background-div px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
<div className="flex items-center justify-between">
<div className="flex">
<Link href={getLinkHref(locale, '')} className="-m-1.5 ml-0.5 p-1.5"
onClick={() => checkLocalAndLoading(locale)}>
<img
className="h-8 w-auto"
src="/website.svg"
width={32}
height={24}
alt={process.env.NEXT_PUBLIC_DOMAIN_NAME}
/>
</Link>
</div>
<button
type="button"
className="-m-2.5 rounded-md p-2.5 text-white z-20"
onClick={() => setMobileMenuOpen(false)}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
<div className="mt-6 flow-root">
<div className="divide-y divide-gray-500/10">
<div className="space-y-2 py-6">
<Link
href={getLinkHref(locale, '')}
onClick={() => checkPageAndLoading('')}
className="block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-white">
{menuText.header0}
</Link>
<Link
href={getLinkHref(locale, 'stickers')}
onClick={() => checkPageAndLoading('stickers')}
className="block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-white">
{menuText.header2}
</Link>
<Link
href={getLinkHref(locale, 'search')}
onClick={() => checkPageAndLoading('search')}
className="block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-white">
{menuText.header3}
</Link>
{
userData.email ?
<Link
href={getLinkHref(locale, 'my')}
onClick={() => checkPageAndLoading('my')}
className="block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-white">
{menuText.header1}
</Link>
:
null
}
</div>
<div className="ml-2 py-4">
<Menu as="div" className="relative inline-block text-left z-20">
<div>
<Menu.Button
className="inline-flex w-full justify-center gap-x-1.5 border border-[rgba(255,255,255,0.5)] rounded-md px-3 py-2 text-sm font-semibold text-white hover:border-[rgba(255,255,255,0.9)]">
<GlobeAltIcon className="w-5 h-5 text-white"/>{locale == 'default' ? 'EN' : locale.toUpperCase()}
<ChevronDownIcon className="-mr-1 h-5 w-5 text-white" aria-hidden="true"/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-0 z-10 mt-2 w-26 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{
languages.map((item) => {
let hrefValue = `/${item.lang}`;
if (page) {
hrefValue = `/${item.lang}/${page}`;
}
return (
<Menu.Item key={item.lang}>
<Link href={hrefValue} onClick={() => checkLocalAndLoading(item.lang)}>
<span
className={'text-gray-700 block px-4 py-2 text-sm hover:text-[#2d6ae0]'}
>
{item.language}
</span>
</Link>
</Menu.Item>
)
})
}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
{
process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' ?
<div
className="relative inline-block text-left text-base font-semibold text-white ml-2">
<LoginButton buttonType={userData.email ? 1 : 0} loginText={authText.loginText}/>
</div>
:
null
}
</div>
</div>
</Dialog.Panel>
</Dialog>
</header>
)
}

View File

@ -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;
}
}

View File

@ -0,0 +1,13 @@
import s from './LoadingDots.module.css';
const LoadingDots = () => {
return (
<span className={s.root}>
<span/>
<span/>
<span/>
</span>
);
};
export default LoadingDots;

View File

@ -0,0 +1 @@
export { default } from './LoadingDots';

View File

@ -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 (
<Transition.Root show={showLoadingModal} as={Fragment}>
<Dialog as="div" className="relative z-40" initialFocus={cancelButtonRef} onClose={setShowLoadingModal} onClick={() => setShowLoadingModal(true)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl">
<div className="flex items-center justify-center">
<div
className="inline-flex items-center px-4 py-2 font-semibold leading-6 text-xl transition ease-in-out duration-150"
style={{color: '#f05011'}}>
<svg className="animate-spin -ml-1 mr-3 h-10 w-10" style={{color: '#f05011'}}
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{loadingText}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@ -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 ? (
<button
className="inline-flex w-full justify-center gap-x-1.5 border border-[rgba(255,255,255,0.5)] rounded-md px-3 py-2 text-sm font-semibold hover:border-[rgba(255,255,255,0.9)]"
disabled
>
<p>Login</p>
{whiteLoadingSvg}
</button>
) :
(
<button
className="inline-flex w-full justify-center gap-x-1.5 border border-[rgba(255,255,255,0.5)] rounded-md px-3 py-2 text-sm font-semibold hover:border-[rgba(255,255,255,0.9)]"
onClick={login}
>
{loginText}
</button>
)
}
</>
)
}
{
buttonType == 1 && (
<>
{
<button
className="my-auto mx-auto mr-4 mt-1 inline-flex w-full justify-center gap-x-1.5 rounded-md text-sm font-semibold"
onClick={logout}
>
<img className="h-8 w-auto rounded-full" src={userData.image} alt=""/>
</button>
}
</>
)
}
</>
)
}
export default LoginButton

View File

@ -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 (
<Transition.Root show={showLoginModal} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={setShowLoginModal} onClick={() => setShowLoginModal(true)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<div className="mt-3 text-center sm:mt-5">
<Dialog.Title as="h3"
className="gradient-text text-3xl font-bold flex justify-center items-center">
<a className="-m-1.5 ml-0.5 p-1.5">
<img
className="h-8 w-auto"
src="/website.svg"
width={32}
height={24}
alt={process.env.NEXT_PUBLIC_DOMAIN_NAME}
/>
</a>
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{loginModalDesc}
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 space-y-3">
{
loadGoogle ? (
<button
type="button"
className={style.loginGoogleBtn}
disabled
>
{blackLoadingSvg}
<p>{loadingText}</p>
</button>
) : (
<button
type="button"
className={"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"}
onClick={async () => {
await signInUseAuth({
redirectPath: redirectPath
})
setLoadGoogle(true)
}}
>
<FcGoogle className='text-xl'/>
<p>{loginModalButtonText}</p>
</button>
)
}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default LoginModal

View File

@ -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 (
<Transition.Root show={showLogoutModal} as={Fragment}>
<Dialog as="div" className="relative z-40" initialFocus={cancelButtonRef} onClose={setShowLogoutModal} onClick={() => setShowLogoutModal(true)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-base font-semibold leading-6 text-gray-900">
{logoutModalDesc}
</Dialog.Title>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={() => confirmButton()}
>
{confirmButtonText}
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setShowLogoutModal(false)}
ref={cancelButtonRef}
>
{cancelButtonText}
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@ -0,0 +1,12 @@
import useOneTapSignin from "~/libs/useOneTapSignin";
const OneTapComponent = () => {
const { showLoadingModal: oneTapIsLoading } = useOneTapSignin({
redirect: false,
parentContainerId: "oneTap",
});
return <div id="oneTap" className="fixed top-0 right-0 " />; // This is done with tailwind. Update with system of choice
};
export default OneTapComponent;

View File

@ -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<string>();
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 (
<div className={(isPricing ? "" : "background-div") + " flex flex-col items-center"}>
<div className="max-w-screen-lg grid place-items-center bg-slate-100 text-slate-600 p-4 gap-4">
<div id="introduction" className="bg-white shadow-md rounded border-slate-200 p-5">
<div className="text-slate-800 space-y-2 mb-0">
</div>
</div>
</div>
</div>
);
return (
<section className={(isPricing ? "" : "background-div") + " bg-cover bg-center bg-no-repeat text-white"}>
<div className="mx-auto w-full max-w-7xl px-5 py-4 md:px-6 md:py-8">
<div className="flex flex-col items-center justify-start">
<div className="mx-auto mb-12 flex max-w-3xl flex-col items-center">
<h2 className="text-5xl font-bold">{pricingText.h1Text}</h2>
</div>
<div className="grid w-full grid-cols-1 gap-16 md:grid-cols-3 md:gap-4 lg:gap-8">
<div className="mx-auto flex w-full max-w-[416px] flex-col">
<div className="flex w-full flex-col items-start rounded-xl bg-[#2b306b] p-10">
<div className="mb-4 rounded-lg bg-[#0a2836] px-4 py-1.5">
<p
className="text-sm font-bold text-white">{pricingText.free}</p>
</div>
<h2 className="mt-8 mb-6 text-3xl font-bold md:mb-8 md:text-5xl lg:mb-12">{pricingText.free0}</h2>
<div
className="w-full bg-[#2b306b] px-6 py-4 text-center font-semibold text-[#2b306b] cursor-pointer inline-flex justify-center items-center text-sm leading-6 shadow-sm"
>
{pricingText.buyText}
</div>
</div>
<div className="mt-10 flex flex-col items-start gap-5">
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.freeIntro0}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="32" height="32" viewBox="0 0 32 32"
strokeWidth="1.5"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" fill="currentColor"/>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.freeIntro1}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="32" height="32" viewBox="0 0 32 32"
strokeWidth="1.5"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"
fill="currentColor"/>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.freeIntro2}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="32" height="32" viewBox="0 0 32 32"
strokeWidth="1.5"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"
fill="currentColor"/>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro4}</span>
</p>
</div>
</div>
</div>
{
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 (
<div key={price.id} className="mx-auto flex w-full max-w-[416px] flex-col">
<div className="flex w-full flex-col items-start rounded-xl bg-[#2b306b] p-10">
<div className="mb-4 rounded-lg bg-[#0a1836] px-4 py-1.5">
<p
className="text-sm font-bold text-white">{pricingText.basic}</p>
</div>
<h2
className="mb-5 text-2xl font-bold md:mb-6 md:text-4xl lg:mb-8">{priceString}/{pricingText.monthText}</h2>
<h2
className="mb-2 text-lg font-bold md:mb-3 md:text-xl lg:mb-4">{priceString} {pricingText.monthlyText}</h2>
<a
type="button"
className="w-full rounded-full bg-[#f05011] px-6 py-4 text-center font-bold text-white cursor-pointer inline-flex justify-center items-center text-sm leading-6 shadow-sm"
onClick={() => handleCheckout(price)}
>
{pricingText.buyText}
{priceIdLoading && priceIdLoading == price.id && (
<i className="flex pl-2 m-0">
<LoadingDots/>
</i>
)}
</a>
</div>
<div className="mt-10 flex flex-col items-start gap-5">
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro0}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro1}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro2}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro4}</span>
</p>
</div>
</div>
</div>
)
} else {
return (
<div key={pricePerString} className="mx-auto flex w-full max-w-[416px] flex-col">
<div
className="flex flex-col items-start rounded-xl bg-[#2d6ae0] bg-cover bg-center bg-no-repeat p-10 text-white"
style={{backgroundImage: 'https://assets.website-files.com/6502af467b2a8c4ee8159a5b/6502af467b2a8c4ee8159a8b_Mask%20group.svg'}}>
<div className="mb-4 flex flex-row flex-wrap gap-4">
<div className="flex items-center gap-1.5 rounded-lg bg-[#ffa11b] px-4 py-1.5 text-white">
<div className="flex">
<svg width="17" height="16" viewBox="0 0 17 16" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M15.2528 4.59353C15.1098 4.47414 14.9361 4.39736 14.7515 4.37194C14.567 4.34652 14.379 4.37349 14.2091 4.44977L11.0466 5.85602L9.20905 2.54353C9.12123 2.38887 8.99398 2.26026 8.84028 2.17079C8.68657 2.08132 8.5119 2.03418 8.33405 2.03418C8.1562 2.03418 7.98153 2.08132 7.82783 2.17079C7.67412 2.26026 7.54688 2.38887 7.45905 2.54353L5.62155 5.85602L2.45905 4.44977C2.28874 4.37361 2.10052 4.3466 1.91567 4.37181C1.73081 4.39701 1.5567 4.47343 1.413 4.59242C1.2693 4.71141 1.16176 4.86822 1.10252 5.04513C1.04329 5.22205 1.03473 5.412 1.0778 5.59353L2.6653 12.3623C2.69566 12.4933 2.7523 12.6168 2.8318 12.7253C2.9113 12.8338 3.012 12.9251 3.1278 12.9935C3.28458 13.0874 3.46383 13.137 3.64655 13.1373C3.73537 13.1371 3.82373 13.1245 3.90905 13.0998C6.80269 12.2998 9.85916 12.2998 12.7528 13.0998C13.017 13.1692 13.298 13.131 13.5341 12.9935C13.6506 12.926 13.7518 12.835 13.8314 12.7263C13.911 12.6177 13.9672 12.4937 13.9966 12.3623L15.5903 5.59353C15.6329 5.41195 15.6239 5.22208 15.5642 5.04537C15.5046 4.86866 15.3967 4.71215 15.2528 4.59353V4.59353Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-sm font-bold text-white">{pricingText.popularText}</p>
</div>
</div>
<h2
className="mb-2 text-2xl font-bold md:mb-3 md:text-4xl lg:mb-6">{pricePerString}/{pricingText.monthText}</h2>
<h2
className="mb-2 text-lg font-bold md:mb-3 md:text-xl lg:mb-6">{priceString} {pricingText.annuallyText} {pricingText.annuallySaveText}</h2>
<a
type="button"
className="w-full rounded-full bg-[#f05011] px-6 py-4 text-center font-bold text-white cursor-pointer inline-flex justify-center items-center text-sm leading-6 shadow-sm"
onClick={() => handleCheckout(price)}
>
{pricingText.buyText}
{priceIdLoading && priceIdLoading == price.id && (
<i className="flex pl-2 m-0">
<LoadingDots/>
</i>
)}
</a>
</div>
<div className="mt-10 flex flex-col items-start gap-5">
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro0}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro1}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro2}</span>
</p>
</div>
<div className="flex flex-row items-start text-white">
<div className="mr-2 flex text-[#2d6ae0]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"
fill="currentColor"></path>
</svg>
</div>
<p className="text-[#7c8aaa]">
<span className="font-bold text-white">{pricingText.subscriptionIntro4}</span>
</p>
</div>
{/*<div className="flex flex-row items-start text-white">*/}
{/* <div className="mr-2 flex text-[#2d6ae0]">*/}
{/* <svg width="32" height="32" viewBox="0 0 32 32" fill="none"*/}
{/* xmlns="http://www.w3.org/2000/svg">*/}
{/* <path*/}
{/* d="M8.82291 15.198C8.47774 15.199 8.1399 15.3027 7.84846 15.4969C7.55703 15.6911 7.32392 15.968 7.1761 16.2955C7.02829 16.623 6.9718 16.9878 7.01319 17.3476C7.05457 17.7074 7.19213 18.0476 7.40995 18.3287L12.0534 24.3014C12.219 24.5172 12.4312 24.6885 12.6725 24.8009C12.9137 24.9134 13.177 24.9638 13.4406 24.9479C14.0042 24.9161 14.513 24.5995 14.8375 24.079L24.4831 7.76799C24.4847 7.76528 24.4863 7.76257 24.488 7.75991C24.5785 7.614 24.5492 7.32485 24.3624 7.1432C24.3111 7.09331 24.2506 7.05499 24.1846 7.03058C24.1186 7.00618 24.0486 6.99621 23.9789 7.00129C23.9091 7.00637 23.8411 7.02639 23.7789 7.06013C23.7168 7.09386 23.662 7.14059 23.6177 7.19743C23.6142 7.2019 23.6107 7.2063 23.607 7.21064L13.8792 18.7511C13.8422 18.795 13.7973 18.8308 13.747 18.8563C13.6967 18.8818 13.6421 18.8966 13.5863 18.8998C13.5305 18.9029 13.4747 18.8944 13.4221 18.8747C13.3695 18.8551 13.3211 18.8246 13.2798 18.7852L10.0513 15.7003C9.71603 15.3776 9.27778 15.1984 8.82291 15.198Z"*/}
{/* fill="currentColor"></path>*/}
{/* </svg>*/}
{/* </div>*/}
{/* <p className="text-[#7c8aaa]">*/}
{/* <span className="font-bold text-white">{pricingText.subscriptionIntro3}</span>*/}
{/* </p>*/}
{/*</div>*/}
</div>
</div>
)
}
})
}
</div>
</div>
</div>
</section>
);
}

View File

@ -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 (
<Transition.Root show={showPricingModal} as={Fragment}>
<Dialog as="div" className="relative z-40" initialFocus={cancelButtonRef} onClose={setShowPricingModal} onClick={() => setShowPricingModal(true)}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl">
<Pricing
redirectUrl={redirectUrl}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}

View File

@ -0,0 +1,16 @@
import React from 'react'
const TopBlurred = () => {
return (
<div className="absolute bottom-auto left-0 right-0 top-0 -z-10">
<img
src="/top_blurred.png"
alt={process.env.NEXT_PUBLIC_WEBSITE_NAME}
className="relative bottom-auto mx-auto top-0 -z-10"
style={{width: '100%'}}
/>
</div>
)
}
export default TopBlurred

32
src/components/svg.tsx Normal file
View File

@ -0,0 +1,32 @@
export const whiteLoadingSvg = (
<svg aria-hidden="true" role="status" className="inline w-4 h-4 mr-3 text-white animate-spin" viewBox="0 0 100 101"
fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="#E5E7EB"/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentColor"/>
</svg>
)
export const blackLoadingSvg = (
<svg aria-hidden="true" role="status" className="inline w-4 h-4 mr-3 text-gray-200 animate-spin dark:text-gray-600"
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="#1C64F2"/>
</svg>
)
export const pinterestSvg = (
<span className="[&>svg]:h-7 [&>svg]:w-7 [&>svg]:fill-[#e60023]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
<path
d="M496 256c0 137-111 248-248 248-25.6 0-50.2-3.9-73.4-11.1 10.1-16.5 25.2-43.5 30.8-65 3-11.6 15.4-59 15.4-59 8.1 15.4 31.7 28.5 56.8 28.5 74.8 0 128.7-68.8 128.7-154.3 0-81.9-66.9-143.2-152.9-143.2-107 0-163.9 71.8-163.9 150.1 0 36.4 19.4 81.7 50.3 96.1 4.7 2.2 7.2 1.2 8.3-3.3 .8-3.4 5-20.3 6.9-28.1 .6-2.5 .3-4.7-1.7-7.1-10.1-12.5-18.3-35.3-18.3-56.6 0-54.7 41.4-107.6 112-107.6 60.9 0 103.6 41.5 103.6 100.9 0 67.1-33.9 113.6-78 113.6-24.3 0-42.6-20.1-36.7-44.8 7-29.5 20.5-61.3 20.5-82.6 0-19-10.2-34.9-31.4-34.9-24.9 0-44.9 25.7-44.9 60.2 0 22 7.4 36.8 7.4 36.8s-24.5 103.8-29 123.2c-5 21.4-3 51.6-.9 71.2C65.4 450.9 0 361.1 0 256 0 119 111 8 248 8s248 111 248 248z"/>
</svg>
</span>
)

34
src/config.ts Normal file
View File

@ -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<typeof locales>;
// 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];
}
}
}

49
src/configs/buildLink.ts Normal file
View File

@ -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));
}

10
src/configs/buildStr.ts Normal file
View File

@ -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;
}
}

260
src/configs/languageText.ts Normal file
View File

@ -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,
}
}

129
src/configs/openaiConfig.ts Normal file
View File

@ -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 "
},
];

View File

@ -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);

View File

@ -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 (
<CommonContext.Provider
value={{
userData,
setUserData,
showLoginModal,
setShowLoginModal,
showLogoutModal,
setShowLogoutModal,
showLoadingModal,
setShowLoadingModal,
showGeneratingModal,
setShowGeneratingModal,
showPricingModal,
setShowPricingModal,
commonText,
authText,
menuText,
pricingText,
}}
>
{children}
</CommonContext.Provider>
);
}
export const useCommonContext = () => useContext(CommonContext)

View File

@ -0,0 +1,7 @@
'use client'
import {SessionProvider} from "next-auth/react";
export function NextAuthProvider({children}: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

10
src/i18n.ts Executable file
View File

@ -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
}));

18
src/libs/R2.ts Normal file
View File

@ -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,
},
});

15
src/libs/db.ts Normal file
View File

@ -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;
}

138
src/libs/handle-stripe.ts Normal file
View File

@ -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,
};

17
src/libs/helpers.ts Normal file
View File

@ -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;
};

View File

@ -0,0 +1,7 @@
import {signIn} from "next-auth/react";
export async function signInUseAuth({redirectPath}) {
const result = await signIn('google', {
callbackUrl: redirectPath
})
}

25
src/libs/replicate.ts Normal file
View File

@ -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."
}
}

View File

@ -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;
}

15
src/libs/stripe.ts Normal file
View File

@ -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'
}
}
);

15
src/libs/stripeClient.ts Normal file
View File

@ -0,0 +1,15 @@
import {loadStripe, Stripe} from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>;
export const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ??
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ??
''
);
}
return stripePromise;
};

View File

@ -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<SignInOptions, "redirect" | "callbackUrl">) => {
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;

Some files were not shown because too many files have changed in this diff Show More