├── .gitignore
├── .vscode
└── settings.json
├── README-zh.md
├── README.md
├── image-converter-next
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── README-zh.md
├── README.md
├── app
│ ├── BaiDuAnalytics.tsx
│ ├── GoogleAnalytics.tsx
│ ├── actions
│ │ └── upload-actions.ts
│ ├── api
│ │ └── upload
│ │ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
├── components.json
├── components
│ ├── TailwindIndicator.tsx
│ ├── ThemeToggle.tsx
│ ├── WebsiteLogo.tsx
│ ├── footer
│ │ ├── Footer.tsx
│ │ ├── FooterLinks.tsx
│ │ └── FooterProducts.tsx
│ ├── header
│ │ ├── Header.tsx
│ │ └── HeaderLinks.tsx
│ ├── home
│ │ ├── FeatureCard.tsx
│ │ └── Features.tsx
│ ├── icons
│ │ ├── expanding-arrow.tsx
│ │ ├── eye.tsx
│ │ ├── index.tsx
│ │ ├── loading-circle.tsx
│ │ ├── loading-dots.module.css
│ │ ├── loading-dots.tsx
│ │ ├── loading-spinner.module.css
│ │ ├── loading-spinner.tsx
│ │ ├── logo.tsx
│ │ ├── moon.tsx
│ │ └── sun.tsx
│ ├── social-icons
│ │ ├── icons.tsx
│ │ └── index.tsx
│ └── ui
│ │ ├── alert.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ └── input.tsx
├── config
│ └── site.ts
├── gtag.js
├── lib
│ ├── s3-client.ts
│ └── utils.ts
├── next-env.d.ts
├── next-sitemap.config.js
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│ ├── favicon.ico
│ ├── logo.png
│ ├── logo.svg
│ ├── og.png
│ ├── placeholder.svg
│ ├── screenshot-1.png
│ └── screenshot-2.png
├── styles
│ ├── globals.css
│ └── loading.css
├── tailwind.config.ts
├── tsconfig.json
└── types
│ └── siteConfig.ts
├── image-converter-worker
├── package.json
├── pnpm-lock.yaml
├── worker.js
└── wrangler.toml
├── screenshot-1.png
└── screenshot-2.png
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Node template
2 | .idea
3 | .DS_Store
4 | dist
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | .temp
14 | yarn.lock
15 |
16 | # Diagnostic reports (https://nodejs.org/api/report.html)
17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Directory for instrumented libs generated by jscoverage/JSCover
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 | *.lcov
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (https://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # Snowpack dependency directory (https://snowpack.dev/)
52 | web_modules/
53 |
54 | # TypeScript cache
55 | *.tsbuildinfo
56 |
57 | # Optional npm cache directory
58 | .npm
59 |
60 | # Optional eslint cache
61 | .eslintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variables file
79 | .env
80 | .env.local
81 | .env.test
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 | .parcel-cache
86 |
87 | # Next.js build output
88 | .next
89 | out
90 |
91 | # Nuxt.js build / generate output
92 | .nuxt
93 | dist
94 |
95 | # Gatsby files
96 | .cache/
97 | # Comment in the assets line in if your project uses Gatsby and not Next.js
98 | # https://nextjs.org/blog/next-9-1#public-directory-support
99 | # assets
100 |
101 | # vuepress build output
102 | .vuepress/dist
103 |
104 | # Serverless directories
105 | .serverless/
106 |
107 | # FuseBox cache
108 | .fusebox/
109 |
110 | # DynamoDB Local files
111 | .dynamodb/
112 |
113 | # TernJS port file
114 | .tern-port
115 |
116 | # Stores VSCode versions used for testing VSCode extensions
117 | .vscode-test
118 |
119 | # yarn v2
120 | .yarn/cache
121 | .yarn/unplugged
122 | .yarn/build-state.yml
123 | .yarn/install-state.gz
124 | .pnp.*
125 |
126 | /.vuepress/dist/
127 |
128 | # sitemap
129 | */sitemap*.xml
130 | */robots.txt
131 |
132 | # Bun
133 | bun.lockb
134 | .bun
135 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.validate": false,
3 | "editor.formatOnSave": true,
4 | "editor.tabSize": 2,
5 | "editor.codeActionsOnSave": {
6 | "source.fixAll.eslint": "explicit",
7 | "source.organizeImports": "explicit"
8 | },
9 | "headwind.runOnSave": false,
10 | "typescript.preferences.importModuleSpecifier": "non-relative",
11 | "eslint.validate": ["javascript", "javascriptreact", "typescript"],
12 | "typescript.tsdk": "node_modules/typescript/lib",
13 | "commentTranslate.source": "Bing",
14 | "cSpell.words": [
15 | "contentlayer",
16 | "lemonsqueezy"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # Image URL Converter
2 |
3 | 这是一个简单的工具,可以将任意图片URL转换为托管在 Cloudflare R2 上的永久链接。适合需要图片托管服务的个人或小型项目使用。
4 |
5 | ## 📦 项目结构
6 |
7 | ```
8 | /image-url-converter
9 | ├── /image-converter-next # Next.js 前端项目
10 | └── /image-converter-worker # Cloudflare Worker 项目
11 | ```
12 |
13 | ## 📌 功能特点
14 |
15 | - 简单易用:只需输入原始图片URL,即可获得永久链接
16 | - 全球加速:使用 Cloudflare CDN,访问速度快
17 | - 免费使用:利用 Cloudflare R2 的免费额度
18 | - 每月 10GB 存储空间
19 | - 每月 10GB 出站流量
20 | - 支持自定义域名
21 | - 完全免费部署
22 |
23 | ## 📚 使用指南
24 |
25 | ### 1. 准备工作
26 |
27 |
28 | 1. 注册 [Cloudflare](https://dash.cloudflare.com/sign-up) 账号
29 | 2. 安装 [Node.js](https://nodejs.org/) (版本 18.0.0 或更高)
30 | 3. 安装 [pnpm](https://pnpm.io/)
31 |
32 | ### 2. 配置 Cloudflare R2
33 |
34 | 1. 登录 [Cloudflare 控制台](https://dash.cloudflare.com)
35 | 2. 在左侧菜单找到并点击 "R2"
36 | 3. 如果是首次使用,会提示创建结算账号,按提示完成即可(不会收费)
37 | 4. 点击 "Create bucket" 创建存储桶
38 | - Bucket name 填写:`images`(或你喜欢的名字)
39 | - 点击 "Create bucket" 完成创建
40 | 5. 在存储桶列表中点击刚创建的存储桶
41 | 6. 点击 "Settings" 标签
42 | 7. 找到 "Public access" 部分
43 | - 开启 "Public bucket" 开关
44 | - 如果有自己的域名,可以在下方设置自定义域名(比如:images.your-domain.com)
45 | - 如果没有自己的域名,复制 "Public bucket URL" 备用
46 | 8. 创建 API 令牌
47 | - 点击右上角的 "Manage R2 API Tokens"
48 | - 点击 "Create API token"
49 | - 权限选择:Object Read & Write
50 | - 点击 "Create token"
51 | - 保存显示的信息:
52 | * Access Key ID
53 | * Secret Access Key
54 |
55 | 注意:Secret Access Key 只显示一次,请务必保存!
56 |
57 | ## 3. 本地使用步骤
58 |
59 | 1. [Fork](https://github.com/weijunext/image-url-converter/fork) 这个项目到你的 GitHub 账号,然后克隆到本地
60 |
61 |
62 | ```bash
63 | git clone [repository-url]
64 | cd image-url-converter
65 | ```
66 |
67 | ### 2. 配置并运行 Worker
68 |
69 | ```bash
70 | # 进入 Worker 项目目录
71 | cd image-converter-worker
72 |
73 | # 安装依赖
74 | npm install
75 |
76 | # 安装 wrangler
77 | npm install -g wrangler
78 |
79 | # 登录到 Cloudflare
80 | wrangler login
81 |
82 | # 部署 Worker
83 | wrangler deploy
84 | ```
85 |
86 | ### 3. 配置并运行 Next.js 应用
87 |
88 | ```bash
89 | # 回到项目根目录
90 | cd ..
91 |
92 | # 进入 Next.js 项目目录
93 | cd image-converter-next
94 |
95 | # 安装依赖
96 | npm install
97 |
98 | # 创建环境变量文件
99 | cp .env.example .env.local
100 | ```
101 |
102 | 编辑 `.env.local` 文件,填入以下信息:
103 | ```
104 | R2_ACCOUNT_ID=你的Cloudflare账号ID(在Cloudflare主页右侧可以找到)
105 | R2_ACCESS_KEY_ID=你的R2 Access Key ID
106 | R2_SECRET_ACCESS_KEY=你的R2 Secret Access Key
107 | R2_BUCKET_NAME=你的存储桶名称(例如:images)
108 | R2_PUBLIC_URL=你的Public bucket URL
109 | ```
110 |
111 | ### 4. 运行开发服务器:
112 | ```bash
113 | npm run dev
114 | ```
115 |
116 | 现在可以访问 http://localhost:3000 使用工具了。
117 |
118 | ## ⚙️ 使用方法
119 |
120 | 1. 确保 Next.js 开发服务器正在运行
121 | 2. 打开浏览器访问 http://localhost:3000
122 | 3. 在输入框中粘贴图片 URL
123 | 4. 点击"转换"按钮
124 | 5. 等待处理完成,获取新的永久链接
125 |
126 | ## ❓ 常见问题
127 |
128 | Q:免费额度够用吗?
129 | A:对于个人使用来说绰绰有余。每月 10GB 存储和 10GB 流量,可以存储数千张图片。
130 |
131 | Q:上传的图片会过期吗?
132 | A:不会。只要你的 Cloudflare 账号正常使用,图片就会一直保存。
133 |
134 | Q:上传速度慢怎么办?
135 | A:图片上传速度主要取决于原始图片所在服务器的响应速度。建议选择稳定的图片源。
136 |
137 | Q:支持哪些图片格式?
138 | A:支持所有常见的图片格式,包括 JPG、PNG、GIF、WebP 等。
139 |
140 | ## 🔔 注意事项
141 |
142 | 1. 请确保你要转换的图片URL是可以公开访问的
143 | 2. 建议定期检查 R2 的使用量,避免超出免费额度
144 | 3. 请勿上传违规或违法的图片内容
145 |
146 | ## ☎️ 技术支持
147 |
148 | 如果遇到问题:
149 | 1. 可以在 GitHub Issues 中提问
150 | 2. 可以查看 [Cloudflare R2 文档](https://developers.cloudflare.com/r2/)
151 | 3. 可以访问 [Cloudflare 帮助中心](https://support.cloudflare.com/)
152 |
153 | ## 📜 许可证
154 |
155 | MIT License
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Image URL Converter
2 |
3 | A simple tool that converts any image URL into a permanent link hosted on Cloudflare R2. Perfect for individuals or small projects needing image hosting services.
4 |
5 | ## 📦 Project Structure
6 |
7 | ```
8 | /image-url-converter
9 | ├── /image-converter-next # Next.js frontend project
10 | └── /image-converter-worker # Cloudflare Worker project
11 | ```
12 |
13 | 
14 |
15 | ## 📌 Features
16 |
17 | - Easy to use: Simply input the original image URL to get a permanent link
18 | - Global acceleration: Fast access via Cloudflare CDN
19 | - Free to use: Leverages Cloudflare R2's free tier
20 | - 10GB storage per month
21 | - 10GB egress traffic per month
22 | - Custom domain support
23 | - Free deployment
24 |
25 | 
26 |
27 | ## 📚 Setup Guide
28 |
29 | ### 1. Prerequisites
30 |
31 | 1. Sign up for a [Cloudflare](https://dash.cloudflare.com/sign-up) account
32 | 2. Install [Node.js](https://nodejs.org/) (version 18.0.0 or higher)
33 | 3. Install [pnpm](https://pnpm.io/)
34 |
35 | ### 2. Configure Cloudflare R2
36 |
37 | 1. Log in to the [Cloudflare Dashboard](https://dash.cloudflare.com)
38 | 2. Find and click "R2" in the left sidebar
39 | 3. If it's your first time, you'll be prompted to create a billing account (it's free)
40 | 4. Click "Create bucket"
41 | - Set Bucket name to: `images` (or any name you prefer)
42 | - Click "Create bucket" to finish
43 | 5. Click on your newly created bucket in the bucket list
44 | 6. Go to the "Settings" tab
45 | 7. Find the "Public access" section
46 | - Enable the "Public bucket" toggle
47 | - If you have your own domain, you can set up a custom domain below (e.g., images.your-domain.com)
48 | - If not, copy the "Public bucket URL" for later use
49 | 8. Create an API token
50 | - Click "Manage R2 API Tokens" in the top right
51 | - Click "Create API token"
52 | - Select Object Read & Write permissions
53 | - Click "Create token"
54 | - Save the following information:
55 | * Access Key ID
56 | * Secret Access Key
57 |
58 | Note: The Secret Access Key is shown only once, make sure to save it!
59 |
60 | ### 3. Local Setup
61 |
62 | 1. [Fork](https://github.com/weijunext/image-url-converter/fork) this project to your GitHub account and clone it locally
63 |
64 | ```bash
65 | git clone [repository-url]
66 | cd image-url-converter
67 | ```
68 |
69 | ### 4. Configure and Run the Worker
70 |
71 | ```bash
72 | # Navigate to the Worker project directory
73 | cd image-converter-worker
74 |
75 | # Install dependencies
76 | npm install
77 |
78 | # Install wrangler
79 | npm install -g wrangler
80 |
81 | # Login to Cloudflare
82 | wrangler login
83 |
84 | # Deploy the Worker
85 | wrangler deploy
86 | ```
87 |
88 | ### 5. Configure and Run the Next.js App
89 |
90 | ```bash
91 | # Return to project root
92 | cd ..
93 |
94 | # Navigate to Next.js project directory
95 | cd image-converter-next
96 |
97 | # Install dependencies
98 | npm install
99 |
100 | # Create environment variables file
101 | cp .env.example .env.local
102 | ```
103 |
104 | Edit `.env.local` with your details:
105 | ```
106 | R2_ACCOUNT_ID=Your Cloudflare Account ID (found on the right side of Cloudflare dashboard)
107 | R2_ACCESS_KEY_ID=Your R2 Access Key ID
108 | R2_SECRET_ACCESS_KEY=Your R2 Secret Access Key
109 | R2_BUCKET_NAME=Your bucket name (e.g., images)
110 | R2_PUBLIC_URL=Your Public bucket URL
111 | ```
112 |
113 | ### 6. Start the Development Server:
114 | ```bash
115 | npm run dev
116 | ```
117 |
118 | You can now access the tool at http://localhost:3000
119 |
120 | ## ⚙️ How to Use
121 |
122 | 1. Ensure the Next.js development server is running
123 | 2. Open your browser and visit http://localhost:3000
124 | 3. Paste an image URL into the input field
125 | 4. Click the "Convert" button
126 | 5. Wait for processing to complete and get your new permanent link
127 |
128 | ## ❓ FAQ
129 |
130 | Q: Is the free tier sufficient?
131 | A: It's more than enough for personal use. 10GB storage and 10GB bandwidth monthly can handle thousands of images.
132 |
133 | Q: Do uploaded images expire?
134 | A: No. Images remain stored as long as your Cloudflare account is active.
135 |
136 | Q: What about slow upload speeds?
137 | A: Upload speed primarily depends on the response time of the original image server. Use stable image sources for best results.
138 |
139 | Q: What image formats are supported?
140 | A: All common image formats including JPG, PNG, GIF, WebP, and more.
141 |
142 | ## 🔔 Important Notes
143 |
144 | 1. Ensure the image URLs you want to convert are publicly accessible
145 | 2. Regularly monitor your R2 usage to stay within the free tier limits
146 | 3. Do not upload inappropriate or illegal image content
147 |
148 | ## ☎️ Support
149 |
150 | If you need help:
151 | 1. Create an issue on GitHub
152 | 2. Check the [Cloudflare R2 documentation](https://developers.cloudflare.com/r2/)
153 | 3. Visit the [Cloudflare Help Center](https://support.cloudflare.com/)
154 |
155 | ## 📜 License
156 |
157 | MIT License
--------------------------------------------------------------------------------
/image-converter-next/.env.example:
--------------------------------------------------------------------------------
1 | R2_ACCOUNT_ID=Your Cloudflare Account ID (can be found on the right side of Cloudflare dashboard)
2 | R2_ACCESS_KEY_ID=Previously saved Access Key ID
3 | R2_SECRET_ACCESS_KEY=Previously saved Secret Access Key
4 | R2_BUCKET_NAME=Your created bucket name (e.g., images)
5 | R2_PUBLIC_URL=Your Public bucket URL or custom domain URL
--------------------------------------------------------------------------------
/image-converter-next/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/image-converter-next/.gitignore:
--------------------------------------------------------------------------------
1 | ### Node template
2 | .idea
3 | .DS_Store
4 | dist
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | .temp
14 | yarn.lock
15 |
16 | # Diagnostic reports (https://nodejs.org/api/report.html)
17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Directory for instrumented libs generated by jscoverage/JSCover
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 | *.lcov
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (https://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # Snowpack dependency directory (https://snowpack.dev/)
52 | web_modules/
53 |
54 | # TypeScript cache
55 | *.tsbuildinfo
56 |
57 | # Optional npm cache directory
58 | .npm
59 |
60 | # Optional eslint cache
61 | .eslintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variables file
79 | .env
80 | .env.local
81 | .env.test
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 | .parcel-cache
86 |
87 | # Next.js build output
88 | .next
89 | out
90 |
91 | # Nuxt.js build / generate output
92 | .nuxt
93 | dist
94 |
95 | # Gatsby files
96 | .cache/
97 | # Comment in the assets line in if your project uses Gatsby and not Next.js
98 | # https://nextjs.org/blog/next-9-1#public-directory-support
99 | # assets
100 |
101 | # vuepress build output
102 | .vuepress/dist
103 |
104 | # Serverless directories
105 | .serverless/
106 |
107 | # FuseBox cache
108 | .fusebox/
109 |
110 | # DynamoDB Local files
111 | .dynamodb/
112 |
113 | # TernJS port file
114 | .tern-port
115 |
116 | # Stores VSCode versions used for testing VSCode extensions
117 | .vscode-test
118 |
119 | # yarn v2
120 | .yarn/cache
121 | .yarn/unplugged
122 | .yarn/build-state.yml
123 | .yarn/install-state.gz
124 | .pnp.*
125 |
126 | /.vuepress/dist/
127 |
128 | # sitemap
129 | */sitemap*.xml
130 | */robots.txt
--------------------------------------------------------------------------------
/image-converter-next/.npmrc:
--------------------------------------------------------------------------------
1 | # if use pnpm
2 | enable-pre-post-scripts=true
3 | public-hoist-pattern[]=*@nextui-org/*
4 | registry=https://registry.npmmirror.com/
5 |
--------------------------------------------------------------------------------
/image-converter-next/README-zh.md:
--------------------------------------------------------------------------------
1 | # Image URL Converter
2 |
3 | 这是一个简单的工具,可以将任意图片URL转换为托管在 Cloudflare R2 上的永久链接。适合需要图片托管服务的个人或小型项目使用。
4 |
5 | ## 项目结构
6 |
7 | ```
8 | /image-url-converter
9 | ├── /image-converter-next # Next.js 前端项目
10 | └── /image-converter-worker # Cloudflare Worker 项目
11 | ```
12 |
13 | ## 功能特点
14 |
15 | - 简单易用:只需输入原始图片URL,即可获得永久链接
16 | - 全球加速:使用 Cloudflare CDN,访问速度快
17 | - 免费使用:利用 Cloudflare R2 的免费额度
18 | - 每月 10GB 存储空间
19 | - 每月 10GB 出站流量
20 | - 支持自定义域名
21 | - 完全免费部署
22 |
23 | ## 使用步骤
24 |
25 | ### 1. 准备工作
26 |
27 |
28 | 1. 注册 [Cloudflare](https://dash.cloudflare.com/sign-up) 账号
29 | 2. 安装 [Node.js](https://nodejs.org/) (版本 18.0.0 或更高)
30 | 3. 安装 [pnpm](https://pnpm.io/)
31 |
32 | ### 2. 配置 Cloudflare R2
33 |
34 | 1. 登录 [Cloudflare 控制台](https://dash.cloudflare.com)
35 | 2. 在左侧菜单找到并点击 "R2"
36 | 3. 如果是首次使用,会提示创建结算账号,按提示完成即可(不会收费)
37 | 4. 点击 "Create bucket" 创建存储桶
38 | - Bucket name 填写:`images`(或你喜欢的名字)
39 | - 点击 "Create bucket" 完成创建
40 | 5. 在存储桶列表中点击刚创建的存储桶
41 | 6. 点击 "Settings" 标签
42 | 7. 找到 "Public access" 部分
43 | - 开启 "Public bucket" 开关
44 | - 如果有自己的域名,可以在下方设置自定义域名(比如:images.your-domain.com)
45 | - 如果没有自己的域名,复制 "Public bucket URL" 备用
46 | 8. 创建 API 令牌
47 | - 点击右上角的 "Manage R2 API Tokens"
48 | - 点击 "Create API token"
49 | - 权限选择:Object Read & Write
50 | - 点击 "Create token"
51 | - 保存显示的信息:
52 | * Access Key ID
53 | * Secret Access Key
54 |
55 | 注意:Secret Access Key 只显示一次,请务必保存!
56 |
57 | ## 3. 本地使用步骤
58 |
59 | 1. [Fork](https://github.com/weijunext/image-url-converter/fork) 这个项目到你的 GitHub 账号,然后克隆到本地
60 |
61 |
62 | ```bash
63 | git clone [repository-url]
64 | cd image-url-converter
65 | ```
66 |
67 | ### 2. 配置并运行 Worker
68 |
69 | ```bash
70 | # 进入 Worker 项目目录
71 | cd image-converter-worker
72 |
73 | # 安装依赖
74 | npm install
75 |
76 | # 安装 wrangler
77 | npm install -g wrangler
78 |
79 | # 登录到 Cloudflare
80 | wrangler login
81 |
82 | # 部署 Worker
83 | wrangler deploy
84 | ```
85 |
86 | ### 3. 配置并运行 Next.js 应用
87 |
88 | ```bash
89 | # 回到项目根目录
90 | cd ..
91 |
92 | # 进入 Next.js 项目目录
93 | cd image-converter-next
94 |
95 | # 安装依赖
96 | npm install
97 |
98 | # 创建环境变量文件
99 | cp .env.example .env.local
100 | ```
101 |
102 | 编辑 `.env.local` 文件,填入以下信息:
103 | ```
104 | R2_ACCOUNT_ID=你的Cloudflare账号ID(在Cloudflare主页右侧可以找到)
105 | R2_ACCESS_KEY_ID=你的R2 Access Key ID
106 | R2_SECRET_ACCESS_KEY=你的R2 Secret Access Key
107 | R2_BUCKET_NAME=你的存储桶名称(例如:images)
108 | R2_PUBLIC_URL=你的Public bucket URL
109 | ```
110 |
111 | ### 4. 运行开发服务器:
112 | ```bash
113 | npm run dev
114 | ```
115 |
116 | 现在可以访问 http://localhost:3000 使用工具了。
117 |
118 | ## 使用方法
119 |
120 | 1. 确保 Next.js 开发服务器正在运行
121 | 2. 打开浏览器访问 http://localhost:3000
122 | 3. 在输入框中粘贴图片 URL
123 | 4. 点击"转换"按钮
124 | 5. 等待处理完成,获取新的永久链接
125 |
126 | ## 常见问题
127 |
128 | Q:免费额度够用吗?
129 | A:对于个人使用来说绰绰有余。每月 10GB 存储和 10GB 流量,可以存储数千张图片。
130 |
131 | Q:上传的图片会过期吗?
132 | A:不会。只要你的 Cloudflare 账号正常使用,图片就会一直保存。
133 |
134 | Q:上传速度慢怎么办?
135 | A:图片上传速度主要取决于原始图片所在服务器的响应速度。建议选择稳定的图片源。
136 |
137 | Q:支持哪些图片格式?
138 | A:支持所有常见的图片格式,包括 JPG、PNG、GIF、WebP 等。
139 |
140 | ## 注意事项
141 |
142 | 1. 请确保你要转换的图片URL是可以公开访问的
143 | 2. 建议定期检查 R2 的使用量,避免超出免费额度
144 | 3. 请勿上传违规或违法的图片内容
145 |
146 | ## 技术支持
147 |
148 | 如果遇到问题:
149 | 1. 可以在 GitHub Issues 中提问
150 | 2. 可以查看 [Cloudflare R2 文档](https://developers.cloudflare.com/r2/)
151 | 3. 可以访问 [Cloudflare 帮助中心](https://support.cloudflare.com/)
152 |
153 | ## 许可证
154 |
155 | MIT License
--------------------------------------------------------------------------------
/image-converter-next/README.md:
--------------------------------------------------------------------------------
1 | # Image URL Converter
2 |
3 | A simple tool that converts any image URL into a permanent link hosted on Cloudflare R2. Perfect for individuals or small projects needing image hosting services.
4 |
5 | ## Project Structure
6 |
7 | ```
8 | /image-url-converter
9 | ├── /image-converter-next # Next.js frontend project
10 | └── /image-converter-worker # Cloudflare Worker project
11 | ```
12 |
13 | ## Features
14 |
15 | - Easy to use: Simply input the original image URL to get a permanent link
16 | - Global acceleration: Fast access via Cloudflare CDN
17 | - Free to use: Leverages Cloudflare R2's free tier
18 | - 10GB storage per month
19 | - 10GB egress traffic per month
20 | - Custom domain support
21 | - Free deployment
22 |
23 | ## Setup Guide
24 |
25 | ### 1. Prerequisites
26 |
27 | 1. Sign up for a [Cloudflare](https://dash.cloudflare.com/sign-up) account
28 | 2. Install [Node.js](https://nodejs.org/) (version 18.0.0 or higher)
29 | 3. Install [pnpm](https://pnpm.io/)
30 |
31 | ### 2. Configure Cloudflare R2
32 |
33 | 1. Log in to the [Cloudflare Dashboard](https://dash.cloudflare.com)
34 | 2. Find and click "R2" in the left sidebar
35 | 3. If it's your first time, you'll be prompted to create a billing account (it's free)
36 | 4. Click "Create bucket"
37 | - Set Bucket name to: `images` (or any name you prefer)
38 | - Click "Create bucket" to finish
39 | 5. Click on your newly created bucket in the bucket list
40 | 6. Go to the "Settings" tab
41 | 7. Find the "Public access" section
42 | - Enable the "Public bucket" toggle
43 | - If you have your own domain, you can set up a custom domain below (e.g., images.your-domain.com)
44 | - If not, copy the "Public bucket URL" for later use
45 | 8. Create an API token
46 | - Click "Manage R2 API Tokens" in the top right
47 | - Click "Create API token"
48 | - Select Object Read & Write permissions
49 | - Click "Create token"
50 | - Save the following information:
51 | * Access Key ID
52 | * Secret Access Key
53 |
54 | Note: The Secret Access Key is shown only once, make sure to save it!
55 |
56 | ### 3. Local Setup
57 |
58 | 1. [Fork](https://github.com/weijunext/image-url-converter/fork) this project to your GitHub account and clone it locally
59 |
60 | ```bash
61 | git clone [repository-url]
62 | cd image-url-converter
63 | ```
64 |
65 | ### 4. Configure and Run the Worker
66 |
67 | ```bash
68 | # Navigate to the Worker project directory
69 | cd image-converter-worker
70 |
71 | # Install dependencies
72 | npm install
73 |
74 | # Install wrangler
75 | npm install -g wrangler
76 |
77 | # Login to Cloudflare
78 | wrangler login
79 |
80 | # Deploy the Worker
81 | wrangler deploy
82 | ```
83 |
84 | ### 5. Configure and Run the Next.js App
85 |
86 | ```bash
87 | # Return to project root
88 | cd ..
89 |
90 | # Navigate to Next.js project directory
91 | cd image-converter-next
92 |
93 | # Install dependencies
94 | npm install
95 |
96 | # Create environment variables file
97 | cp .env.example .env.local
98 | ```
99 |
100 | Edit `.env.local` with your details:
101 | ```
102 | R2_ACCOUNT_ID=Your Cloudflare Account ID (found on the right side of Cloudflare dashboard)
103 | R2_ACCESS_KEY_ID=Your R2 Access Key ID
104 | R2_SECRET_ACCESS_KEY=Your R2 Secret Access Key
105 | R2_BUCKET_NAME=Your bucket name (e.g., images)
106 | R2_PUBLIC_URL=Your Public bucket URL
107 | ```
108 |
109 | ### 6. Start the Development Server:
110 | ```bash
111 | npm run dev
112 | ```
113 |
114 | You can now access the tool at http://localhost:3000
115 |
116 | ## How to Use
117 |
118 | 1. Ensure the Next.js development server is running
119 | 2. Open your browser and visit http://localhost:3000
120 | 3. Paste an image URL into the input field
121 | 4. Click the "Convert" button
122 | 5. Wait for processing to complete and get your new permanent link
123 |
124 | ## FAQ
125 |
126 | Q: Is the free tier sufficient?
127 | A: It's more than enough for personal use. 10GB storage and 10GB bandwidth monthly can handle thousands of images.
128 |
129 | Q: Do uploaded images expire?
130 | A: No. Images remain stored as long as your Cloudflare account is active.
131 |
132 | Q: What about slow upload speeds?
133 | A: Upload speed primarily depends on the response time of the original image server. Use stable image sources for best results.
134 |
135 | Q: What image formats are supported?
136 | A: All common image formats including JPG, PNG, GIF, WebP, and more.
137 |
138 | ## Important Notes
139 |
140 | 1. Ensure the image URLs you want to convert are publicly accessible
141 | 2. Regularly monitor your R2 usage to stay within the free tier limits
142 | 3. Do not upload inappropriate or illegal image content
143 |
144 | ## Support
145 |
146 | If you need help:
147 | 1. Create an issue on GitHub
148 | 2. Check the [Cloudflare R2 documentation](https://developers.cloudflare.com/r2/)
149 | 3. Visit the [Cloudflare Help Center](https://support.cloudflare.com/)
150 |
151 | ## License
152 |
153 | MIT License
--------------------------------------------------------------------------------
/image-converter-next/app/BaiDuAnalytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Script from "next/script";
4 |
5 | const BaiDuAnalytics = () => {
6 | return (
7 | <>
8 | {process.env.NEXT_PUBLIC_BAIDU_TONGJI ? (
9 | <>
10 |
25 | >
26 | ) : (
27 | <>>
28 | )}
29 | >
30 | );
31 | };
32 |
33 | export default BaiDuAnalytics;
34 |
--------------------------------------------------------------------------------
/image-converter-next/app/GoogleAnalytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Script from "next/script";
4 | import * as gtag from "../gtag.js";
5 |
6 | const GoogleAnalytics = () => {
7 | return (
8 | <>
9 | {gtag.GA_TRACKING_ID ? (
10 | <>
11 |
15 |
29 | >
30 | ) : (
31 | <>>
32 | )}
33 | >
34 | );
35 | };
36 |
37 | export default GoogleAnalytics;
38 |
--------------------------------------------------------------------------------
/image-converter-next/app/actions/upload-actions.ts:
--------------------------------------------------------------------------------
1 | import { R2 } from "@/lib/s3-client";
2 | import { HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
3 | import axios from "axios";
4 | import crypto from "crypto";
5 | import heicConvert from "heic-convert";
6 |
7 | export async function generateFileName(imageData: ArrayBuffer | Buffer, extension: string) {
8 | // Convert ArrayBuffer to Buffer if needed
9 | const buffer = Buffer.isBuffer(imageData) ? imageData : Buffer.from(imageData);
10 |
11 | // Create SHA-256 hash from the actual image data
12 | const hash = crypto
13 | .createHash("sha256")
14 | .update(buffer)
15 | .digest("base64")
16 | // Replace non-filename-safe characters
17 | .replace(/[/+=]/g, "");
18 |
19 | return `${hash}.${extension}`;
20 | }
21 |
22 | export async function checkFileExists(filename: string) {
23 | try {
24 | await R2.send(
25 | new HeadObjectCommand({
26 | Bucket: process.env.R2_BUCKET_NAME,
27 | Key: filename,
28 | })
29 | );
30 | return true;
31 | } catch {
32 | return false;
33 | }
34 | }
35 |
36 | export async function uploadToR2(filename: string, data: Buffer | ArrayBuffer) {
37 | let bodyData = Buffer.isBuffer(data) ? data : Buffer.from(data);
38 | const extension = getExtension(filename);
39 |
40 | // Convert HEIC to JPG if needed
41 | if (extension === "heic") {
42 | bodyData = await convertHeicToJpg(bodyData);
43 | filename = filename.replace(/\.heic$/i, ".jpg");
44 | }
45 |
46 | const contentType = getMimeType(extension === "heic" ? "jpg" : extension);
47 |
48 | await R2.send(
49 | new PutObjectCommand({
50 | Bucket: process.env.R2_BUCKET_NAME,
51 | Key: filename,
52 | Body: bodyData,
53 | ContentType: contentType,
54 | CacheControl: "public, max-age=31536000",
55 | })
56 | );
57 | return `${process.env.R2_PUBLIC_URL}/${filename}`;
58 | }
59 |
60 | export async function downloadImage(imageUrl: string) {
61 | const response = await axios.get(imageUrl, {
62 | responseType: "arraybuffer",
63 | });
64 | return response.data;
65 | }
66 |
67 | export function getExtension(str: string): string {
68 | const lower = str.toLowerCase();
69 |
70 | if (lower.includes(".png")) return "png";
71 | if (lower.includes(".jpg") || lower.includes(".jpeg")) return "jpg";
72 | if (lower.includes(".gif")) return "gif";
73 | if (lower.includes(".webp")) return "webp";
74 | if (lower.includes(".svg")) return "svg";
75 | if (lower.includes(".ico")) return "ico";
76 | if (lower.includes(".tiff") || lower.includes(".tif")) return "tiff";
77 | if (lower.includes(".avif")) return "avif";
78 | if (lower.includes(".heic") || lower.includes(".heif")) return "heic";
79 | return "jpg"; // default fallback
80 | }
81 |
82 | export function getMimeType(extension: string): string {
83 | const contentTypes = {
84 | png: "image/png",
85 | jpg: "image/jpeg",
86 | gif: "image/gif",
87 | webp: "image/webp",
88 | svg: "image/svg+xml",
89 | ico: "image/x-icon",
90 | tiff: "image/tiff",
91 | avif: "image/avif",
92 | heic: "image/heic",
93 | };
94 | return contentTypes[extension as keyof typeof contentTypes] || "image/jpeg";
95 | }
96 |
97 | export async function convertHeicToJpg(buffer: Buffer): Promise {
98 | try {
99 | // Convert HEIC to JPEG using heic-convert
100 | const converted = await heicConvert({
101 | buffer: buffer,
102 | format: "JPEG",
103 | // quality: 0.8, // quality defaults to 0.92
104 | });
105 | return Buffer.from(converted);
106 | } catch (error: any) {
107 | console.error("HEIC conversion error:", error);
108 | throw new Error(`Failed to convert HEIC image: ${error.message}`);
109 | }
110 | }
--------------------------------------------------------------------------------
/image-converter-next/app/api/upload/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { downloadImage, generateFileName, getExtension, uploadToR2 } from "../../actions/upload-actions";
3 |
4 | export async function POST(request: Request) {
5 | try {
6 | const contentType = request.headers.get("content-type") || "";
7 | let imageData: ArrayBuffer | Buffer;
8 | let filename: string;
9 |
10 | if (contentType.includes("multipart/form-data")) {
11 | const formData = await request.formData();
12 | const file = formData.get("file") as File;
13 |
14 | if (!file) {
15 | return NextResponse.json({ error: "Please provide an image file" }, { status: 400 });
16 | }
17 |
18 | imageData = await file.arrayBuffer();
19 | const extension = getExtension(file.name);
20 | filename = await generateFileName(imageData, extension);
21 | } else {
22 | const { imageUrl } = await request.json();
23 |
24 | if (!imageUrl) {
25 | return NextResponse.json({ error: "Please provide an image URL" }, { status: 400 });
26 | }
27 |
28 | imageData = await downloadImage(imageUrl);
29 | const extension = getExtension(imageUrl);
30 | filename = await generateFileName(imageData, extension);
31 | }
32 |
33 | const publicUrl = await uploadToR2(filename, imageData);
34 |
35 | return NextResponse.json({
36 | success: true,
37 | url: publicUrl,
38 | cached: false,
39 | });
40 | } catch (error: any) {
41 | console.error("Upload error:", error);
42 | return NextResponse.json({ error: `Upload failed: ${error.message}` }, { status: 500 });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/image-converter-next/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import BaiDuAnalytics from "@/app/BaiDuAnalytics";
2 | import GoogleAnalytics from "@/app/GoogleAnalytics";
3 | import Footer from "@/components/footer/Footer";
4 | import Header from "@/components/header/Header";
5 | import { TailwindIndicator } from "@/components/TailwindIndicator";
6 | import { siteConfig } from "@/config/site";
7 | import { cn } from "@/lib/utils";
8 | import "@/styles/globals.css";
9 | import "@/styles/loading.css";
10 | import { Analytics } from "@vercel/analytics/react";
11 | import { Viewport } from "next";
12 | import { ThemeProvider } from "next-themes";
13 | import { Inter as FontSans } from "next/font/google";
14 |
15 | export const fontSans = FontSans({
16 | subsets: ["latin"],
17 | variable: "--font-sans",
18 | });
19 |
20 | export const metadata = {
21 | title: siteConfig.name,
22 | description: siteConfig.description,
23 | keywords: siteConfig.keywords,
24 | authors: siteConfig.authors,
25 | creator: siteConfig.creator,
26 | icons: siteConfig.icons,
27 | metadataBase: siteConfig.metadataBase,
28 | openGraph: siteConfig.openGraph,
29 | twitter: siteConfig.twitter,
30 | };
31 | export const viewport: Viewport = {
32 | themeColor: siteConfig.themeColors,
33 | };
34 |
35 | export default async function RootLayout({
36 | children,
37 | }: {
38 | children: React.ReactNode;
39 | }) {
40 | return (
41 |
42 |
43 |
49 |
54 |
55 | {children}
56 |
57 |
58 |
59 |
60 | {process.env.NODE_ENV === "development" ? (
61 | <>>
62 | ) : (
63 | <>
64 |
65 |
66 | >
67 | )}
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/image-converter-next/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Features } from "@/components/home/Features";
4 | import { siteConfig } from "@/config/site";
5 | import { motion } from "framer-motion";
6 | import { Clipboard, Image as ImageIcon, Link2 } from "lucide-react";
7 | import { useState } from "react";
8 | import toast from "react-hot-toast";
9 |
10 | export default function Home() {
11 | const [imageUrl, setImageUrl] = useState("");
12 | const [uploadedUrl, setUploadedUrl] = useState("");
13 | const [loading, setLoading] = useState(false);
14 | const [error, setError] = useState("");
15 | const [selectedFile, setSelectedFile] = useState(null);
16 |
17 | const handleSubmit = async (e: React.FormEvent) => {
18 | e.preventDefault();
19 | setLoading(true);
20 | setError("");
21 | setUploadedUrl("");
22 |
23 | try {
24 | let response;
25 |
26 | if (selectedFile) {
27 | // Handle file upload
28 | const formData = new FormData();
29 | formData.append("file", selectedFile);
30 |
31 | response = await fetch("/api/upload", {
32 | method: "POST",
33 | body: formData,
34 | });
35 | } else if (imageUrl) {
36 | // Handle URL upload (existing logic)
37 | response = await fetch("/api/upload", {
38 | method: "POST",
39 | headers: { "Content-Type": "application/json" },
40 | body: JSON.stringify({ imageUrl }),
41 | });
42 | } else {
43 | throw new Error("Please provide an image URL or file");
44 | }
45 |
46 | const data = await response.json();
47 |
48 | if (data.success) {
49 | setUploadedUrl(data.url);
50 | toast.success("Image converted successfully!");
51 | } else {
52 | setError(data.error || "Conversion failed");
53 | toast.error(data.error || "Conversion failed");
54 | }
55 | } catch (err) {
56 | const errorMessage = "An error occurred during processing";
57 | setError(errorMessage);
58 | toast.error(errorMessage);
59 | } finally {
60 | setLoading(false);
61 | }
62 | };
63 |
64 | const handleCopy = async (text: string) => {
65 | try {
66 | await navigator.clipboard.writeText(text);
67 | toast.success("Copied to clipboard");
68 | } catch (err) {
69 | toast.error("Failed to copy");
70 | }
71 | };
72 |
73 | // Add file input handler
74 | const handleFileChange = (e: React.ChangeEvent) => {
75 | const file = e.target.files?.[0];
76 | if (file) {
77 | setSelectedFile(file);
78 | setImageUrl(""); // Clear URL input when file is selected
79 | }
80 | };
81 |
82 | return (
83 |
84 |
85 | {/* Hero Section */}
86 |
87 |
92 |
93 | {siteConfig.name}
94 |
95 |
96 | {siteConfig.description}
97 |
98 |
99 |
100 | {/* Steps */}
101 |
102 |
} text="Paste Image URL" number={1} />
103 |
104 |
} text="Auto Convert" number={2} />
105 |
106 |
} text="Get Permanent Link" number={3} />
107 |
108 |
109 |
110 | {/* Converter Card */}
111 |
117 |
118 |
178 |
179 | {/* Error Message */}
180 | {error && (
181 |
186 |
187 | ⚠️
188 | {error}
189 |
190 |
191 | )}
192 |
193 | {/* Success Result */}
194 | {uploadedUrl && (
195 |
200 |
201 | ✨
202 | Conversion Successful!
203 |
204 |
205 |
206 |
212 |
218 |
219 |
220 |
221 |
222 | )}
223 |
224 |
225 |
226 |
227 |
228 |
229 | );
230 | }
231 |
232 | function LoadingSpinner() {
233 | return (
234 |
249 | );
250 | }
251 |
252 | function Step({
253 | icon,
254 | text,
255 | number,
256 | }: {
257 | icon: React.ReactNode;
258 | text: string;
259 | number: number;
260 | }) {
261 | return (
262 |
263 |
264 |
265 | {number}
266 |
267 | {icon}
268 |
269 |
{text}
270 |
271 | );
272 | }
273 |
274 | function Arrow() {
275 | return (
276 |
291 | );
292 | }
293 |
294 | function PreviewCard({ url }: { url: string }) {
295 | return (
296 |
297 |
298 |

{
303 | e.currentTarget.src = "/placeholder.svg";
304 | }}
305 | />
306 |
307 |
308 | );
309 | }
310 |
--------------------------------------------------------------------------------
/image-converter-next/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "styles/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/image-converter-next/components/TailwindIndicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === "production") return null
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/image-converter-next/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Moon, Sun } from "lucide-react";
4 | import { useTheme } from "next-themes";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 |
14 | export function ThemeToggle() {
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 | setTheme("light")}>
28 | Light
29 |
30 | setTheme("dark")}>
31 | Dark
32 |
33 | setTheme("system")}>
34 | System
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/image-converter-next/components/WebsiteLogo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { getDomain } from "@/lib/utils";
3 | import { useEffect, useState } from "react";
4 |
5 | interface IProps {
6 | url: string;
7 | size?: number;
8 | className?: string;
9 | timeout?: number;
10 | }
11 |
12 | const WebsiteLogo = ({
13 | url,
14 | size = 32,
15 | className = "",
16 | timeout = 1000, // 1 second
17 | }: IProps) => {
18 | const domain = getDomain(url);
19 | const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
20 | const [fallbackIndex, setFallbackIndex] = useState(0);
21 | const [isLoading, setIsLoading] = useState(true);
22 | const [hasError, setHasError] = useState(false);
23 |
24 | const fallbackSources = [
25 | `https://${domain}/logo.svg`,
26 | `https://${domain}/logo.png`,
27 | `https://${domain}/apple-touch-icon.png`,
28 | `https://${domain}/apple-touch-icon-precomposed.png`,
29 | `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
30 | `https://icons.duckduckgo.com/ip3/${domain}.ico`,
31 | `https://${domain}/favicon.ico`,
32 | ];
33 |
34 | useEffect(() => {
35 | let timeoutId: any;
36 |
37 | if (isLoading) {
38 | timeoutId = setTimeout(() => {
39 | handleError();
40 | }, timeout);
41 | }
42 |
43 | return () => {
44 | if (timeoutId) {
45 | clearTimeout(timeoutId);
46 | }
47 | };
48 | }, [imgSrc, isLoading]);
49 |
50 | const handleError = () => {
51 | const nextIndex = fallbackIndex + 1;
52 | if (nextIndex < fallbackSources.length) {
53 | setFallbackIndex(nextIndex);
54 | setImgSrc(fallbackSources[nextIndex]);
55 | setIsLoading(true);
56 | } else {
57 | setHasError(true);
58 | setIsLoading(false);
59 | }
60 | };
61 |
62 | const handleLoad = () => {
63 | setIsLoading(false);
64 | setHasError(false);
65 | };
66 |
67 | return (
68 |
72 | {/* placeholder */}
73 | {isLoading && (
74 |
77 | )}
78 |
79 |

94 |
95 | {/* Fallback: Display first letter of domain when all image sources fail */}
96 | {hasError && (
97 |
101 | {domain.charAt(0).toUpperCase()}
102 |
103 | )}
104 |
105 | );
106 | };
107 |
108 | export default WebsiteLogo;
109 |
--------------------------------------------------------------------------------
/image-converter-next/components/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import FooterLinks from "@/components/footer/FooterLinks";
2 | import FooterProducts from "@/components/footer/FooterProducts";
3 | import { siteConfig } from "@/config/site";
4 | import Link from "next/link";
5 |
6 | const Footer = () => {
7 | const d = new Date();
8 | const currentYear = d.getFullYear();
9 | const { authors } = siteConfig;
10 |
11 | return (
12 |
25 | );
26 | };
27 |
28 | export default Footer;
29 |
--------------------------------------------------------------------------------
/image-converter-next/components/footer/FooterLinks.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import { BsGithub, BsTwitterX, BsWechat } from "react-icons/bs";
4 | import { MdEmail } from "react-icons/md";
5 | import { SiBuymeacoffee, SiJuejin } from "react-icons/si";
6 |
7 | const footerLinks = [
8 | { name: "email", href: "mailto:weijunext@gmail.com", icon: MdEmail },
9 | { name: "twitter", href: "https://twitter.com/weijunext", icon: BsTwitterX },
10 | { name: "github", href: "https://github.com/weijunext/", icon: BsGithub },
11 | {
12 | name: "buyMeCoffee",
13 | href: "https://www.buymeacoffee.com/weijunext",
14 | icon: SiBuymeacoffee,
15 | },
16 | {
17 | name: "juejin",
18 | href: "https://juejin.cn/user/26044008768029",
19 | icon: SiJuejin,
20 | },
21 | {
22 | name: "weChat",
23 | href: "https://weijunext.com/make-a-friend",
24 | icon: BsWechat,
25 | },
26 | ];
27 |
28 | const FooterLinks = () => {
29 | return (
30 |
31 | {footerLinks.map((link) => (
32 |
39 | {link.icon &&
40 | React.createElement(link.icon, { className: "text-lg" })}
41 |
42 | ))}
43 |
44 | );
45 | };
46 |
47 | export default FooterLinks;
48 |
--------------------------------------------------------------------------------
/image-converter-next/components/footer/FooterProducts.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | const footerProducts = [
4 | { url: "https://phcopilot.ai/", name: "Product Hunt Copilot" },
5 | { url: "https://smartexcel.cc/", name: "Smart Excel" },
6 | {
7 | url: "https://landingpage.weijunext.com/",
8 | name: "Landing Page Boilerplate",
9 | },
10 | { url: "https://nextjscn.org/", name: "Next.js 中文文档" },
11 | { url: "https://nextjs.weijunext.com/", name: "Next.js Practice" },
12 | { url: "https://starter.weijunext.com/", name: "Next.js Starter" },
13 | {
14 | url: "https://github.com/weijunext/indie-hacker-tools",
15 | name: "Indie Hacker Tools",
16 | },
17 | { url: "https://weijunext.com/", name: "J实验室" },
18 | ];
19 |
20 | const FooterProducts = () => {
21 | return (
22 |
23 | {footerProducts.map((product, index) => {
24 | return (
25 |
26 |
27 | {product.name}
28 |
29 | {index !== footerProducts.length - 1 ? (
30 | <>
31 | {" • "}
32 | >
33 | ) : (
34 | <>>
35 | )}
36 |
37 | );
38 | })}
39 |
40 | );
41 | };
42 |
43 | export default FooterProducts;
44 |
--------------------------------------------------------------------------------
/image-converter-next/components/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeToggle } from "@/components/ThemeToggle";
2 | import Link from "next/link";
3 | import { BsGithub } from "react-icons/bs";
4 |
5 | const Header = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | Image URL Converter
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Header;
35 |
--------------------------------------------------------------------------------
/image-converter-next/components/header/HeaderLinks.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import { BsGithub, BsTwitterX } from "react-icons/bs";
4 | import { SiBuymeacoffee } from "react-icons/si";
5 |
6 | const headerLinks = [
7 | {
8 | name: "repo",
9 | href: "https://github.com/weijunext/nextjs-15-starter",
10 | icon: BsGithub,
11 | },
12 | { name: "twitter", href: "https://twitter.com/weijunext", icon: BsTwitterX },
13 | {
14 | name: "buyMeCoffee",
15 | href: "https://www.buymeacoffee.com/weijunext",
16 | icon: SiBuymeacoffee,
17 | },
18 | ];
19 |
20 | const HeaderLinks = () => {
21 | return (
22 |
23 | {headerLinks.map((link) => (
24 |
31 | {link.icon &&
32 | React.createElement(link.icon, { className: "text-lg" })}
33 |
34 | ))}
35 |
36 | );
37 | };
38 | export default HeaderLinks;
39 |
--------------------------------------------------------------------------------
/image-converter-next/components/home/FeatureCard.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 |
3 | export function FeatureCard({
4 | icon,
5 | title,
6 | description,
7 | }: {
8 | icon: string;
9 | title: string;
10 | description: string;
11 | }) {
12 | return (
13 |
17 | {icon}
18 |
19 | {title}
20 |
21 | {description}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/image-converter-next/components/home/Features.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { FeatureCard } from "./FeatureCard";
3 |
4 | export function Features() {
5 | return (
6 |
7 |
8 |
9 |
10 | Powerful Features
11 |
12 |
13 | Why choose our image conversion tool
14 |
15 |
16 |
17 |
23 | {features.map((feature, index) => (
24 |
30 |
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | }
38 | export const features = [
39 | {
40 | title: "Permanent Storage",
41 | description:
42 | "Built on Cloudflare R2 storage, ensuring your image links remain valid permanently",
43 | icon: "🗄️",
44 | color: "from-blue-500 to-blue-600",
45 | },
46 | {
47 | title: "Global CDN",
48 | description:
49 | "Leveraging Cloudflare's CDN network for fast access worldwide",
50 | icon: "🚀",
51 | color: "from-purple-500 to-purple-600",
52 | },
53 | {
54 | title: "Free to Use",
55 | description: "Developed using Cloudflare's free tier, available at no cost",
56 | icon: "💎",
57 | color: "from-green-500 to-green-600",
58 | },
59 | {
60 | title: "Secure & Reliable",
61 | description:
62 | "Advanced cloud storage technology ensuring data security and stable access",
63 | icon: "🛡️",
64 | color: "from-orange-500 to-orange-600",
65 | },
66 | {
67 | title: "User Friendly",
68 | description:
69 | "Intuitive interface design - just paste the image URL to start conversion",
70 | icon: "✨",
71 | color: "from-pink-500 to-pink-600",
72 | },
73 | {
74 | title: "Open Source",
75 | description: "Completely open source code, free to deploy and customize",
76 | icon: "📖",
77 | color: "from-teal-500 to-teal-600",
78 | },
79 | ] as const;
80 |
81 | export type Feature = (typeof features)[number];
82 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/expanding-arrow.tsx:
--------------------------------------------------------------------------------
1 | export default function ExpandingArrow({ className }: { className?: string }) {
2 | return (
3 |
4 |
19 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/eye.tsx:
--------------------------------------------------------------------------------
1 | export default function TablerEyeFilled(props: any) {
2 | return (
3 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as LoadingDots } from "./loading-dots";
2 | export { default as LoadingCircle } from "./loading-circle";
3 | export { default as LoadingSpinner } from "./loading-spinner";
4 | export { default as ExpandingArrow } from "./expanding-arrow";
5 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/loading-circle.tsx:
--------------------------------------------------------------------------------
1 | export default function LoadingCircle() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/loading-dots.module.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: inline-flex;
3 | align-items: center;
4 | }
5 |
6 | .loading .spacer {
7 | margin-right: 2px;
8 | }
9 |
10 | .loading span {
11 | animation-name: blink;
12 | animation-duration: 1.4s;
13 | animation-iteration-count: infinite;
14 | animation-fill-mode: both;
15 | width: 5px;
16 | height: 5px;
17 | border-radius: 50%;
18 | display: inline-block;
19 | margin: 0 1px;
20 | }
21 |
22 | .loading span:nth-of-type(2) {
23 | animation-delay: 0.2s;
24 | }
25 |
26 | .loading span:nth-of-type(3) {
27 | animation-delay: 0.4s;
28 | }
29 |
30 | @keyframes blink {
31 | 0% {
32 | opacity: 0.2;
33 | }
34 | 20% {
35 | opacity: 1;
36 | }
37 | 100% {
38 | opacity: 0.2;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/loading-dots.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./loading-dots.module.css";
2 |
3 | const LoadingDots = ({ color = "#000" }: { color?: string }) => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default LoadingDots;
14 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/loading-spinner.module.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | color: gray;
3 | display: inline-block;
4 | position: relative;
5 | width: 80px;
6 | height: 80px;
7 | transform: scale(0.3) translateX(-95px);
8 | }
9 | .spinner div {
10 | transform-origin: 40px 40px;
11 | animation: spinner 1.2s linear infinite;
12 | }
13 | .spinner div:after {
14 | content: " ";
15 | display: block;
16 | position: absolute;
17 | top: 3px;
18 | left: 37px;
19 | width: 6px;
20 | height: 20px;
21 | border-radius: 20%;
22 | background: black;
23 | }
24 | .spinner div:nth-child(1) {
25 | transform: rotate(0deg);
26 | animation-delay: -1.1s;
27 | }
28 | .spinner div:nth-child(2) {
29 | transform: rotate(30deg);
30 | animation-delay: -1s;
31 | }
32 | .spinner div:nth-child(3) {
33 | transform: rotate(60deg);
34 | animation-delay: -0.9s;
35 | }
36 | .spinner div:nth-child(4) {
37 | transform: rotate(90deg);
38 | animation-delay: -0.8s;
39 | }
40 | .spinner div:nth-child(5) {
41 | transform: rotate(120deg);
42 | animation-delay: -0.7s;
43 | }
44 | .spinner div:nth-child(6) {
45 | transform: rotate(150deg);
46 | animation-delay: -0.6s;
47 | }
48 | .spinner div:nth-child(7) {
49 | transform: rotate(180deg);
50 | animation-delay: -0.5s;
51 | }
52 | .spinner div:nth-child(8) {
53 | transform: rotate(210deg);
54 | animation-delay: -0.4s;
55 | }
56 | .spinner div:nth-child(9) {
57 | transform: rotate(240deg);
58 | animation-delay: -0.3s;
59 | }
60 | .spinner div:nth-child(10) {
61 | transform: rotate(270deg);
62 | animation-delay: -0.2s;
63 | }
64 | .spinner div:nth-child(11) {
65 | transform: rotate(300deg);
66 | animation-delay: -0.1s;
67 | }
68 | .spinner div:nth-child(12) {
69 | transform: rotate(330deg);
70 | animation-delay: 0s;
71 | }
72 | @keyframes spinner {
73 | 0% {
74 | opacity: 1;
75 | }
76 | 100% {
77 | opacity: 0;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/loading-spinner.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./loading-spinner.module.css";
2 |
3 | export default function LoadingSpinner() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/logo.tsx:
--------------------------------------------------------------------------------
1 | export default function LogoSVG(props: any) {
2 | return (
3 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/moon.tsx:
--------------------------------------------------------------------------------
1 | export default function PhMoonFill(props: any) {
2 | return (
3 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/image-converter-next/components/icons/sun.tsx:
--------------------------------------------------------------------------------
1 | export default function PhSunBold(props: any) {
2 | return (
3 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/image-converter-next/components/social-icons/icons.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react'
2 |
3 | // Icons taken from: https://simpleicons.org/
4 | // To add a new icon, add a new function here and add it to components in social-icons/index.tsx
5 |
6 | export function Facebook(svgProps: SVGProps) {
7 | return (
8 |
11 | )
12 | }
13 |
14 | export function Github(svgProps: SVGProps) {
15 | return (
16 |
19 | )
20 | }
21 |
22 | export function Linkedin(svgProps: SVGProps) {
23 | return (
24 |
27 | )
28 | }
29 |
30 | export function Mail(svgProps: SVGProps) {
31 | return (
32 |
36 | )
37 | }
38 |
39 | export function Twitter(svgProps: SVGProps) {
40 | return (
41 |
44 | )
45 | }
46 |
47 | export function TwitterX(svgProps: SVGProps) {
48 | return (
49 |
55 | )
56 | }
57 |
58 | export function Youtube(svgProps: SVGProps) {
59 | return (
60 |
63 | )
64 | }
65 |
66 | export function Mastodon(svgProps: SVGProps) {
67 | return (
68 |
71 | )
72 | }
73 |
74 | export function Threads(svgProps: SVGProps) {
75 | return (
76 |
79 | )
80 | }
81 |
82 | export function Instagram(svgProps: SVGProps) {
83 | return (
84 |
87 | )
88 | }
89 |
90 | export function WeChat(svgProps: SVGProps) {
91 | return (
92 |
102 | )
103 | }
104 |
105 | export function JueJin(svgProps: SVGProps) {
106 | return (
107 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/image-converter-next/components/social-icons/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Facebook,
3 | Github,
4 | Instagram,
5 | JueJin,
6 | Linkedin,
7 | Mail,
8 | Mastodon,
9 | Threads,
10 | Twitter,
11 | TwitterX,
12 | WeChat,
13 | Youtube
14 | } from './icons'
15 |
16 | const components = {
17 | mail: Mail,
18 | github: Github,
19 | facebook: Facebook,
20 | youtube: Youtube,
21 | linkedin: Linkedin,
22 | twitter: Twitter,
23 | twitterX: TwitterX,
24 | weChat: WeChat,
25 | jueJin: JueJin,
26 | mastodon: Mastodon,
27 | threads: Threads,
28 | instagram: Instagram
29 | }
30 |
31 | type SocialIconProps = {
32 | kind: keyof typeof components
33 | href: string | undefined
34 | size?: number
35 | }
36 |
37 | const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
38 | if (
39 | !href ||
40 | (kind === 'mail' &&
41 | !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
42 | )
43 | return null
44 |
45 | const SocialSvg = components[kind]
46 |
47 | return (
48 |
54 | {kind}
55 |
58 |
59 | )
60 | }
61 |
62 | export default SocialIcon
63 |
--------------------------------------------------------------------------------
/image-converter-next/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/image-converter-next/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/image-converter-next/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
74 |
75 | ))
76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77 |
78 | const DropdownMenuItem = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef & {
81 | inset?: boolean
82 | }
83 | >(({ className, inset, ...props }, ref) => (
84 | svg]:size-4 [&>svg]:shrink-0",
88 | inset && "pl-8",
89 | className
90 | )}
91 | {...props}
92 | />
93 | ))
94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95 |
96 | const DropdownMenuCheckboxItem = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef
99 | >(({ className, children, checked, ...props }, ref) => (
100 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 | ))
117 | DropdownMenuCheckboxItem.displayName =
118 | DropdownMenuPrimitive.CheckboxItem.displayName
119 |
120 | const DropdownMenuRadioItem = React.forwardRef<
121 | React.ElementRef,
122 | React.ComponentPropsWithoutRef
123 | >(({ className, children, ...props }, ref) => (
124 |
132 |
133 |
134 |
135 |
136 |
137 | {children}
138 |
139 | ))
140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141 |
142 | const DropdownMenuLabel = React.forwardRef<
143 | React.ElementRef,
144 | React.ComponentPropsWithoutRef & {
145 | inset?: boolean
146 | }
147 | >(({ className, inset, ...props }, ref) => (
148 |
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 |
172 | const DropdownMenuShortcut = ({
173 | className,
174 | ...props
175 | }: React.HTMLAttributes) => {
176 | return (
177 |
181 | )
182 | }
183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184 |
185 | export {
186 | DropdownMenu,
187 | DropdownMenuTrigger,
188 | DropdownMenuContent,
189 | DropdownMenuItem,
190 | DropdownMenuCheckboxItem,
191 | DropdownMenuRadioItem,
192 | DropdownMenuLabel,
193 | DropdownMenuSeparator,
194 | DropdownMenuShortcut,
195 | DropdownMenuGroup,
196 | DropdownMenuPortal,
197 | DropdownMenuSub,
198 | DropdownMenuSubContent,
199 | DropdownMenuSubTrigger,
200 | DropdownMenuRadioGroup,
201 | }
202 |
--------------------------------------------------------------------------------
/image-converter-next/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/image-converter-next/config/site.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "@/types/siteConfig";
2 |
3 | export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://starter.weijunext.com";
4 |
5 | const baseSiteConfig = {
6 | name: "Image URL Converter",
7 | description: "A powerful and free tool to convert any image URL into a permanent CDN link. Built with Cloudflare R2 and Next.js, offering global CDN acceleration and permanent storage.",
8 | url: BASE_URL,
9 | metadataBase: BASE_URL,
10 | keywords: [],
11 | authors: [
12 | {
13 | name: "weijunext",
14 | url: "https://weijunext.com",
15 | twitter: 'https://x.com/weijunext',
16 | }
17 | ],
18 | creator: '@weijunext',
19 | themeColors: [
20 | { media: '(prefers-color-scheme: light)', color: 'white' },
21 | { media: '(prefers-color-scheme: dark)', color: 'black' },
22 | ],
23 | defaultNextTheme: 'system', // next-theme option: system | dark | light
24 | icons: {
25 | icon: "/favicon.ico",
26 | shortcut: "/logo.png",
27 | apple: "/logo.png", // apple-touch-icon.png
28 | },
29 | }
30 |
31 | export const siteConfig: SiteConfig = {
32 | ...baseSiteConfig,
33 | openGraph: {
34 | type: "website",
35 | locale: "en-US",
36 | url: baseSiteConfig.url,
37 | title: baseSiteConfig.name,
38 | description: baseSiteConfig.description,
39 | siteName: baseSiteConfig.name,
40 | images: [`${baseSiteConfig.url}/og.png`],
41 | },
42 | twitter: {
43 | card: "summary_large_image",
44 | title: baseSiteConfig.name,
45 | site: baseSiteConfig.url,
46 | description: baseSiteConfig.description,
47 | images: [`${baseSiteConfig.url}/og.png`],
48 | creator: baseSiteConfig.creator,
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/image-converter-next/gtag.js:
--------------------------------------------------------------------------------
1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_ID || null;
2 |
3 | export const pageview = (url) => {
4 | window.gtag("config", GA_TRACKING_ID, {
5 | page_path: url,
6 | });
7 | };
8 |
9 | export const event = ({ action, category, label, value }) => {
10 | window.gtag("event", action, {
11 | event_category: category,
12 | event_label: label,
13 | value: value,
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/image-converter-next/lib/s3-client.ts:
--------------------------------------------------------------------------------
1 | import { S3Client } from '@aws-sdk/client-s3';
2 |
3 | export const R2 = new S3Client({
4 | region: 'auto',
5 | endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
6 | credentials: {
7 | accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
8 | secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
9 | },
10 | });
--------------------------------------------------------------------------------
/image-converter-next/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export const getDomain = (url: string) => {
9 | try {
10 | // Add https:// protocol if not present
11 | const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
12 | const domain = new URL(urlWithProtocol).hostname;
13 | // Remove 'www.' prefix if exists
14 | return domain.replace(/^www\./, '');
15 | } catch (error) {
16 | // Return original input if URL parsing fails
17 | return url;
18 | }
19 | };
--------------------------------------------------------------------------------
/image-converter-next/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/image-converter-next/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 |
3 | module.exports = {
4 | siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://starter.weijunext.com",
5 | generateRobotsTxt: true,
6 | sitemapSize: 7000,
7 | };
8 |
--------------------------------------------------------------------------------
/image-converter-next/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/image-converter-next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-15-starter",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "next dev",
6 | "postbuild": "next-sitemap",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@aws-sdk/client-s3": "3.734.0",
13 | "@radix-ui/react-dropdown-menu": "^2.1.4",
14 | "@radix-ui/react-slot": "^1.1.0",
15 | "@types/heic-convert": "^2.1.0",
16 | "@vercel/analytics": "^1.4.1",
17 | "axios": "^1.7.9",
18 | "class-variance-authority": "^0.7.1",
19 | "clsx": "^2.1.1",
20 | "dayjs": "^1.11.13",
21 | "form-data": "^4.0.1",
22 | "framer-motion": "^12.0.5",
23 | "heic-convert": "^2.1.0",
24 | "lucide-react": "^0.468.0",
25 | "next": "15.0.4",
26 | "next-mdx-remote": "^5.0.0",
27 | "next-themes": "^0.4.4",
28 | "react": "^19.0.0",
29 | "react-dom": "^19.0.0",
30 | "react-hot-toast": "^2.4.1",
31 | "react-icons": "^5.4.0",
32 | "remark-gfm": "^4.0.0",
33 | "tailwind-merge": "^2.5.5",
34 | "tailwindcss-animate": "^1.0.7"
35 | },
36 | "devDependencies": {
37 | "@types/node": "^20",
38 | "@types/react": "^19",
39 | "@types/react-dom": "^19",
40 | "autoprefixer": "^10.4.19",
41 | "eslint": "^8",
42 | "eslint-config-next": "15.0.4",
43 | "next-sitemap": "^4.2.3",
44 | "postcss": "^8.4.38",
45 | "tailwindcss": "^3.4.3",
46 | "typescript": "^5"
47 | }
48 | }
--------------------------------------------------------------------------------
/image-converter-next/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/image-converter-next/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/image-converter-next/public/favicon.ico
--------------------------------------------------------------------------------
/image-converter-next/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/image-converter-next/public/logo.png
--------------------------------------------------------------------------------
/image-converter-next/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
540 |
--------------------------------------------------------------------------------
/image-converter-next/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/image-converter-next/public/og.png
--------------------------------------------------------------------------------
/image-converter-next/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/image-converter-next/public/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/image-converter-next/public/screenshot-1.png
--------------------------------------------------------------------------------
/image-converter-next/public/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/image-converter-next/public/screenshot-2.png
--------------------------------------------------------------------------------
/image-converter-next/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | scroll-behavior: smooth;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 0 0% 100%;
12 | --foreground: 240 10% 3.9%;
13 |
14 | --card: 0 0% 100%;
15 | --card-foreground: 240 10% 3.9%;
16 |
17 | --popover: 0 0% 100%;
18 | --popover-foreground: 240 10% 3.9%;
19 |
20 | --primary: 240 5.9% 10%;
21 | --primary-foreground: 0 0% 98%;
22 |
23 | --secondary: 240 4.8% 95.9%;
24 | --secondary-foreground: 240 5.9% 10%;
25 |
26 | --muted: 240 4.8% 95.9%;
27 | --muted-foreground: 240 3.8% 46.1%;
28 |
29 | --accent: 240 4.8% 95.9%;
30 | --accent-foreground: 240 5.9% 10%;
31 |
32 | --destructive: 0 84.2% 60.2%;
33 | --destructive-foreground: 0 0% 98%;
34 |
35 | --border: 240 5.9% 90%;
36 | --input: 240 5.9% 90%;
37 | --ring: 240 10% 3.9%;
38 |
39 | --radius: 0.5rem;
40 |
41 | --chart-1: 12 76% 61%;
42 |
43 | --chart-2: 173 58% 39%;
44 |
45 | --chart-3: 197 37% 24%;
46 |
47 | --chart-4: 43 74% 66%;
48 |
49 | --chart-5: 27 87% 67%;
50 | }
51 |
52 | .dark {
53 | --background: 240 10% 3.9%;
54 | --foreground: 0 0% 98%;
55 |
56 | --card: 240 10% 3.9%;
57 | --card-foreground: 0 0% 98%;
58 |
59 | --popover: 240 10% 3.9%;
60 | --popover-foreground: 0 0% 98%;
61 |
62 | --primary: 0 0% 98%;
63 | --primary-foreground: 240 5.9% 10%;
64 |
65 | --secondary: 240 3.7% 15.9%;
66 | --secondary-foreground: 0 0% 98%;
67 |
68 | --muted: 240 3.7% 15.9%;
69 | --muted-foreground: 240 5% 64.9%;
70 |
71 | --accent: 240 3.7% 15.9%;
72 | --accent-foreground: 0 0% 98%;
73 |
74 | --destructive: 0 62.8% 30.6%;
75 | --destructive-foreground: 0 0% 98%;
76 |
77 | --border: 240 3.7% 15.9%;
78 | --input: 240 3.7% 15.9%;
79 | --ring: 240 4.9% 83.9%;
80 | --chart-1: 220 70% 50%;
81 | --chart-2: 160 60% 45%;
82 | --chart-3: 30 80% 55%;
83 | --chart-4: 280 65% 60%;
84 | --chart-5: 340 75% 55%;
85 | }
86 | }
87 |
88 | @layer base {
89 | * {
90 | @apply border-border;
91 | }
92 |
93 | body {
94 | @apply bg-background text-foreground;
95 | }
96 | }
--------------------------------------------------------------------------------
/image-converter-next/styles/loading.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: inline-flex;
3 | align-items: center;
4 | }
5 |
6 | .loading .spacer {
7 | margin-right: 2px;
8 | }
9 |
10 | .loading span {
11 | animation-name: blink;
12 | animation-duration: 1.4s;
13 | animation-iteration-count: infinite;
14 | animation-fill-mode: both;
15 | width: 5px;
16 | height: 5px;
17 | border-radius: 50%;
18 | display: inline-block;
19 | margin: 0 1px;
20 | }
21 |
22 | .loading span:nth-of-type(2) {
23 | animation-delay: 0.2s;
24 | }
25 |
26 | .loading span:nth-of-type(3) {
27 | animation-delay: 0.4s;
28 | }
29 |
30 | @keyframes blink {
31 | 0% {
32 | opacity: 0.2;
33 | }
34 | 20% {
35 | opacity: 1;
36 | }
37 | 100% {
38 | opacity: 0.2;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/image-converter-next/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: '2rem',
16 | screens: {
17 | '2xl': '1400px'
18 | }
19 | },
20 | extend: {
21 | colors: {
22 | border: 'hsl(var(--border))',
23 | input: 'hsl(var(--input))',
24 | ring: 'hsl(var(--ring))',
25 | background: 'hsl(var(--background))',
26 | foreground: 'hsl(var(--foreground))',
27 | primary: {
28 | DEFAULT: 'hsl(var(--primary))',
29 | foreground: 'hsl(var(--primary-foreground))'
30 | },
31 | secondary: {
32 | DEFAULT: 'hsl(var(--secondary))',
33 | foreground: 'hsl(var(--secondary-foreground))'
34 | },
35 | destructive: {
36 | DEFAULT: 'hsl(var(--destructive))',
37 | foreground: 'hsl(var(--destructive-foreground))'
38 | },
39 | muted: {
40 | DEFAULT: 'hsl(var(--muted))',
41 | foreground: 'hsl(var(--muted-foreground))'
42 | },
43 | accent: {
44 | DEFAULT: 'hsl(var(--accent))',
45 | foreground: 'hsl(var(--accent-foreground))'
46 | },
47 | popover: {
48 | DEFAULT: 'hsl(var(--popover))',
49 | foreground: 'hsl(var(--popover-foreground))'
50 | },
51 | card: {
52 | DEFAULT: 'hsl(var(--card))',
53 | foreground: 'hsl(var(--card-foreground))'
54 | },
55 | chart: {
56 | '1': 'hsl(var(--chart-1))',
57 | '2': 'hsl(var(--chart-2))',
58 | '3': 'hsl(var(--chart-3))',
59 | '4': 'hsl(var(--chart-4))',
60 | '5': 'hsl(var(--chart-5))'
61 | }
62 | },
63 | borderRadius: {
64 | lg: 'var(--radius)',
65 | md: 'calc(var(--radius) - 2px)',
66 | sm: 'calc(var(--radius) - 4px)'
67 | },
68 | keyframes: {
69 | 'accordion-down': {
70 | from: {
71 | height: '0'
72 | },
73 | to: {
74 | height: 'var(--radix-accordion-content-height)'
75 | }
76 | },
77 | 'accordion-up': {
78 | from: {
79 | height: 'var(--radix-accordion-content-height)'
80 | },
81 | to: {
82 | height: '0'
83 | }
84 | }
85 | },
86 | animation: {
87 | 'accordion-down': 'accordion-down 0.2s ease-out',
88 | 'accordion-up': 'accordion-up 0.2s ease-out'
89 | }
90 | }
91 | },
92 | plugins: [require("tailwindcss-animate")],
93 | } satisfies Config
94 |
95 | export default config
--------------------------------------------------------------------------------
/image-converter-next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/image-converter-next/types/siteConfig.ts:
--------------------------------------------------------------------------------
1 |
2 | export type AuthorsConfig = {
3 | name: string
4 | url: string
5 | twitter?: string
6 | }
7 | export type ThemeColor = {
8 | media: string
9 | color: string
10 | }
11 | export type SiteConfig = {
12 | name: string
13 | description: string
14 | url: string
15 | keywords: string[]
16 | authors: AuthorsConfig[]
17 | creator: string
18 | metadataBase: URL | string
19 | themeColors?: string | ThemeColor[]
20 | defaultNextTheme?: string
21 | icons: {
22 | icon: string
23 | shortcut?: string
24 | apple?: string
25 | }
26 | openGraph: {
27 | type: string
28 | locale: string
29 | url: string
30 | title: string
31 | description: string
32 | siteName: string
33 | images?: string[]
34 | },
35 | twitter: {
36 | card: string
37 | title: string
38 | site: string
39 | description: string
40 | images?: string[]
41 | creator: string
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/image-converter-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-converter-worker",
3 | "version": "1.0.0",
4 | "main": "worker.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "keywords": [],
9 | "author": "",
10 | "license": "ISC",
11 | "description": "",
12 | "dependencies": {
13 | "wrangler": "^3.105.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/image-converter-worker/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | wrangler:
9 | specifier: ^3.105.1
10 | version: 3.105.1
11 |
12 | packages:
13 |
14 | /@cloudflare/kv-asset-handler@0.3.4:
15 | resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
16 | engines: {node: '>=16.13'}
17 | dependencies:
18 | mime: 3.0.0
19 | dev: false
20 |
21 | /@cloudflare/workerd-darwin-64@1.20250124.0:
22 | resolution: {integrity: sha512-P5Z5KfVAuoCidIc0o2JPQZFLNTXDjtxN8vhtreCUr6V+xF5pqDNwQqeBDnDDF0gcszFQOYi2OZAB9e1MwssTwA==}
23 | engines: {node: '>=16'}
24 | cpu: [x64]
25 | os: [darwin]
26 | requiresBuild: true
27 | dev: false
28 | optional: true
29 |
30 | /@cloudflare/workerd-darwin-arm64@1.20250124.0:
31 | resolution: {integrity: sha512-lVxf6qIfmJ5rS6rmGKV7lt6ApY6nhD4kAQTK4vKYm/npk2sXod6LASIY0U4WBCwy4N+S75a8hP2QtmQf+KV3Iw==}
32 | engines: {node: '>=16'}
33 | cpu: [arm64]
34 | os: [darwin]
35 | requiresBuild: true
36 | dev: false
37 | optional: true
38 |
39 | /@cloudflare/workerd-linux-64@1.20250124.0:
40 | resolution: {integrity: sha512-5S4GzN08vW/CfzaM1rVAkRhPPSDX1O1t7u0pj+xdbGl4GcazBzE4ZLre+y9OMplZ9PBCkxXkRWqHXzabWA1x4A==}
41 | engines: {node: '>=16'}
42 | cpu: [x64]
43 | os: [linux]
44 | requiresBuild: true
45 | dev: false
46 | optional: true
47 |
48 | /@cloudflare/workerd-linux-arm64@1.20250124.0:
49 | resolution: {integrity: sha512-CHSYnutDfXgUWL9WcP0GbzIb5OyC9RZVCJGhKbDTQy6/uH7AivNcLzXtOhNdqetKjERmOxUbL9Us7vcMQLztog==}
50 | engines: {node: '>=16'}
51 | cpu: [arm64]
52 | os: [linux]
53 | requiresBuild: true
54 | dev: false
55 | optional: true
56 |
57 | /@cloudflare/workerd-windows-64@1.20250124.0:
58 | resolution: {integrity: sha512-5TunEy5x4pNUQ10Z47qP5iF6m3X9uB2ZScKDLkNaWtbQ7EcMCapOWzuynVkTKIMBgDeKw6DAB8nbbkybPyMS9w==}
59 | engines: {node: '>=16'}
60 | cpu: [x64]
61 | os: [win32]
62 | requiresBuild: true
63 | dev: false
64 | optional: true
65 |
66 | /@cspotcode/source-map-support@0.8.1:
67 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
68 | engines: {node: '>=12'}
69 | dependencies:
70 | '@jridgewell/trace-mapping': 0.3.9
71 | dev: false
72 |
73 | /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19):
74 | resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
75 | peerDependencies:
76 | esbuild: '*'
77 | dependencies:
78 | esbuild: 0.17.19
79 | dev: false
80 |
81 | /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19):
82 | resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==}
83 | peerDependencies:
84 | esbuild: '*'
85 | dependencies:
86 | esbuild: 0.17.19
87 | escape-string-regexp: 4.0.0
88 | rollup-plugin-node-polyfills: 0.2.1
89 | dev: false
90 |
91 | /@esbuild/android-arm64@0.17.19:
92 | resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
93 | engines: {node: '>=12'}
94 | cpu: [arm64]
95 | os: [android]
96 | requiresBuild: true
97 | dev: false
98 | optional: true
99 |
100 | /@esbuild/android-arm@0.17.19:
101 | resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
102 | engines: {node: '>=12'}
103 | cpu: [arm]
104 | os: [android]
105 | requiresBuild: true
106 | dev: false
107 | optional: true
108 |
109 | /@esbuild/android-x64@0.17.19:
110 | resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
111 | engines: {node: '>=12'}
112 | cpu: [x64]
113 | os: [android]
114 | requiresBuild: true
115 | dev: false
116 | optional: true
117 |
118 | /@esbuild/darwin-arm64@0.17.19:
119 | resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
120 | engines: {node: '>=12'}
121 | cpu: [arm64]
122 | os: [darwin]
123 | requiresBuild: true
124 | dev: false
125 | optional: true
126 |
127 | /@esbuild/darwin-x64@0.17.19:
128 | resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
129 | engines: {node: '>=12'}
130 | cpu: [x64]
131 | os: [darwin]
132 | requiresBuild: true
133 | dev: false
134 | optional: true
135 |
136 | /@esbuild/freebsd-arm64@0.17.19:
137 | resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
138 | engines: {node: '>=12'}
139 | cpu: [arm64]
140 | os: [freebsd]
141 | requiresBuild: true
142 | dev: false
143 | optional: true
144 |
145 | /@esbuild/freebsd-x64@0.17.19:
146 | resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
147 | engines: {node: '>=12'}
148 | cpu: [x64]
149 | os: [freebsd]
150 | requiresBuild: true
151 | dev: false
152 | optional: true
153 |
154 | /@esbuild/linux-arm64@0.17.19:
155 | resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
156 | engines: {node: '>=12'}
157 | cpu: [arm64]
158 | os: [linux]
159 | requiresBuild: true
160 | dev: false
161 | optional: true
162 |
163 | /@esbuild/linux-arm@0.17.19:
164 | resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
165 | engines: {node: '>=12'}
166 | cpu: [arm]
167 | os: [linux]
168 | requiresBuild: true
169 | dev: false
170 | optional: true
171 |
172 | /@esbuild/linux-ia32@0.17.19:
173 | resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
174 | engines: {node: '>=12'}
175 | cpu: [ia32]
176 | os: [linux]
177 | requiresBuild: true
178 | dev: false
179 | optional: true
180 |
181 | /@esbuild/linux-loong64@0.17.19:
182 | resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
183 | engines: {node: '>=12'}
184 | cpu: [loong64]
185 | os: [linux]
186 | requiresBuild: true
187 | dev: false
188 | optional: true
189 |
190 | /@esbuild/linux-mips64el@0.17.19:
191 | resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
192 | engines: {node: '>=12'}
193 | cpu: [mips64el]
194 | os: [linux]
195 | requiresBuild: true
196 | dev: false
197 | optional: true
198 |
199 | /@esbuild/linux-ppc64@0.17.19:
200 | resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
201 | engines: {node: '>=12'}
202 | cpu: [ppc64]
203 | os: [linux]
204 | requiresBuild: true
205 | dev: false
206 | optional: true
207 |
208 | /@esbuild/linux-riscv64@0.17.19:
209 | resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
210 | engines: {node: '>=12'}
211 | cpu: [riscv64]
212 | os: [linux]
213 | requiresBuild: true
214 | dev: false
215 | optional: true
216 |
217 | /@esbuild/linux-s390x@0.17.19:
218 | resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
219 | engines: {node: '>=12'}
220 | cpu: [s390x]
221 | os: [linux]
222 | requiresBuild: true
223 | dev: false
224 | optional: true
225 |
226 | /@esbuild/linux-x64@0.17.19:
227 | resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
228 | engines: {node: '>=12'}
229 | cpu: [x64]
230 | os: [linux]
231 | requiresBuild: true
232 | dev: false
233 | optional: true
234 |
235 | /@esbuild/netbsd-x64@0.17.19:
236 | resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
237 | engines: {node: '>=12'}
238 | cpu: [x64]
239 | os: [netbsd]
240 | requiresBuild: true
241 | dev: false
242 | optional: true
243 |
244 | /@esbuild/openbsd-x64@0.17.19:
245 | resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
246 | engines: {node: '>=12'}
247 | cpu: [x64]
248 | os: [openbsd]
249 | requiresBuild: true
250 | dev: false
251 | optional: true
252 |
253 | /@esbuild/sunos-x64@0.17.19:
254 | resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
255 | engines: {node: '>=12'}
256 | cpu: [x64]
257 | os: [sunos]
258 | requiresBuild: true
259 | dev: false
260 | optional: true
261 |
262 | /@esbuild/win32-arm64@0.17.19:
263 | resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
264 | engines: {node: '>=12'}
265 | cpu: [arm64]
266 | os: [win32]
267 | requiresBuild: true
268 | dev: false
269 | optional: true
270 |
271 | /@esbuild/win32-ia32@0.17.19:
272 | resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
273 | engines: {node: '>=12'}
274 | cpu: [ia32]
275 | os: [win32]
276 | requiresBuild: true
277 | dev: false
278 | optional: true
279 |
280 | /@esbuild/win32-x64@0.17.19:
281 | resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
282 | engines: {node: '>=12'}
283 | cpu: [x64]
284 | os: [win32]
285 | requiresBuild: true
286 | dev: false
287 | optional: true
288 |
289 | /@fastify/busboy@2.1.1:
290 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
291 | engines: {node: '>=14'}
292 | dev: false
293 |
294 | /@jridgewell/resolve-uri@3.1.2:
295 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
296 | engines: {node: '>=6.0.0'}
297 | dev: false
298 |
299 | /@jridgewell/sourcemap-codec@1.5.0:
300 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
301 | dev: false
302 |
303 | /@jridgewell/trace-mapping@0.3.9:
304 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
305 | dependencies:
306 | '@jridgewell/resolve-uri': 3.1.2
307 | '@jridgewell/sourcemap-codec': 1.5.0
308 | dev: false
309 |
310 | /acorn-walk@8.3.4:
311 | resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
312 | engines: {node: '>=0.4.0'}
313 | dependencies:
314 | acorn: 8.14.0
315 | dev: false
316 |
317 | /acorn@8.14.0:
318 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
319 | engines: {node: '>=0.4.0'}
320 | hasBin: true
321 | dev: false
322 |
323 | /as-table@1.0.55:
324 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
325 | dependencies:
326 | printable-characters: 1.0.42
327 | dev: false
328 |
329 | /blake3-wasm@2.1.5:
330 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
331 | dev: false
332 |
333 | /capnp-ts@0.7.0:
334 | resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
335 | dependencies:
336 | debug: 4.4.0
337 | tslib: 2.8.1
338 | transitivePeerDependencies:
339 | - supports-color
340 | dev: false
341 |
342 | /confbox@0.1.8:
343 | resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
344 | dev: false
345 |
346 | /cookie@0.7.2:
347 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
348 | engines: {node: '>= 0.6'}
349 | dev: false
350 |
351 | /data-uri-to-buffer@2.0.2:
352 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
353 | dev: false
354 |
355 | /debug@4.4.0:
356 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
357 | engines: {node: '>=6.0'}
358 | peerDependencies:
359 | supports-color: '*'
360 | peerDependenciesMeta:
361 | supports-color:
362 | optional: true
363 | dependencies:
364 | ms: 2.1.3
365 | dev: false
366 |
367 | /defu@6.1.4:
368 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
369 | dev: false
370 |
371 | /esbuild@0.17.19:
372 | resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
373 | engines: {node: '>=12'}
374 | hasBin: true
375 | requiresBuild: true
376 | optionalDependencies:
377 | '@esbuild/android-arm': 0.17.19
378 | '@esbuild/android-arm64': 0.17.19
379 | '@esbuild/android-x64': 0.17.19
380 | '@esbuild/darwin-arm64': 0.17.19
381 | '@esbuild/darwin-x64': 0.17.19
382 | '@esbuild/freebsd-arm64': 0.17.19
383 | '@esbuild/freebsd-x64': 0.17.19
384 | '@esbuild/linux-arm': 0.17.19
385 | '@esbuild/linux-arm64': 0.17.19
386 | '@esbuild/linux-ia32': 0.17.19
387 | '@esbuild/linux-loong64': 0.17.19
388 | '@esbuild/linux-mips64el': 0.17.19
389 | '@esbuild/linux-ppc64': 0.17.19
390 | '@esbuild/linux-riscv64': 0.17.19
391 | '@esbuild/linux-s390x': 0.17.19
392 | '@esbuild/linux-x64': 0.17.19
393 | '@esbuild/netbsd-x64': 0.17.19
394 | '@esbuild/openbsd-x64': 0.17.19
395 | '@esbuild/sunos-x64': 0.17.19
396 | '@esbuild/win32-arm64': 0.17.19
397 | '@esbuild/win32-ia32': 0.17.19
398 | '@esbuild/win32-x64': 0.17.19
399 | dev: false
400 |
401 | /escape-string-regexp@4.0.0:
402 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
403 | engines: {node: '>=10'}
404 | dev: false
405 |
406 | /estree-walker@0.6.1:
407 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
408 | dev: false
409 |
410 | /exit-hook@2.2.1:
411 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
412 | engines: {node: '>=6'}
413 | dev: false
414 |
415 | /fsevents@2.3.3:
416 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
417 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
418 | os: [darwin]
419 | requiresBuild: true
420 | dev: false
421 | optional: true
422 |
423 | /get-source@2.0.12:
424 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
425 | dependencies:
426 | data-uri-to-buffer: 2.0.2
427 | source-map: 0.6.1
428 | dev: false
429 |
430 | /glob-to-regexp@0.4.1:
431 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
432 | dev: false
433 |
434 | /magic-string@0.25.9:
435 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
436 | dependencies:
437 | sourcemap-codec: 1.4.8
438 | dev: false
439 |
440 | /mime@3.0.0:
441 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
442 | engines: {node: '>=10.0.0'}
443 | hasBin: true
444 | dev: false
445 |
446 | /miniflare@3.20250124.0:
447 | resolution: {integrity: sha512-ewsetUwhj4FqeLoE3UMqYHHyCYIOPzdhlpF9CHuHpMZbfLvI9SPd+VrKrLfOgyAF97EHqVWb6WamIrLdgtj6Kg==}
448 | engines: {node: '>=16.13'}
449 | hasBin: true
450 | dependencies:
451 | '@cspotcode/source-map-support': 0.8.1
452 | acorn: 8.14.0
453 | acorn-walk: 8.3.4
454 | capnp-ts: 0.7.0
455 | exit-hook: 2.2.1
456 | glob-to-regexp: 0.4.1
457 | stoppable: 1.1.0
458 | undici: 5.28.5
459 | workerd: 1.20250124.0
460 | ws: 8.18.0
461 | youch: 3.3.4
462 | zod: 3.24.1
463 | transitivePeerDependencies:
464 | - bufferutil
465 | - supports-color
466 | - utf-8-validate
467 | dev: false
468 |
469 | /mlly@1.7.4:
470 | resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
471 | dependencies:
472 | acorn: 8.14.0
473 | pathe: 2.0.2
474 | pkg-types: 1.3.1
475 | ufo: 1.5.4
476 | dev: false
477 |
478 | /ms@2.1.3:
479 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
480 | dev: false
481 |
482 | /mustache@4.2.0:
483 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
484 | hasBin: true
485 | dev: false
486 |
487 | /ohash@1.1.4:
488 | resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==}
489 | dev: false
490 |
491 | /path-to-regexp@6.3.0:
492 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
493 | dev: false
494 |
495 | /pathe@1.1.2:
496 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
497 | dev: false
498 |
499 | /pathe@2.0.2:
500 | resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==}
501 | dev: false
502 |
503 | /pkg-types@1.3.1:
504 | resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
505 | dependencies:
506 | confbox: 0.1.8
507 | mlly: 1.7.4
508 | pathe: 2.0.2
509 | dev: false
510 |
511 | /printable-characters@1.0.42:
512 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
513 | dev: false
514 |
515 | /rollup-plugin-inject@3.0.2:
516 | resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
517 | deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
518 | dependencies:
519 | estree-walker: 0.6.1
520 | magic-string: 0.25.9
521 | rollup-pluginutils: 2.8.2
522 | dev: false
523 |
524 | /rollup-plugin-node-polyfills@0.2.1:
525 | resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==}
526 | dependencies:
527 | rollup-plugin-inject: 3.0.2
528 | dev: false
529 |
530 | /rollup-pluginutils@2.8.2:
531 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
532 | dependencies:
533 | estree-walker: 0.6.1
534 | dev: false
535 |
536 | /source-map@0.6.1:
537 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
538 | engines: {node: '>=0.10.0'}
539 | dev: false
540 |
541 | /sourcemap-codec@1.4.8:
542 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
543 | deprecated: Please use @jridgewell/sourcemap-codec instead
544 | dev: false
545 |
546 | /stacktracey@2.1.8:
547 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
548 | dependencies:
549 | as-table: 1.0.55
550 | get-source: 2.0.12
551 | dev: false
552 |
553 | /stoppable@1.1.0:
554 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
555 | engines: {node: '>=4', npm: '>=6'}
556 | dev: false
557 |
558 | /tslib@2.8.1:
559 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
560 | dev: false
561 |
562 | /ufo@1.5.4:
563 | resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
564 | dev: false
565 |
566 | /undici@5.28.5:
567 | resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==}
568 | engines: {node: '>=14.0'}
569 | dependencies:
570 | '@fastify/busboy': 2.1.1
571 | dev: false
572 |
573 | /unenv@2.0.0-rc.0:
574 | resolution: {integrity: sha512-H0kl2w8jFL/FAk0xvjVing4bS3jd//mbg1QChDnn58l9Sc5RtduaKmLAL8n+eBw5jJo8ZjYV7CrEGage5LAOZQ==}
575 | dependencies:
576 | defu: 6.1.4
577 | mlly: 1.7.4
578 | ohash: 1.1.4
579 | pathe: 1.1.2
580 | ufo: 1.5.4
581 | dev: false
582 |
583 | /workerd@1.20250124.0:
584 | resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==}
585 | engines: {node: '>=16'}
586 | hasBin: true
587 | requiresBuild: true
588 | optionalDependencies:
589 | '@cloudflare/workerd-darwin-64': 1.20250124.0
590 | '@cloudflare/workerd-darwin-arm64': 1.20250124.0
591 | '@cloudflare/workerd-linux-64': 1.20250124.0
592 | '@cloudflare/workerd-linux-arm64': 1.20250124.0
593 | '@cloudflare/workerd-windows-64': 1.20250124.0
594 | dev: false
595 |
596 | /wrangler@3.105.1:
597 | resolution: {integrity: sha512-Hl+wwWrMuDAcQOo+oKccf/MlAF+BHN66hbjGLo7cYhsrj1fm+w2jcFhiVTrRDpdJHPJMDfMGGbH8Gq7sexUGEQ==}
598 | engines: {node: '>=16.17.0'}
599 | hasBin: true
600 | peerDependencies:
601 | '@cloudflare/workers-types': ^4.20250121.0
602 | peerDependenciesMeta:
603 | '@cloudflare/workers-types':
604 | optional: true
605 | dependencies:
606 | '@cloudflare/kv-asset-handler': 0.3.4
607 | '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
608 | '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19)
609 | blake3-wasm: 2.1.5
610 | esbuild: 0.17.19
611 | miniflare: 3.20250124.0
612 | path-to-regexp: 6.3.0
613 | unenv: 2.0.0-rc.0
614 | workerd: 1.20250124.0
615 | optionalDependencies:
616 | fsevents: 2.3.3
617 | transitivePeerDependencies:
618 | - bufferutil
619 | - supports-color
620 | - utf-8-validate
621 | dev: false
622 |
623 | /ws@8.18.0:
624 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
625 | engines: {node: '>=10.0.0'}
626 | peerDependencies:
627 | bufferutil: ^4.0.1
628 | utf-8-validate: '>=5.0.2'
629 | peerDependenciesMeta:
630 | bufferutil:
631 | optional: true
632 | utf-8-validate:
633 | optional: true
634 | dev: false
635 |
636 | /youch@3.3.4:
637 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
638 | dependencies:
639 | cookie: 0.7.2
640 | mustache: 4.2.0
641 | stacktracey: 2.1.8
642 | dev: false
643 |
644 | /zod@3.24.1:
645 | resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
646 | dev: false
647 |
--------------------------------------------------------------------------------
/image-converter-worker/worker.js:
--------------------------------------------------------------------------------
1 | export default {
2 | async fetch(request, env) {
3 | const url = new URL(request.url);
4 | const key = url.pathname.slice(1);
5 |
6 | if (!key) {
7 | return new Response('Not Found', { status: 404 });
8 | }
9 |
10 | const object = await env.MY_BUCKET.get(key);
11 |
12 | if (!object) {
13 | return new Response('Not Found', { status: 404 });
14 | }
15 |
16 | const headers = new Headers();
17 | object.writeHttpMetadata(headers);
18 | headers.set('etag', object.httpEtag);
19 | headers.set('Cache-Control', 'public, max-age=31536000');
20 | headers.set('Access-Control-Allow-Origin', '*');
21 |
22 | return new Response(object.body, {
23 | headers,
24 | });
25 | },
26 | };
--------------------------------------------------------------------------------
/image-converter-worker/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "r2-public"
2 | main = "worker.js"
3 | compatibility_date = "2024-01-25"
4 |
5 | [[r2_buckets]]
6 | binding = "MY_BUCKET"
7 | bucket_name = "images"
8 | preview_bucket_name = "images"
9 |
--------------------------------------------------------------------------------
/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/screenshot-1.png
--------------------------------------------------------------------------------
/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weijunext/image-url-converter/8180eba87f2bfe5d6f110b5a8694c396ce75b0a6/screenshot-2.png
--------------------------------------------------------------------------------