diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..70f541d --- /dev/null +++ b/.env.example @@ -0,0 +1,88 @@ +#-------------------------------------------------------------------------------------------------------- +# website URL +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_SITE_URL=http://localhost + +#-------------------------------------------------------------------------------------------------------- +# website name +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_WEBSITE_NAME="贴纸网站" + +#-------------------------------------------------------------------------------------------------------- +# image alt text +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT="贴纸网站" + +#-------------------------------------------------------------------------------------------------------- +# domain name +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_DOMAIN_NAME="Sticker" + +#-------------------------------------------------------------------------------------------------------- +# postgres config +#-------------------------------------------------------------------------------------------------------- +POSTGRES_URL="postgres://sticker:pass1234@127.0.0.1:5432/sticker" + +#-------------------------------------------------------------------------------------------------------- +# Google auth config +# 0 代表不检查登录,则登录相关的按钮也不展示出来,1代表要检查 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_CHECK_GOOGLE_LOGIN=0 +NEXT_PUBLIC_GOOGLE_CLIENT_ID= +GOOGLE_SECRET_ID= + +#-------------------------------------------------------------------------------------------------------- +# NEXTAUTH config create command: openssl rand -base64 32 +#-------------------------------------------------------------------------------------------------------- +NEXTAUTH_URL=http://localhost +NEXTAUTH_SECRET= + +#-------------------------------------------------------------------------------------------------------- +# Google gtag id +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_GOOGLE_TAG_ID= + +#-------------------------------------------------------------------------------------------------------- +# Update these with your Stripe credentials from https://dashboard.stripe.com/apikeys +# 0 代表不检查支付,则支付页面也不展示出来,1代表要检查 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_CHECK_AVAILABLE_TIME=0 +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +# 免费生成次数 +FREE_TIMES=2 + +#-------------------------------------------------------------------------------------------------------- +# replicate config +#-------------------------------------------------------------------------------------------------------- +# replicate 生成结束后的回调地址,本地时我才用的 ngrok,线上的话就是你的域名 +REPLICATE_WEBHOOK= +# replicate 的 API token,需要去你的 replicate 账号里面复制 https://replicate.com/account/api-tokens +REPLICATE_API_TOKEN= +# 生成贴纸的API版本 https://replicate.com/fofr/sticker-maker/versions ,最新的那个版本只生成一张图片了,下方这个版本是还会一次生成两张图片的 +REPLICATE_API_VERSION="6443cc831f51eb01333f50b757157411d7cadb6215144cc721e3688b70004ad0" + +#-------------------------------------------------------------------------------------------------------- +# cloudflare R2 config +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_STORAGE_URL= +STORAGE_DOMAIN= +R2_BUCKET= +R2_ACCOUNT_ID= +R2_ENDPOINT= +R2_TOKEN_VALUE= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= + +#-------------------------------------------------------------------------------------------------------- +# openai config +#-------------------------------------------------------------------------------------------------------- +OPENAI_API_KEY= +OPENAI_API_BASE_URL=https://openrouter.ai/api +OPENAI_API_MODEL="openai/gpt-3.5-turbo" + +#-------------------------------------------------------------------------------------------------------- +# stickers 里面每页显示多少条数据 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_PAGES_SIZE=24 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..bc64a4c --- /dev/null +++ b/.env.production @@ -0,0 +1,88 @@ +#-------------------------------------------------------------------------------------------------------- +# website URL +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_SITE_URL= + +#-------------------------------------------------------------------------------------------------------- +# website name +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_WEBSITE_NAME= + +#-------------------------------------------------------------------------------------------------------- +# image alt text +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_IMAGE_ALT_ADDITION_TEXT= + +#-------------------------------------------------------------------------------------------------------- +# domain name +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_DOMAIN_NAME= + +#-------------------------------------------------------------------------------------------------------- +# postgres config +#-------------------------------------------------------------------------------------------------------- +POSTGRES_URL= + +#-------------------------------------------------------------------------------------------------------- +# Google auth config +# 0 代表不检查登录,则登录相关的按钮也不展示出来,1代表要检查 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_CHECK_GOOGLE_LOGIN=0 +NEXT_PUBLIC_GOOGLE_CLIENT_ID= +GOOGLE_SECRET_ID= + +#-------------------------------------------------------------------------------------------------------- +# NEXTAUTH config create command: openssl rand -base64 32 +#-------------------------------------------------------------------------------------------------------- +NEXTAUTH_URL= +NEXTAUTH_SECRET= + +#-------------------------------------------------------------------------------------------------------- +# Google gtag id +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_GOOGLE_TAG_ID= + +#-------------------------------------------------------------------------------------------------------- +# Update these with your Stripe credentials from https://dashboard.stripe.com/apikeys +# 0 代表不检查支付,则支付页面也不展示出来,1代表要检查 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_CHECK_AVAILABLE_TIME=0 +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +# 免费生成次数 +FREE_TIMES=2 + +#-------------------------------------------------------------------------------------------------------- +# replicate config +#-------------------------------------------------------------------------------------------------------- +# replicate 生成结束后的回调地址,本地时我才用的 ngrok,线上的话就是你的域名 +REPLICATE_WEBHOOK= +# replicate 的 API token,需要去你的 replicate 账号里面复制 https://replicate.com/account/api-tokens +REPLICATE_API_TOKEN= +# 生成贴纸的API版本 https://replicate.com/fofr/sticker-maker/versions ,最新的那个版本只生成一张图片了,下方这个版本是还会一次生成两张图片的 +REPLICATE_API_VERSION="6443cc831f51eb01333f50b757157411d7cadb6215144cc721e3688b70004ad0" + +#-------------------------------------------------------------------------------------------------------- +# cloudflare R2 config +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_STORAGE_URL= +STORAGE_DOMAIN= +R2_BUCKET= +R2_ACCOUNT_ID= +R2_ENDPOINT= +R2_TOKEN_VALUE= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= + +#-------------------------------------------------------------------------------------------------------- +# openai config +#-------------------------------------------------------------------------------------------------------- +OPENAI_API_KEY= +OPENAI_API_BASE_URL=https://openrouter.ai/api +OPENAI_API_MODEL="openai/gpt-3.5-turbo" + +#-------------------------------------------------------------------------------------------------------- +# stickers 里面每页显示多少条数据 +#-------------------------------------------------------------------------------------------------------- +NEXT_PUBLIC_PAGES_SIZE=24 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md index a7c9ed7..ec30e27 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ # StickerShow Sticker.Show 代码,仅限哥飞的朋友们社群成员使用 + +### 1. 右上角 Fork 本项目到你自己的 github 仓库 + +### 2. Clone你自己的仓库代码到本地 + +```bash +git clone (your git url) +``` + +### 3. 安装依赖 + +```bash +cd StickerShow && yarn +#or +cd StickerShow && npm install +#or +cd StickerShow && pnpm install +``` + +### 4. 复制 .env.example 重命名为 .env.local + +修改.env.local其中的配置为你项目的配置,生产环境配置在 .env.production + +### 5. 额外的配置 + +1) 谷歌登录认证配置 👉 [Google-Auth-Help](https://github.com/SoraWebui/SoraWebui/blob/login/help/Google-Auth.md) +2) 数据库配置 👉 Any PostgreSQL +3) 目录 /sql 下有需要的数据表,创建数据库并执行这些SQL创建数据表 +4) R2的配置,为了存储生成的图片,需要去 Cloudflare 后台创建 bucket 后进行配置。项目用到了图像转换功能,通过URL来压缩图片,这个需要在 Cloudflare 的 「图像」->「转换」这里面针对具体的域名开启 +5) Stripe 价格配置在 src/configs/stripeConfig.ts ,里面具体的的配置项需要你从 stripe 后台获取。 +6) 对接 stripe 支付参考的是该项目👉 https://github.com/vercel/nextjs-subscription-payments +上边这个支付项目能单独运行,但它用的 supabase,当前项目是已经改成了支持任何 PostgreSQL 数据库的代码 + +### 6. 运行 + +```bash +yarn dev +#or +npm run dev +#or +pnpm dev +``` + +### 7. 在浏览器打开 [http://localhost](http://localhost) + + +## 注意!!! +### 1. 贴纸生成结果是通过接收 replicate 的回调,于是本地调试的时候,需要将 REPLICATE_WEBHOOK 这个配置成公网URL,可以配置 [ngrok](https://ngrok.com/) 来接收结果,本地就能处理生成的图片了 + +类似配置成:https://0123-153-121-77-112.ngrok-free.app,这个是需要你本地运行 ngrok 与 其云端服务对上后生成的,直接用这个是不会通的哈 + +线上时,REPLICATE_WEBHOOK 这个配置为你的线上域名就可以 + +### 2. 项目用到 vercel 的定时任务来将贴纸文本翻译为其他语言,线上版本时会生效,配置在 vercel.json 这个文件,设置的一分钟运行一次,免费账户有限制(应该是只能一天调用一次),查看 vercel 的文档 https://vercel.com/docs/cron-jobs + +当然,你可以删除 vercel.json 这个文件,那样就不会触发定时任务了。你可以采用别的方式来调用接口触发定时任务,比如你自己的服务器运行一个脚本定时调用接口来翻译 + +### 3. stripe 不激活的账号就是测试模式,就可以本地调通支付流程;对接支付参考的是这个项目👉 https://github.com/vercel/nextjs-subscription-payments + +### 4. 最重要的是熟悉代码且会改代码,本项目对接 stripe 支付只是简易版本对接,各种边界条件没有考虑到。 + +比如本项目是支付后就无限制使用,没有对某个价格订阅的次数做限制,比如用户重复订阅的判断等等,这些需要你自己去完善。 + +每个人想给订阅增加的限制,无法做到通用配置,需要你自己去研究代码该怎么添加。 + +目前配置的是1个月付,1个年付,多个价格也可以,只是界面样式你得自己调整一下。 + +### 5. 本项目现有代码是能够跑通全流程的,只需将所有配置都做好。 +配置项有点多,需要细心点去进行相关配置,不要遗漏。 + + +## 有任何疑问联系 Wechat: GeFei55 + diff --git a/global.d.ts b/global.d.ts new file mode 100755 index 0000000..977f992 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,3 @@ +// Use type safe message keys with `next-intl` +type Messages = typeof import('./messages/en.json'); +declare interface IntlMessages extends Messages {} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..2a82337 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,128 @@ +{ + "FileDesc": { + "lang": "en", + "language": "English", + "languageInChineseSimple": "英语" + }, + "IndexPageText": { + "title": "英文Title", + "description": "英文description", + "h1Text": "英文首页 H1", + "descriptionBelowH1Text": "英文首页 H1下方的文字" + }, + "CommonText": { + "loadingText": "Loading...", + "generateText": "Generating...", + "placeholderText": "Type or paste text here...", + "buttonText": "Generate", + "footerDescText": "英文footer左下角文字", + "timesLeft": "You can generate", + "timesRight": "times", + "download": "Download", + "result": "Result", + "moreWorks": "My Stickers", + "generateNew": "Generate New", + "displayPublic": "display public", + "similarText": "similar", + "prompt": "input prompt", + "revised": "revised prompt", + "exploreMore": "More Stickers", + "keyword": "sticker", + "searchButtonText": "Search" + }, + "MenuText": { + "header0": "Sticker Generator", + "header1": "My Stickers", + "header2": "Discover Stickers", + "header3": "Search Stickers", + "footerLegal": "Legal", + "footerLegal0": "Privacy Policy", + "footerLegal1": "Terms & Conditions", + "footerSupport": "Support", + "footerSupport0": "Pricing", + "footerSupport1": "Manage Subscribe" + }, + "AuthText": { + "loginText": "Log in", + "loginModalDesc": "Please continue by logging in", + "loginModalButtonText": "Login with Google", + "logoutModalDesc": "Are you want to log out ?", + "confirmButtonText": "Confirm", + "cancelButtonText": "Cancel" + }, + "PricingText": { + "title": "Pricing", + "description": "价格页面description", + "h1Text": "Get plan for generate", + "basic": "Basic", + "essential": "Essential", + "growth": "Growth", + "buyText": "Get started", + "popularText": "Popular", + "creditsText": "Generate times", + "creditText": "Generate", + "free": "Free", + "free0": "Free plan", + "freeText": "Current plan", + "freeIntro0": "%freeTimes% generate per month", + "freeIntro1": "Choose not to make the generated results public", + "freeIntro2": "Download images with transparent background", + "subscriptionIntro0": "Unlimited Generate", + "subscriptionIntro1": "Choose not to make the generated results public", + "subscriptionIntro2": "Download images with transparent background", + "subscriptionIntro3": "Download images in SVG format", + "subscriptionIntro4": "Image resolution increased to 1024X1024", + "monthText": "month", + "monthlyText": "billed monthly", + "annualText": "annual", + "annuallyText": "billed yearly", + "annuallySaveText": "save 50%" + }, + "PrivacyPolicyText": { + "title": "英文隐私政策 title", + "description": "英文隐私政策description", + "h1Text": "英文隐私政策 H1", + "detailText": "英文隐私政策详情 markdown 文本" + }, + "TermsOfServiceText": { + "title": "英文服务条款 title", + "description": "英文服务条款description", + "h1Text": "英文服务条款 H1", + "detailText": "英文服务条款详情 markdown 文本" + }, + "WorksText": { + "title": "英文我的页面 title", + "description": "英文我的页面 description", + "h1Text": "英文我的页面 H1", + "descriptionBelowH1Text": "英文我的页面 H1下方的文字", + "descText": "Don't have sticker", + "toContinue": "to continue." + }, + "ExploreText": { + "title": "Discover %countSticker% Free PNG Stickers for Download", + "description": "Discover over %countSticker% free stickers in PNG format for your creative projects. Easily download and add flair to social media, websites, and more!", + "h1Text": "Discover Stickers", + "descriptionBelowH1Text": "Your Go-To Free Online Custom Sticker Maker & Generator!", + "pageText": "Page %pageNumber%", + "h2Text": "Discover Our Collection of %countSticker% Free Stickers in PNG Format for Easy Download" + }, + "DetailText": { + "title": "%prompt% Sticker, Download in PNG or SVG", + "description": "Discover the %prompt% sticker, Download in PNG or SVG format now!", + "h1Text": "%prompt% Sticker", + "descriptionBelowH1Text": "Discover the %prompt% sticker, Download in PNG or SVG format now!", + "numberText": "#%detailId%", + "h2Text": "%prompt% Stickers Collection" + }, + "SearchText": { + "title": "Search %countSticker% Free PNG Stickers for Download", + "description": "Search over %countStickerAll% free stickers in PNG format for your creative projects. Easily download and add flair to social media, websites, and more!", + "h1Text": "Search Stickers", + "h2Text": "Search Our Collection of %countSticker% Free Stickers in PNG Format for Easy Download", + "titleSearch": "Search %countSticker% Free %prompt% Stickers", + "h2TextSearch": "Search Our Collection of %countSticker% Free %prompt% Stickers in PNG Format for Easy Download" + }, + "QuestionText": { + "detailText": "" + } +} diff --git a/messages/zh.json b/messages/zh.json new file mode 100644 index 0000000..e953dba --- /dev/null +++ b/messages/zh.json @@ -0,0 +1,128 @@ +{ + "FileDesc": { + "lang": "en", + "language": "简体中文", + "languageInChineseSimple": "简体中文" + }, + "IndexPageText": { + "title": "中文Title", + "description": "中文description", + "h1Text": "首页 H1", + "descriptionBelowH1Text": "首页 H1下方的文字" + }, + "CommonText": { + "loadingText": "加载中...", + "generateText": "生成中...", + "placeholderText": "在此处输入或粘贴文本...", + "buttonText": "生成", + "footerDescText": "footer左下角文字", + "timesLeft": "您可以生成", + "timesRight": "次", + "download": "下载", + "result": "结果", + "moreWorks": "我的贴纸", + "generateNew": "生成新的", + "displayPublic": "公开展示", + "similarText": "相似", + "prompt": "输入提示", + "revised": "修订提示", + "exploreMore": "更多贴纸", + "keyword": "贴纸", + "searchButtonText": "搜索" + }, + "MenuText": { + "header0": "贴纸生成器", + "header1": "我的贴纸", + "header2": "发现贴纸", + "header3": "搜索贴纸", + "footerLegal": "法律信息", + "footerLegal0": "隐私政策", + "footerLegal1": "条款和条件", + "footerSupport": "支持", + "footerSupport0": "定价", + "footerSupport1": "管理订阅" + }, + "AuthText": { + "loginText": "登录", + "loginModalDesc": "请继续登录", + "loginModalButtonText": "使用 Google 登录", + "logoutModalDesc": "您想要登出吗?", + "confirmButtonText": "确认", + "cancelButtonText": "取消" + }, + "PricingText": { + "title": "定价", + "description": "价格页面description", + "h1Text": "获取生成计划", + "basic": "基础", + "essential": "必要", + "growth": "成长", + "buyText": "开始", + "popularText": "受欢迎的", + "creditsText": "生成次数", + "creditText": "次", + "free": "免费", + "free0": "免费计划", + "freeText": "当前计划", + "freeIntro0": "每月生成 %freeTimes% 次", + "freeIntro1": "选择不公开生成结果", + "freeIntro2": "下载带有透明背景的图片", + "subscriptionIntro0": "无限生成", + "subscriptionIntro1": "选择不公开生成结果", + "subscriptionIntro2": "下载带有透明背景的图片", + "subscriptionIntro3": "以 SVG 格式下载图片", + "subscriptionIntro4": "图片分辨率提升至 1024X1024", + "monthText": "月", + "monthlyText": "按月计费", + "annualText": "年", + "annuallyText": "按年计费", + "annuallySaveText": "节省 50%" + }, + "PrivacyPolicyText": { + "title": "隐私政策 title", + "description": "隐私政策description", + "h1Text": "隐私政策 H1", + "detailText": "隐私政策详情 markdown 文本" + }, + "TermsOfServiceText": { + "title": "服务条款 title", + "description": "服务条款description", + "h1Text": "服务条款 H1", + "detailText": "服务条款详情 markdown 文本" + }, + "WorksText": { + "title": "我的页面 title", + "description": "我的页面 description", + "h1Text": "我的页面 H1", + "descriptionBelowH1Text": "我的页面 H1下方的文字", + "descText": "没有贴纸", + "toContinue": "继续。" + }, + "ExploreText": { + "title": "发现 %countSticker% 张免费 PNG 贴纸下载", + "description": "为您的创意项目发现超过 %countSticker% 张免费的 PNG 格式贴纸。轻松下载并为社交媒体、网站等增添风采!", + "h1Text": "发现页面 H1", + "descriptionBelowH1Text": "发现页面 H1下方的文字", + "pageText": "第 %pageNumber% 页", + "h2Text": "探索我们的贴纸系列,包含 %countSticker% 张免费的 PNG 格式贴纸,便于下载" + }, + "DetailText": { + "title": "%prompt% 贴纸,以 PNG 或 SVG 格式下载", + "description": "发现 %prompt% 贴纸,现在就下载 PNG 或 SVG 格式!", + "h1Text": "%prompt% 贴纸", + "descriptionBelowH1Text": "发现 %prompt% 贴纸,现在就下载 PNG 或 SVG 格式!", + "numberText": "#%detailId%", + "h2Text": "%prompt% 贴纸系列" + }, + "SearchText": { + "title": "搜索 %countSticker% 张免费 PNG 贴纸进行下载", + "description": "为您的创意项目搜索超过 %countSticker% 张免费的 %prompt% PNG 格式贴纸。轻松下载并为社交媒体、网站等增添风采!", + "h1Text": "搜索贴纸", + "h2Text": "搜索我们的 %countSticker% 张免费贴纸,支持 PNG 格式,便于下载", + "titleSearch": "搜索 %countSticker% 免费 %prompt% 贴纸", + "h2TextSearch": "搜索我们的 %countSticker% 张免费 %prompt% 贴纸,支持 PNG 格式,方便下载" + }, + "QuestionText": { + "detailText": "" + } +} diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..43efb54 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,18 @@ +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin(); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + async redirects() { + return [ + {source: '/en', destination: '/', permanent: true}, + {source: '/stickers/1', destination: '/stickers', permanent: true}, + {source: '/stickers/0', destination: '/stickers', permanent: true}, + {source: '/:locale/stickers/1', destination: '/:locale/stickers', permanent: true}, + {source: '/:locale/stickers/0', destination: '/:locale/stickers', permanent: true}, + ]; + } +}; + +export default withNextIntl(nextConfig); diff --git a/package.json b/package.json new file mode 100644 index 0000000..768bfc4 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "next-init", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 80", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@headlessui/react": "^1.7.18", + "@heroicons/react": "^2.1.1", + "@next/third-parties": "^14.1.3", + "@stripe/stripe-js": "^3.0.7", + "@tailwindcss/typography": "^0.5.10", + "ahooks": "^3.7.10", + "aws-sdk": "^2.1572.0", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "google-auth-library": "^9.6.3", + "next": "14.1.3", + "next-auth": "^4.24.6", + "next-intl": "^3.9.2", + "pg": "^8.11.3", + "react": "^18", + "react-dom": "^18", + "react-icons": "^5.0.1", + "react-markdown": "^9.0.1", + "replicate": "^0.27.1", + "stripe": "^14.19.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.3", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/appicon.svg b/public/appicon.svg new file mode 100644 index 0000000..6a1f3ff --- /dev/null +++ b/public/appicon.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..97cecde Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100755 index 0000000..601230b --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,8 @@ +User-Agent: * +Allow: * +Disallow: /api +Disallow: /api/ +Disallow: /api* +Disallow: /*/api +Disallow: /*/api/ +Disallow: /*/api* diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..7e4032e --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,124 @@ + + + + https://sticker.show/ + 2024-03-18 + daily + 1.0 + + + https://sticker.show/stickers + 2024-03-18 + daily + 1.0 + + + https://sticker.show/de/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/de/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/es/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/es/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/fr/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/fr/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/ja/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/ja/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/ko/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/ko/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/pt/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/pt/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/tw/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/tw/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/vi/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/vi/stickers + 2024-03-18 + daily + 0.9 + + + https://sticker.show/zh/ + 2024-03-18 + daily + 0.9 + + + https://sticker.show/zh/stickers + 2024-03-18 + daily + 0.9 + + + diff --git a/public/top_blurred.png b/public/top_blurred.png new file mode 100644 index 0000000..3ab0ad6 Binary files /dev/null and b/public/top_blurred.png differ diff --git a/public/website.svg b/public/website.svg new file mode 100644 index 0000000..2d96531 --- /dev/null +++ b/public/website.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sql/tables/1_user_info.sql b/sql/tables/1_user_info.sql new file mode 100644 index 0000000..94f03ca --- /dev/null +++ b/sql/tables/1_user_info.sql @@ -0,0 +1,31 @@ +-- auto-generated definition +create table user_info +( + id bigint generated by default as identity + primary key, + created_at timestamp with time zone default now() not null, + updated_at timestamp with time zone default now() not null, + user_id varchar, + name varchar, + email varchar, + image varchar, + last_login_ip varchar +); + +comment on table user_info is 'user info table'; + +comment on column user_info.id is 'sequence id'; + +comment on column user_info.created_at is 'create time'; + +comment on column user_info.updated_at is 'update time'; + +comment on column user_info.user_id is 'user uuid'; + +comment on column user_info.name is 'user name'; + +comment on column user_info.email is 'user email'; + +comment on column user_info.image is 'user avatar path'; + +comment on column user_info.last_login_ip is 'user last login ip'; diff --git a/sql/tables/2_user_available.sql b/sql/tables/2_user_available.sql new file mode 100644 index 0000000..1ee54e4 --- /dev/null +++ b/sql/tables/2_user_available.sql @@ -0,0 +1,25 @@ +-- auto-generated definition +create table user_available +( + id bigint generated by default as identity + primary key, + created_at timestamp with time zone default now() not null, + updated_at timestamp with time zone default now() not null, + user_id varchar, + stripe_customer_id varchar, + available_times integer +); + +comment on table user_available is '用户可用点数记录表'; + +comment on column user_available.id is '自增id'; + +comment on column user_available.created_at is '创建时间'; + +comment on column user_available.updated_at is '更新时间'; + +comment on column user_available.user_id is '用户id'; + +comment on column user_available.stripe_customer_id is 'stripe用户id'; + +comment on column user_available.available_times is '可用点数'; diff --git a/sql/tables/3_stripe_customers.sql b/sql/tables/3_stripe_customers.sql new file mode 100644 index 0000000..5eeb2f7 --- /dev/null +++ b/sql/tables/3_stripe_customers.sql @@ -0,0 +1,9 @@ +-- auto-generated definition +create table stripe_customers +( + user_id varchar not null + primary key, + stripe_customer_id text +); + +comment on table stripe_customers is 'stripe customers'; diff --git a/sql/tables/4_stripe_subscriptions.sql b/sql/tables/4_stripe_subscriptions.sql new file mode 100644 index 0000000..fa72974 --- /dev/null +++ b/sql/tables/4_stripe_subscriptions.sql @@ -0,0 +1,23 @@ +create type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid', 'paused'); +-- auto-generated definition +create table stripe_subscriptions +( + id text not null + primary key, + user_id varchar, + status subscription_status, + metadata jsonb, + price_id text, + quantity integer, + cancel_at_period_end boolean, + created timestamp with time zone default timezone('utc'::text, now()) not null, + current_period_start timestamp with time zone default timezone('utc'::text, now()) not null, + current_period_end timestamp with time zone default timezone('utc'::text, now()) not null, + ended_at timestamp with time zone default timezone('utc'::text, now()), + cancel_at timestamp with time zone default timezone('utc'::text, now()), + canceled_at timestamp with time zone default timezone('utc'::text, now()), + trial_start timestamp with time zone default timezone('utc'::text, now()), + trial_end timestamp with time zone default timezone('utc'::text, now()) +); + +comment on table stripe_subscriptions is 'subscriptions'; diff --git a/sql/tables/5_works.sql b/sql/tables/5_works.sql new file mode 100644 index 0000000..d9ced6c --- /dev/null +++ b/sql/tables/5_works.sql @@ -0,0 +1,47 @@ +-- auto-generated definition +create table works +( + id bigint generated by default as identity + primary key, + created_at timestamp with time zone default now() not null, + updated_at timestamp with time zone default now() not null, + uid varchar, + input_text varchar, + revised_text varchar, + output_url varchar, + is_public boolean default false, + status integer, + user_id varchar, + is_origin boolean, + origin_language varchar, + current_language varchar, + is_delete boolean default false +); + +comment on table works is 'works'; + +comment on column works.id is 'sequence id'; + +comment on column works.created_at is 'create time'; + +comment on column works.updated_at is 'update time'; + +comment on column works.uid is 'uid'; + +comment on column works.input_text is 'input_text'; + +comment on column works.revised_text is 'revised_text'; + +comment on column works.output_url is 'output_url'; + +comment on column works.is_public is 'is_public'; + +comment on column works.status is 'status'; + +comment on column works.user_id is 'user_id'; + +comment on column works.is_origin is 'is_origin'; + +comment on column works.origin_language is 'origin_language'; + +comment on column works.current_language is 'current_language'; diff --git a/sql/tables/6_key_value.sql b/sql/tables/6_key_value.sql new file mode 100644 index 0000000..f5966f7 --- /dev/null +++ b/sql/tables/6_key_value.sql @@ -0,0 +1,12 @@ +-- auto-generated definition +create table key_value +( + key varchar not null + constraint key_value_pk + primary key, + value varchar +); + +comment on column key_value.key is 'key'; + +comment on column key_value.value is 'value'; diff --git a/sql/tables/7_works_translate_task.sql b/sql/tables/7_works_translate_task.sql new file mode 100644 index 0000000..d601d43 --- /dev/null +++ b/sql/tables/7_works_translate_task.sql @@ -0,0 +1,25 @@ +-- auto-generated definition +create table works_translate_task +( + id bigint generated by default as identity + primary key, + created_at timestamp with time zone default now() not null, + updated_at timestamp with time zone default now() not null, + uid varchar, + origin_language varchar, + status varchar +); + +comment on table works_translate_task is 'works_translate_task'; + +comment on column works_translate_task.id is '自增id'; + +comment on column works_translate_task.created_at is '创建时间'; + +comment on column works_translate_task.updated_at is '更新时间'; + +comment on column works_translate_task.uid is 'uid'; + +comment on column works_translate_task.origin_language is 'origin_language'; + +comment on column works_translate_task.status is 'status: 0未翻译,1已翻译'; diff --git a/sql/tables/8_search_log.sql b/sql/tables/8_search_log.sql new file mode 100644 index 0000000..38ad544 --- /dev/null +++ b/sql/tables/8_search_log.sql @@ -0,0 +1,25 @@ +-- auto-generated definition +create table search_log +( + id bigint generated by default as identity + primary key, + created_at timestamp with time zone default now() not null, + search_text varchar, + result_count varchar, + user_agent varchar, + search_ip varchar +); + +comment on table search_log is 'search_log'; + +comment on column search_log.id is '自增id'; + +comment on column search_log.created_at is 'created_at'; + +comment on column search_log.search_text is 'search_text'; + +comment on column search_log.result_count is 'result_count'; + +comment on column search_log.user_agent is 'user_agent'; + +comment on column search_log.search_ip is 'search_ip'; diff --git a/sql/tables/9_sensitive_words.sql b/sql/tables/9_sensitive_words.sql new file mode 100644 index 0000000..396dfb7 --- /dev/null +++ b/sql/tables/9_sensitive_words.sql @@ -0,0 +1,16 @@ +-- auto-generated definition +create table sensitive_words +( + id bigint generated by default as identity + primary key, + words varchar, + level varchar +); + +comment on table sensitive_words is 'sensitive'; + +comment on column sensitive_words.id is '自增id'; + +comment on column sensitive_words.words is 'words'; + +comment on column sensitive_words.level is 'level'; diff --git a/src/app/[locale]/PageComponent.tsx b/src/app/[locale]/PageComponent.tsx new file mode 100644 index 0000000..e46c485 --- /dev/null +++ b/src/app/[locale]/PageComponent.tsx @@ -0,0 +1,397 @@ +'use client' +import HeadInfo from "~/components/HeadInfo"; +import Header from "~/components/Header"; +import Footer from "~/components/Footer"; +import {useCommonContext} from "~/context/common-context"; +import {useEffect, useRef, useState} from "react"; +import {useInterval} from "ahooks"; +import PricingModal from "~/components/PricingModal"; +import Link from "next/link"; +import {Switch} from "@headlessui/react"; +import Markdown from "react-markdown"; +import {getCompressionImageLink, getLinkHref, getShareToPinterest} from "~/configs/buildLink"; +import {useRouter} from "next/navigation"; +import {getResultStrAddSticker} from "~/configs/buildStr"; +import TopBlurred from "~/components/TopBlurred"; +import {pinterestSvg} from '~/components/svg' + +function classNames(...classes) { + return classes.filter(Boolean).join(' ') +} + +const PageComponent = ({ + locale, + indexText, + questionText, + resultInfoListInit, + searchParams, + }) => { + const router = useRouter(); + const [pagePath] = useState(""); + + const { + setShowLoadingModal, + setShowLoginModal, + setShowPricingModal, + setShowGeneratingModal, + commonText, + userData, + pricingText, + menuText + } = useCommonContext(); + const [resultInfoList, setResultInfoList] = useState(resultInfoListInit); + const [countRefresh, setCountRefresh] = useState(0); + + const useCustomEffect = (effect, deps) => { + const isInitialMount = useRef(true); + useEffect(() => { + if (process.env.NODE_ENV === 'production' || isInitialMount.current) { + isInitialMount.current = false; + return effect(); + } + }, deps); + }; + + useCustomEffect(() => { + getLocalStorage() + if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0' && process.env.NEXT_PUBLIC_CHECK_AVAILABLE_TIME != '0') { + setIntervalAvailableTimes(1000); + } + setShowLoadingModal(false); + // setIntervalLatest(10000); + return () => { + } + }, []); + + const getLocalStorage = () => { + const textStr = localStorage.getItem('textStr'); + if (textStr) { + setTextStr(textStr); + localStorage.removeItem('textStr'); + return; + } + if (searchParams?.prompt) { + setTextStr(searchParams.prompt); + } + } + + const [textStr, setTextStr] = useState(''); + + const handleSubmit = async (e: { preventDefault: () => void }) => { + e.preventDefault(); + if (!textStr) { + return; + } + if (!userData && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') { + setShowLoginModal(true); + localStorage.setItem('textStr', textStr); + return; + } + setShowGeneratingModal(true); + const requestData = { + textStr: textStr, + user_id: userData?.user_id, + is_public: isPublic + } + const responseData = await fetch(`/api/generate/handle`, { + method: 'POST', + body: JSON.stringify(requestData) + }); + const result = await responseData.json(); + if (result.status == 601 && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') { + setShowLoginModal(true); + localStorage.setItem('textStr', textStr); + return; + } + if (result.status == 602 && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') { + setShowPricingModal(true); + localStorage.setItem('textStr', textStr); + return; + } + const currentUid = result.uid; + setUid(currentUid); + if (process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') { + setIntervalAvailableTimes(1000); + } + setIntervalResultInfo(6000); + } + + const [availableTimes, setAvailableTimes] = useState({ + available_times: 0, + subscribeStatus: '0' + }); + const [resultInfo, setResultInfo] = useState({ + uid: '', + status: 0, + input_text: '', + output_url: [], + revised_text: '', + origin_language: '', + current_language: '' + }); + const [intervalAvailableTimes, setIntervalAvailableTimes] = useState(undefined); + const [uid, setUid] = useState(''); + const [intervalResultInfo, setIntervalResultInfo] = useState(undefined); + + const getResultInfo = async () => { + if (!userData?.user_id && process.env.NEXT_PUBLIC_CHECK_GOOGLE_LOGIN != '0') { + return + } + const userId = userData?.user_id; + const response = await fetch(`/api/works/getResultInfo?uid=${uid}&userId=${userId}`); + const resultInfo = await response.json(); + if (resultInfo.status == 1) { + setResultInfo(resultInfo); + router.push(getLinkHref(locale, `sticker/${resultInfo.uid}`)); + setShowGeneratingModal(false); + setIntervalResultInfo(undefined); + setIntervalAvailableTimes(1000); + } + } + useInterval(() => { + getResultInfo(); + }, intervalResultInfo); + + const getAvailableTimes = async () => { + if (!userData) { + return; + } + const userId = userData.user_id; + if (userId) { + const response = await fetch(`/api/user/getAvailableTimes?userId=${userId}`); + const availableTimes = await response.json(); + setAvailableTimes(availableTimes); + if (availableTimes.available_times >= 0) { + setIntervalAvailableTimes(undefined); + } + } + } + useInterval(() => { + getAvailableTimes(); + }, intervalAvailableTimes); + + const downloadResult = (url) => { + window.location.href = url; + } + + const [isPublic, setIsPublic] = useState(true); + + + const checkSubscribe = () => { + if (availableTimes.subscribeStatus == 'active') { + setIsPublic(!isPublic); + } else { + setShowPricingModal(true); + } + } + + const [intervalLatest, setIntervalLatest] = useState(undefined); + const getLatestList = async () => { + if (countRefresh >= 9) { + setIntervalLatest(undefined); + return; + } + const requestData = { + locale: locale + } + const response = await fetch(`/api/works/getLatestPublicResultList`, { + method: 'POST', + body: JSON.stringify(requestData) + }); + const result = await response.json(); + setCountRefresh(countRefresh + 1); + setResultInfoList(result); + } + useInterval(() => { + getLatestList(); + }, intervalLatest); + + const hasAnyKey = (obj) => { + return Object.keys(obj).length > 0; + } + + return ( + <> + { + hasAnyKey(searchParams) ? + + : + null + } + +
+ +
+ +
+
+
+

{indexText.h1Text}

+
+

{indexText.descriptionBelowH1Text}

+
+
+ +
+
+
+
+