Spaces:
Running
Running
github-actions[bot] commited on
Commit ยท
4cb7ab8
0
Parent(s):
deploy: sync to hugging face
Browse filesThis view is limited to 50 files because it contains too many changes. ย See raw diff
- .github/workflows/ci.yml +47 -0
- .github/workflows/deploy_hf.yml +36 -0
- .gitignore +31 -0
- CHAT_HISTORY.md +42 -0
- Dockerfile +53 -0
- README.md +72 -0
- backend/main.py +163 -0
- backend/requirements.txt +7 -0
- backend/stress_test.py +47 -0
- backend/test_main.py +112 -0
- check_ports.sh +79 -0
- deploy.sh +35 -0
- docs/API.md +69 -0
- docs/MONITORING.md +59 -0
- docs/ROADMAP_AND_IDEAS.md +125 -0
- frontend/.gitignore +41 -0
- frontend/README.md +36 -0
- frontend/eslint.config.mjs +18 -0
- frontend/next.config.ts +15 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +33 -0
- frontend/postcss.config.mjs +7 -0
- frontend/public/ads.txt +1 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
- frontend/public/manifest.json +16 -0
- frontend/public/next.svg +1 -0
- frontend/public/robots.txt +4 -0
- frontend/public/sitemap.xml +22 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/src/app/about/page.tsx +50 -0
- frontend/src/app/de/page.tsx +87 -0
- frontend/src/app/de/visa-photo/page.tsx +31 -0
- frontend/src/app/echo/page.tsx +35 -0
- frontend/src/app/echo/privacy/page.tsx +49 -0
- frontend/src/app/en/echo/page.tsx +35 -0
- frontend/src/app/en/page.tsx +93 -0
- frontend/src/app/en/visa-photo/page.tsx +41 -0
- frontend/src/app/es/page.tsx +75 -0
- frontend/src/app/es/visa-photo/page.tsx +32 -0
- frontend/src/app/favicon.ico +0 -0
- frontend/src/app/fr/page.tsx +84 -0
- frontend/src/app/fr/visa-photo/page.tsx +32 -0
- frontend/src/app/globals.css +26 -0
- frontend/src/app/ja/page.tsx +84 -0
- frontend/src/app/ja/visa-photo/page.tsx +32 -0
- frontend/src/app/ko/page.tsx +75 -0
- frontend/src/app/ko/visa-photo/page.tsx +32 -0
- frontend/src/app/layout.tsx +68 -0
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI - Backend Testing
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main ]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
test-backend:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
|
| 15 |
+
- name: Set up Python 3.12
|
| 16 |
+
uses: actions/setup-python@v5
|
| 17 |
+
with:
|
| 18 |
+
python-version: '3.12'
|
| 19 |
+
|
| 20 |
+
- name: Install dependencies
|
| 21 |
+
run: |
|
| 22 |
+
cd backend
|
| 23 |
+
python -m pip install --upgrade pip
|
| 24 |
+
pip install -r requirements.txt
|
| 25 |
+
pip install pytest httpx
|
| 26 |
+
|
| 27 |
+
- name: Run Pytest
|
| 28 |
+
run: |
|
| 29 |
+
cd backend
|
| 30 |
+
pytest test_main.py
|
| 31 |
+
|
| 32 |
+
- name: Start server for Stress Test
|
| 33 |
+
run: |
|
| 34 |
+
cd backend
|
| 35 |
+
# Install uvicorn if not present (it should be in requirements.txt)
|
| 36 |
+
nohup python -m uvicorn main:app --host 0.0.0.0 --port 13002 > server.log 2>&1 &
|
| 37 |
+
# Wait for the AI model to download and load (might take a while in CI)
|
| 38 |
+
timeout 120 bash -c 'until curl -s http://localhost:13002/ > /dev/null; do sleep 5; done'
|
| 39 |
+
|
| 40 |
+
- name: Run Stress Test
|
| 41 |
+
run: |
|
| 42 |
+
cd backend
|
| 43 |
+
python stress_test.py
|
| 44 |
+
|
| 45 |
+
- name: Show server logs on failure
|
| 46 |
+
if: failure()
|
| 47 |
+
run: cat backend/server.log
|
.github/workflows/deploy_hf.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
deploy:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- uses: actions/checkout@v4
|
| 13 |
+
with:
|
| 14 |
+
fetch-depth: 0
|
| 15 |
+
|
| 16 |
+
- name: Push to HF (Clean History)
|
| 17 |
+
env:
|
| 18 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 19 |
+
run: |
|
| 20 |
+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
| 21 |
+
git config --global user.name "github-actions[bot]"
|
| 22 |
+
|
| 23 |
+
# Prepare a clean directory for HF (avoiding .git history)
|
| 24 |
+
mkdir -p ../hf_space
|
| 25 |
+
cp -R . ../hf_space/
|
| 26 |
+
cd ../hf_space
|
| 27 |
+
rm -rf .git
|
| 28 |
+
|
| 29 |
+
# Initialize new repo for HF Space (Stateless)
|
| 30 |
+
git init -b main
|
| 31 |
+
git add .
|
| 32 |
+
git commit -m "deploy: sync to hugging face"
|
| 33 |
+
|
| 34 |
+
# Force push to HF Space
|
| 35 |
+
git remote add space https://winterandchaiyun:$HF_TOKEN@huggingface.co/spaces/winterandchaiyun/quicktools
|
| 36 |
+
git push --force space main
|
.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
backend/venv/
|
| 3 |
+
frontend/node_modules/
|
| 4 |
+
|
| 5 |
+
# Next.js build output
|
| 6 |
+
frontend/.next/
|
| 7 |
+
frontend/out/
|
| 8 |
+
|
| 9 |
+
# Logs
|
| 10 |
+
*.log
|
| 11 |
+
npm-debug.log*
|
| 12 |
+
yarn-debug.log*
|
| 13 |
+
yarn-error.log*
|
| 14 |
+
pnpm-debug.log*
|
| 15 |
+
|
| 16 |
+
# OS generated files
|
| 17 |
+
.DS_Store
|
| 18 |
+
Thumbs.db
|
| 19 |
+
|
| 20 |
+
# Environment variables
|
| 21 |
+
.env
|
| 22 |
+
.env.local
|
| 23 |
+
.env.development.local
|
| 24 |
+
.env.test.local
|
| 25 |
+
.env.production.local
|
| 26 |
+
|
| 27 |
+
# Python
|
| 28 |
+
__pycache__/
|
| 29 |
+
*.py[cod]
|
| 30 |
+
*$py.class
|
| 31 |
+
indexing_key.json
|
CHAT_HISTORY.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Visa Photo Maker ้กน็ฎๅฏน่ฏๅ
จ่ฎฐๅฝ
|
| 2 |
+
|
| 3 |
+
> ๆญคๆไปถ็ฑ chat_recorder.py ่ชๅจ็ๆ
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# --- ๅฏน่ฏๅฏผๅบ @ 2025-12-31 09:27:59 ---
|
| 7 |
+
|
| 8 |
+
## 1. ๅๅงๆๆณ
|
| 9 |
+
็จๆทๆๅบๅไธไธช Visa Photo Maker๏ผ่ฆๆฑ่ฝๅป่ใ่ฎพไธบ็ฝ่ฒ่ๆฏใ่ฐๆดๅฐบๅฏธๅนถไธ่ฝฝใ
|
| 10 |
+
|
| 11 |
+
## 2. ๆนๆก็กฎๅฎ
|
| 12 |
+
- **ๅ็ซฏ**: Next.js (React) + Tailwind CSSใ
|
| 13 |
+
- **ๅ็ซฏ**: FastAPI (Python)ใ
|
| 14 |
+
- **ๆ ธๅฟๅบ**: rembg (AIๅป่), Pillow (ๅพๅๅค็)ใ
|
| 15 |
+
- **ๆต็จ**: ไธไผ -> ่ชๅจๅป่ -> ๅๆ็ฝๅบ -> ่ฃๅช(600x600px) -> ไธ่ฝฝใ
|
| 16 |
+
|
| 17 |
+
## 3. ๅผๅๅทฅๅ
ทๅๅค
|
| 18 |
+
- ๅๅปบไบ : ็จไบไฟๅญๅๆฌก่ฎจ่ฎบ็ๆฎตใ
|
| 19 |
+
- ๅๅปบไบ : ็จไบๅฏผๅบๆดๆฎตๅฏน่ฏๅๅฒใ
|
| 20 |
+
|
| 21 |
+
# --- ๅฏผๅบ็ปๆ ---
|
| 22 |
+
|
| 23 |
+
# --- ๅฏน่ฏๅฏผๅบ @ 2026-02-08 17:00:00 ---
|
| 24 |
+
|
| 25 |
+
## 4. ๆฉๅฑๅทฅๅ
ท๏ผEcho Reddit Assistant
|
| 26 |
+
็จๆทๆๅบๅขๅ ็ฌฌไบไธชๅทฅๅ
ท "Echo"๏ผๅฎไฝไธบๅ
่ดน็ Reddit ๅๅคๅฉๆ๏ผๆต่งๅจๆฉๅฑ๏ผใ
|
| 27 |
+
|
| 28 |
+
### ๆๆฏๅฎ็ฐ
|
| 29 |
+
- **ๆถๆๆผ่ฟ**: ๅฐไธป้กตไปๅไธๅทฅๅ
ท้จๆทๅ็บงไธบๅคๅทฅๅ
ท Grid ๅธๅฑ๏ผๆฏๆๆฐดๅนณๆฉๅฑใ
|
| 30 |
+
- **ๅค่ฏญ่จๆฏๆ**: ๅฎ็ฐไบ Echo ็ไธญ่ฑๆๅ่ฏญ่ฝๅฐ้กต (`/echo`, `/en/echo`)ใ
|
| 31 |
+
- **่ฎพ่ฎก้ฃๆ ผ**: ๅปถ็ปญไบ VisaBerry ็ๆ็ฎใไธไธใMaterial Design ้ฃๆ ผ๏ผไฝฟ็จ Emoji ไฝไธบๅพๆ ไปฅๅฎ็ฐ้ถๅปถ่ฟๅ ่ฝฝใ
|
| 32 |
+
- **ๅ่งๆง**: ๅปบ็ซไบไธ้จ็้็งๆฟ็ญ้กต้ข (`/echo/privacy`)๏ผ่ฏฆ็ป่ฏดๆไบ๏ผ
|
| 33 |
+
- ไฝฟ็จ Firebase Authentication ็ฎก็็ปๅฝ็ถๆ๏ผไธๅญๅจ Reddit ๅฏ็ ๏ผใ
|
| 34 |
+
- ๆถ้ๅฟๅๆดปๅจๆฅๅฟ๏ผ็นๅปใไบคไบ๏ผ็จไบๆง่ฝไผๅใ
|
| 35 |
+
- ๅญๅจ AI ็ๆ็ๅๅคไปฅๆ็ปญไผๅๆจกๅไธไธๆๆๅบ่ฝๅใ
|
| 36 |
+
|
| 37 |
+
### ้จ็ฝฒไธๅๅธ
|
| 38 |
+
- **ๅค่ฏญ่จ้จๆทๆดๆฐ**: ๅๆญฅๆดๆฐไบ ZH, EN, DE, JA, FR ็ญๅคไธช่ฏญ่จ็ๆฌ็้จๆทไธป้กต Gridใ
|
| 39 |
+
- **CI/CD**: ้่ฟ GitHub Actions ่ชๅจๅๆญฅ่ณ Hugging Face Spaces ็ไบง็ฏๅขใ
|
| 40 |
+
- **้กต่้ๆ**: ๆดๆฐไบๅ
จๅฑ Footer๏ผๅฐ Echo ๅ ๅ
ฅๅทฅๅ
ทๅฏผ่ชใ
|
| 41 |
+
|
| 42 |
+
# --- ๅฏผๅบ็ปๆ ---
|
Dockerfile
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build Frontend
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Final Image
|
| 10 |
+
FROM python:3.11-slim
|
| 11 |
+
|
| 12 |
+
# Create a non-root user
|
| 13 |
+
RUN useradd -m -u 1000 user
|
| 14 |
+
|
| 15 |
+
# Install Node.js
|
| 16 |
+
RUN apt-get update && apt-get install -y curl && \
|
| 17 |
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
| 18 |
+
apt-get install -y nodejs && \
|
| 19 |
+
rm -rf /var/lib/apt/lists/*
|
| 20 |
+
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
# Install Python dependencies
|
| 24 |
+
COPY backend/requirements.txt ./backend/
|
| 25 |
+
RUN pip install --no-cache-dir -r backend/requirements.txt
|
| 26 |
+
|
| 27 |
+
# Pre-download rembg models
|
| 28 |
+
ENV U2NET_HOME=/app/.u2net
|
| 29 |
+
RUN mkdir -p /app/.u2net && \
|
| 30 |
+
python3 -c "from rembg import new_session; new_session('u2net')" && \
|
| 31 |
+
chown -R user:user /app/.u2net
|
| 32 |
+
|
| 33 |
+
# Copy backend code
|
| 34 |
+
COPY backend/ ./backend/
|
| 35 |
+
|
| 36 |
+
# Copy built frontend
|
| 37 |
+
COPY --from=frontend-builder /app/frontend/.next ./frontend/.next
|
| 38 |
+
COPY --from=frontend-builder /app/frontend/public ./frontend/public
|
| 39 |
+
COPY --from=frontend-builder /app/frontend/package*.json ./frontend/
|
| 40 |
+
COPY --from=frontend-builder /app/frontend/next.config.ts ./frontend/
|
| 41 |
+
COPY --from=frontend-builder /app/frontend/node_modules ./frontend/node_modules
|
| 42 |
+
|
| 43 |
+
# Copy startup script
|
| 44 |
+
COPY scripts/start_hf.sh ./scripts/start_hf.sh
|
| 45 |
+
RUN chmod +x ./scripts/start_hf.sh && chown user:user ./scripts/start_hf.sh
|
| 46 |
+
|
| 47 |
+
# Ensure permissions
|
| 48 |
+
RUN chown -R user:user /app
|
| 49 |
+
|
| 50 |
+
USER user
|
| 51 |
+
EXPOSE 7860
|
| 52 |
+
|
| 53 |
+
CMD ["./scripts/start_hf.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: QuickTools
|
| 3 |
+
emoji: ๐ธ
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Visa Photo Maker
|
| 12 |
+
|
| 13 |
+
A professional, multilingual, privacy-first web application for generating standard visa and passport photos.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
- **AI-Powered**: Automatic background removal and biometric cropping.
|
| 17 |
+
- **Privacy-First**: RAM-only processing, zero disk storage for user images.
|
| 18 |
+
- **Multilingual**: Supports ZH, EN, ES, FR, JA, KO.
|
| 19 |
+
- **Global Standards**: Supports US, EU, UK, China, Japan, Korea, Australia, Canada.
|
| 20 |
+
- **SEO Optimized**: Pre-rendered routes and Schema.org integration.
|
| 21 |
+
|
| 22 |
+
## Tech Stack
|
| 23 |
+
- **Frontend**: Next.js 15+, Tailwind CSS, TypeScript.
|
| 24 |
+
- **Backend**: FastAPI (Python 3.12), Rembg, Pillow, NumPy.
|
| 25 |
+
- **DevOps**: PM2 for process management.
|
| 26 |
+
|
| 27 |
+
## Getting Started
|
| 28 |
+
|
| 29 |
+
### Backend
|
| 30 |
+
1. `cd backend`
|
| 31 |
+
2. `python -m venv venv`
|
| 32 |
+
3. `source venv/bin/activate`
|
| 33 |
+
4. `pip install -r requirements.txt`
|
| 34 |
+
5. `python main.py` (Runs on port 13002)
|
| 35 |
+
|
| 36 |
+
### Frontend
|
| 37 |
+
1. `cd frontend`
|
| 38 |
+
2. `npm install`
|
| 39 |
+
3. `npm run dev` (Runs on port 3000 in dev, 13001 in production)
|
| 40 |
+
|
| 41 |
+
## Monitoring & Maintenance
|
| 42 |
+
|
| 43 |
+
We use a watchdog script to ensure services stay online.
|
| 44 |
+
|
| 45 |
+
- **Port Checker**: `./check_ports.sh` scans for active services.
|
| 46 |
+
- **Auto-Recovery**: `scripts/watchdog.sh` (Runs via cron every minute).
|
| 47 |
+
- **Guide**: See [docs/MONITORING.md](docs/MONITORING.md) for setup instructions.
|
| 48 |
+
|
| 49 |
+
## Testing
|
| 50 |
+
|
| 51 |
+
### Backend Unit Tests
|
| 52 |
+
We use `pytest` to ensure image processing and API logic remain correct.
|
| 53 |
+
```bash
|
| 54 |
+
cd backend
|
| 55 |
+
venv/bin/pytest test_main.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## Deployment
|
| 59 |
+
1. Run the safe deployment script:
|
| 60 |
+
```bash
|
| 61 |
+
./deploy.sh
|
| 62 |
+
```
|
| 63 |
+
(This script automatically runs unit tests before building and reloading.)
|
| 64 |
+
Processes are managed by PM2:
|
| 65 |
+
```bash
|
| 66 |
+
pm2 list
|
| 67 |
+
pm2 restart visa-backend
|
| 68 |
+
pm2 restart visa-frontend
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## License
|
| 72 |
+
MIT
|
backend/main.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException, Form
|
| 3 |
+
from fastapi.responses import StreamingResponse
|
| 4 |
+
from rembg import remove
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import io
|
| 7 |
+
import uvicorn
|
| 8 |
+
import asyncio
|
| 9 |
+
import numpy as np
|
| 10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
|
| 12 |
+
# Configure logging
|
| 13 |
+
logging.basicConfig(
|
| 14 |
+
level=logging.INFO,
|
| 15 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 16 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 17 |
+
)
|
| 18 |
+
logger = logging.getLogger("visa-backend")
|
| 19 |
+
|
| 20 |
+
app = FastAPI(title="Visa Photo Maker API")
|
| 21 |
+
|
| 22 |
+
app.add_middleware(
|
| 23 |
+
CORSMiddleware,
|
| 24 |
+
allow_origins=["*"],
|
| 25 |
+
allow_credentials=True,
|
| 26 |
+
allow_methods=["*"],
|
| 27 |
+
allow_headers=["*"],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
@app.get("/")
|
| 31 |
+
def read_root():
|
| 32 |
+
return {"status": "ok", "message": "Visa Photo Maker Backend is running"}
|
| 33 |
+
|
| 34 |
+
def add_white_background(
|
| 35 |
+
image: Image.Image,
|
| 36 |
+
size: tuple = (600, 600),
|
| 37 |
+
crop_params: dict = None
|
| 38 |
+
) -> Image.Image:
|
| 39 |
+
"""
|
| 40 |
+
1. ่ฃๅชๆ้ๆ่พน็ผ (ๅฆๆๆฒกๆๆไพๆๅจ่ฃๅชๅๆฐ)
|
| 41 |
+
2. ๅๅปบ็ฝ่ฒ่ๆฏ
|
| 42 |
+
3. ๅฐไบบ็ฉไธปไฝ็ผฉๆพๅนถๅฑ
ไธญ
|
| 43 |
+
"""
|
| 44 |
+
# ็กฎไฟๅพ็ๆฏ RGBA ๆจกๅผ
|
| 45 |
+
image = image.convert("RGBA")
|
| 46 |
+
logger.info(f"Original image size: {image.size}, Target size: {size}")
|
| 47 |
+
|
| 48 |
+
if crop_params and all(k in crop_params for k in ('x', 'y', 'w', 'h')):
|
| 49 |
+
# --- ไฝฟ็จๆๅจ่ฃๅชๅๆฐ ---
|
| 50 |
+
logger.info(f"Using manual crop params: {crop_params}")
|
| 51 |
+
x, y, w, h = crop_params['x'], crop_params['y'], crop_params['w'], crop_params['h']
|
| 52 |
+
# ่ฃๅชๅๅพ (crop_params ๅบ่ฏฅๆฏ็ธๅฏนไบๅๅพ็ๅ็ด ๅๆ )
|
| 53 |
+
image = image.crop((x, y, x + w, y + h))
|
| 54 |
+
else:
|
| 55 |
+
# --- ่ชๅจๅผบๅ่ฃๅช้ป่พๅผๅง ---
|
| 56 |
+
# ๅฐ PIL Image ่ฝฌไธบ numpy ๆฐ็ปไปฅๅค็ Alpha ้้
|
| 57 |
+
img_np = np.array(image)
|
| 58 |
+
|
| 59 |
+
# ่ทๅ Alpha ้้ (็ฌฌ4ไธช้้)
|
| 60 |
+
alpha = img_np[:, :, 3]
|
| 61 |
+
|
| 62 |
+
# ่ฎพๅฎ้ๅผ๏ผAlpha ๅผๅฐไบ 50 ็ๅ็ด ่งไธบๅฎๅ
จ้ๆ
|
| 63 |
+
threshold = 50
|
| 64 |
+
mask = alpha > threshold
|
| 65 |
+
|
| 66 |
+
# ๅฆๆๅ
จๅพ้ฝๆฏ้ๆ็๏ผๆฒกๆๆฃๆตๅฐไบบ๏ผ๏ผๅฐฑ็ดๆฅ่ฟๅ็ฝๅพ
|
| 67 |
+
if not np.any(mask):
|
| 68 |
+
logger.warning("No subject found (image is empty)")
|
| 69 |
+
return Image.new("RGB", size, (255, 255, 255))
|
| 70 |
+
|
| 71 |
+
# ่ทๅ้้ถๅบๅ็ๅๆ (่ก, ๅ)
|
| 72 |
+
rows = np.any(mask, axis=1)
|
| 73 |
+
cols = np.any(mask, axis=0)
|
| 74 |
+
|
| 75 |
+
y_min, y_max = np.where(rows)[0][[0, -1]]
|
| 76 |
+
x_min, x_max = np.where(cols)[0][[0, -1]]
|
| 77 |
+
|
| 78 |
+
# ่ฃๅชๅพ็
|
| 79 |
+
image = image.crop((x_min, y_min, x_max + 1, y_max + 1))
|
| 80 |
+
logger.info(f"Cropped size (tight): {image.size}")
|
| 81 |
+
|
| 82 |
+
# --- ๆบ่ฝๅ่บซ่ฃๅช (Smart Body Crop) ---
|
| 83 |
+
target_ratio = size[1] / size[0]
|
| 84 |
+
max_allowed_ratio = target_ratio + 0.15
|
| 85 |
+
|
| 86 |
+
w, h = image.size
|
| 87 |
+
current_ratio = h / w
|
| 88 |
+
|
| 89 |
+
if current_ratio > max_allowed_ratio:
|
| 90 |
+
logger.info(f"Image is too tall (ratio {current_ratio:.2f} > {max_allowed_ratio:.2f}), cropping bottom...")
|
| 91 |
+
new_h = int(w * max_allowed_ratio)
|
| 92 |
+
image = image.crop((0, 0, w, new_h))
|
| 93 |
+
logger.info(f"New size after smart crop: {image.size}")
|
| 94 |
+
# ------------------------------------
|
| 95 |
+
|
| 96 |
+
# 3. ่ฎก็ฎ็ผฉๆพๆฏไพ๏ผCover ๆจกๅผ๏ผไฝ็จๅพฎ็ไธ็น่พน่ท (95% ่ฆ็)
|
| 97 |
+
scale_width = (size[0] * 0.95) / image.width
|
| 98 |
+
scale_height = (size[1] * 0.95) / image.height
|
| 99 |
+
|
| 100 |
+
scale_factor = max(scale_width, scale_height)
|
| 101 |
+
|
| 102 |
+
new_width = int(image.width * scale_factor)
|
| 103 |
+
new_height = int(image.height * scale_factor)
|
| 104 |
+
|
| 105 |
+
logger.info(f"Resizing: {new_width}x{new_height}")
|
| 106 |
+
resized_img = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 107 |
+
|
| 108 |
+
final_image = Image.new("RGB", size, (255, 255, 255))
|
| 109 |
+
x_offset = (size[0] - new_width) // 2
|
| 110 |
+
|
| 111 |
+
top_padding = int(size[1] * 0.10)
|
| 112 |
+
y_offset = top_padding
|
| 113 |
+
|
| 114 |
+
if y_offset + new_height < size[1]:
|
| 115 |
+
y_offset = (size[1] - new_height) // 2
|
| 116 |
+
elif y_offset + new_height > size[1] + 100:
|
| 117 |
+
y_offset = int(size[1] * 0.05)
|
| 118 |
+
|
| 119 |
+
final_image.paste(resized_img, (x_offset, y_offset), resized_img)
|
| 120 |
+
|
| 121 |
+
return final_image.convert("RGB")
|
| 122 |
+
|
| 123 |
+
# Global lock to prevent CPU overload
|
| 124 |
+
processing_lock = asyncio.Lock()
|
| 125 |
+
|
| 126 |
+
@app.post("/process-image")
|
| 127 |
+
async def process_image_endpoint(
|
| 128 |
+
file: UploadFile = File(...),
|
| 129 |
+
width: int = Form(600),
|
| 130 |
+
height: int = Form(600),
|
| 131 |
+
crop_x: int = Form(None),
|
| 132 |
+
crop_y: int = Form(None),
|
| 133 |
+
crop_w: int = Form(None),
|
| 134 |
+
crop_h: int = Form(None)
|
| 135 |
+
):
|
| 136 |
+
# Wait for the lock before processing
|
| 137 |
+
async with processing_lock:
|
| 138 |
+
try:
|
| 139 |
+
input_image_bytes = await file.read()
|
| 140 |
+
logger.info(f"Processing image for file: {file.filename}")
|
| 141 |
+
|
| 142 |
+
output_image_bytes = await asyncio.to_thread(remove, input_image_bytes)
|
| 143 |
+
img_no_bg = Image.open(io.BytesIO(output_image_bytes))
|
| 144 |
+
|
| 145 |
+
crop_params = None
|
| 146 |
+
if crop_x is not None and crop_y is not None:
|
| 147 |
+
crop_params = {'x': crop_x, 'y': crop_y, 'w': crop_w, 'h': crop_h}
|
| 148 |
+
|
| 149 |
+
final_image = add_white_background(img_no_bg, size=(width, height), crop_params=crop_params)
|
| 150 |
+
|
| 151 |
+
img_io = io.BytesIO()
|
| 152 |
+
final_image.save(img_io, format="JPEG", quality=95)
|
| 153 |
+
img_io.seek(0)
|
| 154 |
+
|
| 155 |
+
logger.info(f"Successfully processed image: {file.filename}")
|
| 156 |
+
return StreamingResponse(img_io, media_type="image/jpeg")
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error processing image: {str(e)}", exc_info=True)
|
| 160 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 161 |
+
|
| 162 |
+
if __name__ == "__main__":
|
| 163 |
+
uvicorn.run(app, host="0.0.0.0", port=13002)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
rembg
|
| 4 |
+
pillow
|
| 5 |
+
python-multipart
|
| 6 |
+
numpy
|
| 7 |
+
onnxruntime
|
backend/stress_test.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import httpx
|
| 3 |
+
import time
|
| 4 |
+
import io
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
# ้
็ฝฎๅ็ซฏ API ๅฐๅ
|
| 8 |
+
URL = "http://localhost:13002/process-image"
|
| 9 |
+
|
| 10 |
+
async def send_request(client, task_id):
|
| 11 |
+
# Create a unique dummy image for each request
|
| 12 |
+
file_io = io.BytesIO()
|
| 13 |
+
image = Image.new('RGBA', size=(800, 800), color=(task_id * 50, 0, 0, 255))
|
| 14 |
+
image.save(file_io, 'png')
|
| 15 |
+
file_io.seek(0)
|
| 16 |
+
|
| 17 |
+
print(f"[Task {task_id}] Sending request...")
|
| 18 |
+
start_time = time.time()
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
response = await client.post(
|
| 22 |
+
URL,
|
| 23 |
+
files={"file": ("test.png", file_io, "image/png")},
|
| 24 |
+
data={"width": "600", "height": "600"},
|
| 25 |
+
timeout=60.0
|
| 26 |
+
)
|
| 27 |
+
end_time = time.time()
|
| 28 |
+
print(f"[Task {task_id}] Completed in {end_time - start_time:.2f}s with status {response.status_code}")
|
| 29 |
+
return response.status_code
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"[Task {task_id}] Failed: {str(e)}")
|
| 32 |
+
return None
|
| 33 |
+
|
| 34 |
+
async def run_stress_test():
|
| 35 |
+
async with httpx.AsyncClient() as client:
|
| 36 |
+
# Fire 3 requests simultaneously
|
| 37 |
+
tasks = [send_request(client, i) for i in range(1, 4)]
|
| 38 |
+
print(f"--- Starting Stress Test: 3 Concurrent Requests ---")
|
| 39 |
+
start_total = time.time()
|
| 40 |
+
results = await asyncio.gather(*tasks)
|
| 41 |
+
end_total = time.time()
|
| 42 |
+
print(f"--- Stress Test Finished ---")
|
| 43 |
+
print(f"Total time for all tasks: {end_total - start_total:.2f}s")
|
| 44 |
+
print(f"Success count: {results.count(200)}/3")
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
asyncio.run(run_stress_test())
|
backend/test_main.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from fastapi.testclient import TestClient
|
| 3 |
+
from main import app
|
| 4 |
+
import io
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
client = TestClient(app)
|
| 8 |
+
|
| 9 |
+
def test_read_root():
|
| 10 |
+
response = client.get("/")
|
| 11 |
+
assert response.status_code == 200
|
| 12 |
+
assert "running" in response.json()["message"]
|
| 13 |
+
|
| 14 |
+
@pytest.mark.parametrize("width, height", [
|
| 15 |
+
(600, 600), # US
|
| 16 |
+
(413, 531), # EU
|
| 17 |
+
(354, 472), # JP
|
| 18 |
+
(591, 827), # CA
|
| 19 |
+
])
|
| 20 |
+
def test_process_image_various_sizes(width, height):
|
| 21 |
+
# Create a dummy RGBA image (red square)
|
| 22 |
+
file = io.BytesIO()
|
| 23 |
+
# Use a larger source image to test cropping/resizing
|
| 24 |
+
image = Image.new('RGBA', size=(1000, 1500), color=(255, 0, 0, 255))
|
| 25 |
+
image.save(file, 'png')
|
| 26 |
+
file.seek(0)
|
| 27 |
+
|
| 28 |
+
response = client.post(
|
| 29 |
+
"/process-image",
|
| 30 |
+
files={"file": ("test.png", file, "image/png")},
|
| 31 |
+
data={"width": str(width), "height": str(height)}
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
assert response.status_code == 200
|
| 35 |
+
assert response.headers["content-type"] == "image/jpeg"
|
| 36 |
+
|
| 37 |
+
output_image = Image.open(io.BytesIO(response.content))
|
| 38 |
+
assert output_image.size == (width, height)
|
| 39 |
+
|
| 40 |
+
def test_manual_crop():
|
| 41 |
+
# Create a 500x500 source image
|
| 42 |
+
file = io.BytesIO()
|
| 43 |
+
image = Image.new('RGBA', size=(500, 500), color=(0, 0, 255, 255))
|
| 44 |
+
image.save(file, 'png')
|
| 45 |
+
file.seek(0)
|
| 46 |
+
|
| 47 |
+
# Define a crop in the middle (100, 100 to 300, 300)
|
| 48 |
+
response = client.post(
|
| 49 |
+
"/process-image",
|
| 50 |
+
files={"file": ("manual.png", file, "image/png")},
|
| 51 |
+
data={
|
| 52 |
+
"width": "600",
|
| 53 |
+
"height": "600",
|
| 54 |
+
"crop_x": "100",
|
| 55 |
+
"crop_y": "100",
|
| 56 |
+
"crop_w": "200",
|
| 57 |
+
"crop_h": "200"
|
| 58 |
+
}
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
assert response.status_code == 200
|
| 62 |
+
output_image = Image.open(io.BytesIO(response.content))
|
| 63 |
+
# Output should still be normalized to the requested 600x600
|
| 64 |
+
assert output_image.size == (600, 600)
|
| 65 |
+
|
| 66 |
+
import xml.etree.ElementTree as ET
|
| 67 |
+
import os
|
| 68 |
+
|
| 69 |
+
def test_sitemap_validity():
|
| 70 |
+
# Path to the sitemap file in frontend/public
|
| 71 |
+
sitemap_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "public", "sitemap.xml")
|
| 72 |
+
|
| 73 |
+
# 1. Check if file exists
|
| 74 |
+
assert os.path.exists(sitemap_path), "sitemap.xml does not exist in frontend/public"
|
| 75 |
+
|
| 76 |
+
# 2. Try to parse XML
|
| 77 |
+
try:
|
| 78 |
+
tree = ET.parse(sitemap_path)
|
| 79 |
+
root = tree.getroot()
|
| 80 |
+
except ET.ParseError as e:
|
| 81 |
+
pytest.fail(f"sitemap.xml is not valid XML: {e}")
|
| 82 |
+
|
| 83 |
+
# 3. Check Namespace
|
| 84 |
+
assert "sitemaps.org" in root.tag, "Sitemap namespace missing or incorrect"
|
| 85 |
+
|
| 86 |
+
# 4. Check URLs
|
| 87 |
+
urls = root.findall(".//{http://www.sitemaps.org/schemas/sitemap/0.9}loc")
|
| 88 |
+
assert len(urls) >= 16, f"Expected at least 16 URLs (8 portals + 8 tools), found {len(urls)}"
|
| 89 |
+
|
| 90 |
+
for loc in urls:
|
| 91 |
+
url_text = loc.text.strip()
|
| 92 |
+
# Verify no whitespace inside the URL
|
| 93 |
+
assert " " not in url_text, f"Sitemap URL contains spaces: '{url_text}'"
|
| 94 |
+
assert url_text.startswith("https://quicktools.dpdns.org"), f"Invalid URL domain in sitemap: {url_text}"
|
| 95 |
+
assert not url_text.endswith("/undefined"), f"Sitemap contains undefined path: {url_text}"
|
| 96 |
+
# Check for typical malformed patterns
|
| 97 |
+
assert "\n" not in url_text, f"Sitemap URL contains newline: '{url_text}'"
|
| 98 |
+
|
| 99 |
+
def test_processing_lock():
|
| 100 |
+
# This is hard to test with a simple TestClient because it's synchronous,
|
| 101 |
+
# but we can verify it doesn't crash.
|
| 102 |
+
file = io.BytesIO()
|
| 103 |
+
image = Image.new('RGBA', size=(10, 10), color=(0, 255, 0, 255))
|
| 104 |
+
image.save(file, 'png')
|
| 105 |
+
file.seek(0)
|
| 106 |
+
|
| 107 |
+
response = client.post(
|
| 108 |
+
"/process-image",
|
| 109 |
+
files={"file": ("test.png", file, "image/png")},
|
| 110 |
+
data={"width": "100", "height": "100"}
|
| 111 |
+
)
|
| 112 |
+
assert response.status_code == 200
|
check_ports.sh
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Define colors
|
| 4 |
+
RED='\033[0;31m'
|
| 5 |
+
GREEN='\033[0;32m'
|
| 6 |
+
YELLOW='\033[1;33m'
|
| 7 |
+
BLUE='\033[0;34m'
|
| 8 |
+
NC='\033[0m' # No Color
|
| 9 |
+
|
| 10 |
+
# Get optional port argument
|
| 11 |
+
TARGET_PORT=$1
|
| 12 |
+
|
| 13 |
+
echo -e "${BLUE}๐ Scanning for open ports...${NC}\n"
|
| 14 |
+
|
| 15 |
+
# Print Header
|
| 16 |
+
printf "% -8s % -65s % -15s %b\n" "PORT" "WORKING DIRECTORY [COMMAND]" "PID" "NOTE"
|
| 17 |
+
echo "--------------------------------------------------------------------------------------------------------------------------------"
|
| 18 |
+
|
| 19 |
+
# Use ss to get listening ports
|
| 20 |
+
ss -tunlp | grep LISTEN | while read -r line; do
|
| 21 |
+
local_addr=$(echo $line | awk '{print $5}')
|
| 22 |
+
port=${local_addr##*:}
|
| 23 |
+
|
| 24 |
+
if [ -n "$TARGET_PORT" ] && [ "$port" != "$TARGET_PORT" ]; then
|
| 25 |
+
continue
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
proc_raw=$(echo $line | grep -o 'users:(.*)')
|
| 29 |
+
|
| 30 |
+
if [ -n "$proc_raw" ]; then
|
| 31 |
+
pid=$(echo $proc_raw | grep -o 'pid=[0-9]*' | head -n 1 | cut -d'=' -f2)
|
| 32 |
+
|
| 33 |
+
# Get Current Working Directory
|
| 34 |
+
cwd=$(readlink -f /proc/$pid/cwd 2>/dev/null)
|
| 35 |
+
[ -z "$cwd" ] && cwd="unknown"
|
| 36 |
+
|
| 37 |
+
# Get Command
|
| 38 |
+
cmd_full=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | sed 's/ $//')
|
| 39 |
+
|
| 40 |
+
# Child process handling
|
| 41 |
+
if [[ "$cmd_full" == *"spawn_main"* ]]; then
|
| 42 |
+
ppid=$(ps -o ppid= -p $pid 2>/dev/null | tr -d ' ')
|
| 43 |
+
[ -n "$ppid" ] && cmd_full=$(cat /proc/$ppid/cmdline 2>/dev/null | tr '\0' ' ' | sed 's/ $//')
|
| 44 |
+
fi
|
| 45 |
+
|
| 46 |
+
# Extract Brief Command
|
| 47 |
+
if [[ "$cmd_full" == *"python"* ]]; then
|
| 48 |
+
# Extract script name or module
|
| 49 |
+
cmd_brief=$(echo "$cmd_full" | grep -oE "([^ ]+\.py|-m [^ ]+)" | head -1)
|
| 50 |
+
[ -z "$cmd_brief" ] && cmd_brief="python"
|
| 51 |
+
elif [[ "$cmd_full" == *"node"* ]]; then
|
| 52 |
+
# Just take the first argument after node if it exists
|
| 53 |
+
cmd_brief=$(echo "$cmd_full" | awk '{print $2}')
|
| 54 |
+
[ -z "$cmd_brief" ] || [[ "$cmd_brief" == -* ]] && cmd_brief="node"
|
| 55 |
+
cmd_brief=$(basename "$cmd_brief")
|
| 56 |
+
else
|
| 57 |
+
cmd_brief=$(echo "$cmd_full" | awk '{print $1}')
|
| 58 |
+
cmd_brief=$(basename "$cmd_brief")
|
| 59 |
+
fi
|
| 60 |
+
|
| 61 |
+
cwd_short=$(echo "$cwd" | sed "s|/home/$USER|~|g")
|
| 62 |
+
info_display="${cwd_short} [${cmd_brief}]"
|
| 63 |
+
else
|
| 64 |
+
pid="-"
|
| 65 |
+
info_display="-"
|
| 66 |
+
fi
|
| 67 |
+
|
| 68 |
+
note=""
|
| 69 |
+
if [ "$port" == "13002" ]; then note="${GREEN}Visa-Backend${NC}"
|
| 70 |
+
elif [ "$port" == "13001" ]; then note="${GREEN}Visa-Frontend${NC}"
|
| 71 |
+
elif [ "$port" == "10318" ]; then note="${BLUE}n8n${NC}"
|
| 72 |
+
elif [ "$port" == "20241" ]; then note="${BLUE}Cloudflare${NC}"
|
| 73 |
+
elif [ "$port" == "8501" ]; then note="${YELLOW}Streamlit${NC}"
|
| 74 |
+
fi
|
| 75 |
+
|
| 76 |
+
printf "% -8s % -65s % -15s %b\n" "$port" "${info_display:0:64}" "$pid" "$note"
|
| 77 |
+
done | sort -n
|
| 78 |
+
|
| 79 |
+
echo -e "\n${BLUE}๐ก Format: WORKING_DIR [SCRIPT/COMMAND]${NC}"
|
deploy.sh
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Safe Deployment Script for VisaBerry / QuickTools
|
| 4 |
+
|
| 5 |
+
echo "๐ Starting Safe Deployment Process..."
|
| 6 |
+
|
| 7 |
+
# 1. Run Backend Unit Tests
|
| 8 |
+
echo "๐งช Running Backend Tests..."
|
| 9 |
+
cd backend
|
| 10 |
+
if venv/bin/pytest test_main.py; then
|
| 11 |
+
echo "โ
Tests Passed!"
|
| 12 |
+
else
|
| 13 |
+
echo "โ Tests Failed! Deployment Aborted."
|
| 14 |
+
exit 1
|
| 15 |
+
fi
|
| 16 |
+
cd ..
|
| 17 |
+
|
| 18 |
+
# 2. Build Frontend
|
| 19 |
+
echo "๐๏ธ Building Frontend..."
|
| 20 |
+
cd frontend
|
| 21 |
+
rm -rf .next
|
| 22 |
+
if npm run build; then
|
| 23 |
+
echo "โ
Build Successful!"
|
| 24 |
+
else
|
| 25 |
+
echo "โ Build Failed! Deployment Aborted."
|
| 26 |
+
exit 1
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
# 3. Zero-Downtime Reload via PM2
|
| 30 |
+
echo "๐ Reloading Services..."
|
| 31 |
+
pm2 reload visa-frontend
|
| 32 |
+
# Backend is usually fast, restart is fine, or also reload if using gunicorn
|
| 33 |
+
pm2 restart visa-backend
|
| 34 |
+
|
| 35 |
+
echo "โจ Deployment Complete! VisaBerry is live and stable."
|
docs/API.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Visa Photo Maker Documentation
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
Visa Photo Maker is a privacy-first, bilingual web application that automatically processes user-uploaded photos into standard visa/passport photo formats. It uses AI to remove backgrounds and intelligently crops the image to meet specific aspect ratios and composition requirements.
|
| 5 |
+
|
| 6 |
+
## Privacy & Security
|
| 7 |
+
**Does the server keep my photos?**
|
| 8 |
+
**NO.**
|
| 9 |
+
- Images are processed in **RAM (Memory)** only.
|
| 10 |
+
- The backend receives the file -> Removes Background -> Crops -> Returns the result immediately.
|
| 11 |
+
- No `save()` to disk operations are performed for user images.
|
| 12 |
+
- Once the request is finished, the data is discarded by the server's garbage collector.
|
| 13 |
+
|
| 14 |
+
## Features
|
| 15 |
+
- **AI Background Removal:** Uses `rembg` (U2-Net) to remove complex backgrounds.
|
| 16 |
+
- **Smart Cropping:**
|
| 17 |
+
- Automatically detects the subject.
|
| 18 |
+
- Cleans up semi-transparent noise.
|
| 19 |
+
- Crops excess body parts for correct head-to-body ratios.
|
| 20 |
+
- Ensures no white borders ("Cover Mode").
|
| 21 |
+
- **Internationalization (i18n):**
|
| 22 |
+
- Full support for 6 languages:
|
| 23 |
+
- ๐จ๐ณ Chinese (ZH)
|
| 24 |
+
- ๐บ๐ธ English (EN)
|
| 25 |
+
- ๐ช๐ธ Spanish (ES)
|
| 26 |
+
- ๐ซ๐ท French (FR)
|
| 27 |
+
- ๐ฏ๐ต Japanese (JA)
|
| 28 |
+
- ๐ฐ๐ท Korean (KO)
|
| 29 |
+
- Dedicated SEO-friendly routes (e.g., `/es`, `/ja`).
|
| 30 |
+
- **Multi-Standard Support:**
|
| 31 |
+
- US/India: 2x2 inch (600x600 px)
|
| 32 |
+
- Europe/UK/China: 35x45 mm
|
| 33 |
+
- Japan: 30x40 mm
|
| 34 |
+
- Canada: 50x70 mm
|
| 35 |
+
- **Queue System:** Strict "One at a time" processing to protect server resources.
|
| 36 |
+
|
| 37 |
+
## API Reference
|
| 38 |
+
|
| 39 |
+
### `POST /process-image`
|
| 40 |
+
|
| 41 |
+
Uploads an image and returns the processed JPEG.
|
| 42 |
+
|
| 43 |
+
**Parameters (Form Data):**
|
| 44 |
+
- `file`: The image file (JPG/PNG).
|
| 45 |
+
- `width`: Target width in pixels (e.g., 600).
|
| 46 |
+
- `height`: Target height in pixels (e.g., 600).
|
| 47 |
+
|
| 48 |
+
**Response:**
|
| 49 |
+
- Returns a binary JPEG stream of the processed image.
|
| 50 |
+
|
| 51 |
+
**Concurrency:**
|
| 52 |
+
- The server implements a **Global Lock**.
|
| 53 |
+
- Only **1 request** is processed at a time.
|
| 54 |
+
- Subsequent requests will wait in a queue until the lock is released.
|
| 55 |
+
|
| 56 |
+
## Architecture
|
| 57 |
+
- **Frontend:** Next.js (React) on Port 3000.
|
| 58 |
+
- Components: `src/components/VisaPhotoMaker.tsx` (Core Logic).
|
| 59 |
+
- Routes: `src/app/[lang]/page.tsx`.
|
| 60 |
+
- **Backend:** FastAPI (Python) on Port 13002.
|
| 61 |
+
- **Process Management:** PM2.
|
| 62 |
+
|
| 63 |
+
## Deployment
|
| 64 |
+
Managed via PM2:
|
| 65 |
+
```bash
|
| 66 |
+
pm2 list
|
| 67 |
+
pm2 restart visa-backend
|
| 68 |
+
pm2 restart visa-frontend
|
| 69 |
+
```
|
docs/MONITORING.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Service Monitoring & Auto-Recovery Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to ensure the Visa Photo Maker services (Frontend & Backend) remain online and automatically recover from crashes or stops.
|
| 4 |
+
|
| 5 |
+
## 1. Watchdog Script
|
| 6 |
+
|
| 7 |
+
A simple bash script `scripts/watchdog.sh` has been created to check the health of your services every minute.
|
| 8 |
+
|
| 9 |
+
- **Location**: `scripts/watchdog.sh`
|
| 10 |
+
- **What it does**:
|
| 11 |
+
1. Checks if **Port 13002** (Backend) is open.
|
| 12 |
+
2. Checks if **Port 13001** (Frontend) is open.
|
| 13 |
+
3. If a port is closed, it triggers `pm2 restart <app_name>`.
|
| 14 |
+
4. Logs all incidents to `watchdog.log` in the project root.
|
| 15 |
+
|
| 16 |
+
## 2. Setup Instructions
|
| 17 |
+
|
| 18 |
+
### Step 1: Make Script Executable
|
| 19 |
+
Run this in your terminal:
|
| 20 |
+
```bash
|
| 21 |
+
chmod +x scripts/watchdog.sh
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Step 2: Test the Script
|
| 25 |
+
Run it manually once to make sure it works (it should be silent if everything is OK):
|
| 26 |
+
```bash
|
| 27 |
+
./scripts/watchdog.sh
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Step 3: Enable Auto-Run (Crontab)
|
| 31 |
+
To make it run every minute automatically:
|
| 32 |
+
|
| 33 |
+
1. Open your crontab editor:
|
| 34 |
+
```bash
|
| 35 |
+
crontab -e
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
2. Add the following line at the bottom of the file:
|
| 39 |
+
*(Update the path if you moved the project)*
|
| 40 |
+
```bash
|
| 41 |
+
* * * * * /home/ubuntu/code/visa_photo_maker/scripts/watchdog.sh
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
3. Save and exit (usually `Ctrl+O`, `Enter`, `Ctrl+X` if using nano).
|
| 45 |
+
|
| 46 |
+
## 3. Viewing Logs
|
| 47 |
+
|
| 48 |
+
To see if the watchdog has restarted anything, check the log file:
|
| 49 |
+
```bash
|
| 50 |
+
tail -f watchdog.log
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## 4. Other Protections Enabled
|
| 54 |
+
|
| 55 |
+
Besides this script, the following PM2 protections are already active:
|
| 56 |
+
|
| 57 |
+
1. **Memory Limit**: Backend restarts if it exceeds **2GB** RAM.
|
| 58 |
+
2. **Backoff Restart**: If the app crashes repeatedly, PM2 waits 100ms+ before restarting to prevent CPU spikes.
|
| 59 |
+
3. **4 Workers**: The backend runs 4 parallel instances to utilize the quad-core CPU.
|
docs/ROADMAP_AND_IDEAS.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Visa Photo Maker: Roadmap & Monetization Ideas
|
| 2 |
+
|
| 3 |
+
This document tracks the current progress of the project and outlines future possibilities for growth and revenue.
|
| 4 |
+
|
| 5 |
+
## ๐ Current Progress (Phase 1: Foundation)
|
| 6 |
+
|
| 7 |
+
### Technical Implementation
|
| 8 |
+
- **AI Core**: Integrated `rembg` (U2-Net) for precise background removal in RAM.
|
| 9 |
+
- **Biometric Logic**: Smart cropping that prioritizes head/shoulder ratios according to international standards.
|
| 10 |
+
- **Manual Control**: Added `react-easy-crop` for user-driven fine-tuning.
|
| 11 |
+
- **Print Engine**: Pure-frontend Canvas logic to generate 4x6 inch (1800x1200px) printable grids.
|
| 12 |
+
- **Concurrency**: `asyncio.Lock` implemented to protect CPU from overloading.
|
| 13 |
+
|
| 14 |
+
### SEO & GEO (Growth)
|
| 15 |
+
- **Multilingual**: Full localization for **ZH, EN, ES, FR, JA, KO** with dedicated routes.
|
| 16 |
+
- **GEO-friendly**: `hreflang` tags, `sitemap.xml`, and `robots.txt` implemented.
|
| 17 |
+
- **Schema.org**: `SoftwareApplication` and `FAQPage` JSON-LD injected for AI search engines.
|
| 18 |
+
- **Trust Building**: Compliance Checklists and Trust Badges integrated into the UI.
|
| 19 |
+
|
| 20 |
+
### Quality Assurance
|
| 21 |
+
- **Unit Testing**: `pytest` suite covering root health, dynamic sizing, manual cropping, and locking.
|
| 22 |
+
- **CI/CD**: GitHub Actions pipeline for automated testing on every push.
|
| 23 |
+
|
| 24 |
+
### ๐ Analytics & Tracking
|
| 25 |
+
- **Umami Analytics**: Implemented for privacy-first traffic monitoring.
|
| 26 |
+
- *Website ID*: `45fc6a7a-d84d-4db9-adfd-e5346a9de470`
|
| 27 |
+
- **Microsoft Clarity**: Implemented for behavioral analysis and heatmaps.
|
| 28 |
+
- *Project ID*: `uu7gwujo4s`
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## ๐ง GEO & Advanced SEO Insights (The Strategy)
|
| 33 |
+
|
| 34 |
+
### 1. GEO (Generative Engine Optimization)
|
| 35 |
+
- **Concept**: Optimizing for AI crawlers (ChatGPT, Claude, Gemini) rather than just traditional search bots.
|
| 36 |
+
- **Implementation**:
|
| 37 |
+
- Reclaimed control of `robots.txt` from Cloudflare ("Self-Managed" mode) to explicitly allow **GPTBot**, **ClaudeBot**, and **Bytespider**.
|
| 38 |
+
- Structured content using JSON-LD Schema to help LLMs understand the tool's utility and privacy promises.
|
| 39 |
+
- **Insight**: AI engines value **Unique Facts** (like RAM-only processing) and **Technical Precision**.
|
| 40 |
+
|
| 41 |
+
### 2. High-Performance UX (Lighthouse 100)
|
| 42 |
+
- **Contrast & Accessibility**: Darkened gray text across all UI components to meet WCAG 2.1 standards, ensuring 100/100 Accessibility score.
|
| 43 |
+
- **Semantic Hierarchy**: Fixed heading levels (H1 -> H2 -> H3) to ensure a logical document flow for crawlers and screen readers.
|
| 44 |
+
- **Speed Index**: Minimized render-blocking assets. Using Emojis as icons provides zero-latency visual feedback.
|
| 45 |
+
|
| 46 |
+
### 3. Portal Architecture
|
| 47 |
+
- **Domain**: `quicktools.dpdns.org`
|
| 48 |
+
- **Hub & Spoke Strategy**: The root `/` acts as a multi-language portal, distributing authority to specialized sub-tools.
|
| 49 |
+
- **Multi-Tool Expansion (Feb 2026)**: Successfully transitioned from a single-tool site to a collection platform with the launch of **Echo: Reddit Assistant**.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## ๐ Current Tools
|
| 54 |
+
|
| 55 |
+
### ๐ธ VisaBerry (Passport Photo)
|
| 56 |
+
- **Status**: Stable / Production
|
| 57 |
+
- **Core**: AI background removal and biometric cropping.
|
| 58 |
+
|
| 59 |
+
### ๐ฃ Echo (Reddit Assistant)
|
| 60 |
+
- **Status**: Beta / Production
|
| 61 |
+
- **Concept**: A Chrome extension helper for high-quality Reddit engagement.
|
| 62 |
+
- **Privacy Focus**: Firebase Auth for stateless session management; no storage of sensitive Reddit credentials.
|
| 63 |
+
- **Refinement**: AI-generated responses are stored anonymously to improve context-aware suggestions.
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## ๐ง Commercial Philosophy (The "Numbers Game")
|
| 68 |
+
|
| 69 |
+
### 1. Value is Just the Entry Ticket
|
| 70 |
+
Building a high-value tool like VisaBerry is the foundation, but **Transaction = Value ร Probability**. In a crowded market, the cost of "being found" and "building trust" is significantly higher than the cost of development.
|
| 71 |
+
|
| 72 |
+
### 2. High Distribution / "Cast a Wide Net"
|
| 73 |
+
The strategy of implementing 8 languages and granular SEO routes is not just about reachโit's about reducing the **Transaction Cost** by meeting the user exactly where they are (their local language and specific search intent).
|
| 74 |
+
|
| 75 |
+
### 3. Business as a Gravity Field
|
| 76 |
+
Instead of chasing individual customers, we build a platform that acts as a "gravity field," attracting high-intent users at the exact moment they need a solution.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## ๐ฐ Monetization Ideas (Phase 2: Revenue)
|
| 81 |
+
|
| 82 |
+
### 1. The "Convenience" Model (Freemium)
|
| 83 |
+
- **Current**: Single photo download is 100% free.
|
| 84 |
+
- **Upsell**: Charge a small fee ($0.99 - $1.99) to unlock the **4x6 Printable Sheet**.
|
| 85 |
+
|
| 86 |
+
### 2. Affiliate Marketing (The "Travel Toolkit" Matrix)
|
| 87 |
+
- **Fintech (High Trust)**: **Wise / Revolut** referral programs. (Essential for currency exchange).
|
| 88 |
+
- **Connectivity (High Velocity)**: **Airalo / Holafly** (eSIM cards). Perfect for instant conversion on the download page.
|
| 89 |
+
- **Security (Recurring)**: **SafetyWing** (Nomad/Travel Insurance). High lifetime value.
|
| 90 |
+
- **Concierge (High Margin)**: **iVisa / VisaHQ**. For users who find the manual process too difficult.
|
| 91 |
+
- **Destination (Upsell)**: **Klook / GetYourGuide**. For tour and ticket bookings.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## ๐ Production Transition & Deployment
|
| 96 |
+
|
| 97 |
+
### 1. Mode Switch (Dev to Prod)
|
| 98 |
+
To optimize performance and minimize memory usage, the frontend has been transitioned to production mode:
|
| 99 |
+
- **Build**: `npm run build` was executed to generate a minimized, optimized bundle.
|
| 100 |
+
- **Run**: Switched PM2 command from `npm run dev` to `npm run start`.
|
| 101 |
+
- **Result**: Faster response times and significantly lower RAM overhead.
|
| 102 |
+
|
| 103 |
+
### 2. Environment Variables & Analytics
|
| 104 |
+
- **Loading**: Next.js automatically detects `.env.local`.
|
| 105 |
+
- **Injection**: Variables starting with `NEXT_PUBLIC_` are injected into the client-side code during the **build** phase.
|
| 106 |
+
- **Maintenance**: If `NEXT_PUBLIC_` variables are changed, a **re-build** is required:
|
| 107 |
+
```bash
|
| 108 |
+
cd frontend
|
| 109 |
+
# Update .env.local
|
| 110 |
+
npm run build
|
| 111 |
+
pm2 restart visa-frontend
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 3. Process Management (PM2)
|
| 115 |
+
Current production processes:
|
| 116 |
+
- `visa-frontend`: Next.js production server (Port 3000).
|
| 117 |
+
- `visa-backend`: FastAPI Uvicorn server (Port 13002).
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## ๐ Maintenance Reminders
|
| 122 |
+
- Keep the `rembg` model updated.
|
| 123 |
+
- Monitor `pm2` memory usage (currently ~200-400MB).
|
| 124 |
+
- Ensure `sitemap.xml` is updated when new languages or routes are added.
|
| 125 |
+
- **Safety First**: NEVER add `save()` to disk for user images to keep the privacy promise.
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
frontend/next.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
async rewrites() {
|
| 5 |
+
return [
|
| 6 |
+
{
|
| 7 |
+
source: '/api/:path*',
|
| 8 |
+
destination: 'http://localhost:13002/:path*',
|
| 9 |
+
},
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export default nextConfig;
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start -p 13001",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"next": "16.1.1",
|
| 13 |
+
"react": "19.2.3",
|
| 14 |
+
"react-dom": "19.2.3",
|
| 15 |
+
"react-easy-crop": "^5.5.6"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@tailwindcss/postcss": "^4",
|
| 19 |
+
"@types/node": "^20",
|
| 20 |
+
"@types/react": "^19",
|
| 21 |
+
"@types/react-dom": "^19",
|
| 22 |
+
"eslint": "^9",
|
| 23 |
+
"eslint-config-next": "16.1.1",
|
| 24 |
+
"tailwindcss": "^4",
|
| 25 |
+
"typescript": "^5"
|
| 26 |
+
},
|
| 27 |
+
"browserslist": [
|
| 28 |
+
"chrome >= 87",
|
| 29 |
+
"firefox >= 78",
|
| 30 |
+
"edge >= 88",
|
| 31 |
+
"safari >= 14"
|
| 32 |
+
]
|
| 33 |
+
}
|
frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
frontend/public/ads.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
google.com, pub-8465309813507611, DIRECT, f08c47fec0942fa0
|
frontend/public/file.svg
ADDED
|
|
frontend/public/globe.svg
ADDED
|
|
frontend/public/manifest.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "QuickTools - VisaBerry",
|
| 3 |
+
"short_name": "VisaBerry",
|
| 4 |
+
"description": "Professional Visa & Passport Photo Maker",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#ffffff",
|
| 8 |
+
"theme_color": "#2563eb",
|
| 9 |
+
"icons": [
|
| 10 |
+
{
|
| 11 |
+
"src": "https://quicktools.dpdns.org/file.svg",
|
| 12 |
+
"sizes": "192x192",
|
| 13 |
+
"type": "image/svg+xml"
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
}
|
frontend/public/next.svg
ADDED
|
|
frontend/public/robots.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
User-agent: *
|
| 2 |
+
Allow: /
|
| 3 |
+
|
| 4 |
+
Sitemap: https://quicktools.dpdns.org/sitemap.xml
|
frontend/public/sitemap.xml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
| 3 |
+
<!-- Portals -->
|
| 4 |
+
<url><loc>https://quicktools.dpdns.org/</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>
|
| 5 |
+
<url><loc>https://quicktools.dpdns.org/en</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 6 |
+
<url><loc>https://quicktools.dpdns.org/de</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 7 |
+
<url><loc>https://quicktools.dpdns.org/es</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 8 |
+
<url><loc>https://quicktools.dpdns.org/fr</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 9 |
+
<url><loc>https://quicktools.dpdns.org/ru</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 10 |
+
<url><loc>https://quicktools.dpdns.org/ja</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 11 |
+
<url><loc>https://quicktools.dpdns.org/ko</loc><lastmod>2026-01-20</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
| 12 |
+
|
| 13 |
+
<!-- VisaBerry Tools -->
|
| 14 |
+
<url><loc>https://quicktools.dpdns.org/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 15 |
+
<url><loc>https://quicktools.dpdns.org/en/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 16 |
+
<url><loc>https://quicktools.dpdns.org/de/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 17 |
+
<url><loc>https://quicktools.dpdns.org/es/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 18 |
+
<url><loc>https://quicktools.dpdns.org/fr/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 19 |
+
<url><loc>https://quicktools.dpdns.org/ru/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 20 |
+
<url><loc>https://quicktools.dpdns.org/ja/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 21 |
+
<url><loc>https://quicktools.dpdns.org/ko/visa-photo</loc><lastmod>2026-01-20</lastmod><changefreq>daily</changefreq><priority>1.0</priority></url>
|
| 22 |
+
</urlset>
|
frontend/public/vercel.svg
ADDED
|
|
frontend/public/window.svg
ADDED
|
|
frontend/src/app/about/page.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from 'next/link';
|
| 2 |
+
|
| 3 |
+
export default function AboutUs() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="min-h-screen bg-white text-gray-900 py-16 px-6 lg:px-8">
|
| 6 |
+
<div className="max-w-3xl mx-auto">
|
| 7 |
+
<Link href="/" className="text-blue-600 hover:underline mb-8 inline-block">← Back to Home</Link>
|
| 8 |
+
<h1 className="text-4xl font-bold mb-8">About QuickTools</h1>
|
| 9 |
+
|
| 10 |
+
<p className="text-lg mb-6 leading-relaxed">
|
| 11 |
+
QuickTools was born from a simple observation: <strong>Getting a standard visa or passport photo should not be a difficult or expensive process.</strong>
|
| 12 |
+
</p>
|
| 13 |
+
|
| 14 |
+
<section className="mb-12">
|
| 15 |
+
<h2 className="text-2xl font-semibold mb-4 text-blue-600">Our Mission</h2>
|
| 16 |
+
<p className="mb-4">
|
| 17 |
+
We aim to provide professional-grade utility tools that are free, fast, and privacy-first. We believe that technology should be accessible to everyone without the need for complex registrations or hidden fees.
|
| 18 |
+
</p>
|
| 19 |
+
</section>
|
| 20 |
+
|
| 21 |
+
<section className="mb-12">
|
| 22 |
+
<h2 className="text-2xl font-semibold mb-4 text-blue-600">Why VisaBerry?</h2>
|
| 23 |
+
<p className="mb-4">
|
| 24 |
+
VisaBerry is our flagship tool designed to solve the common headache of visa photo compliance. Embassies have strict requirements for background color, face position, and image dimensions.
|
| 25 |
+
</p>
|
| 26 |
+
<p className="mb-4">
|
| 27 |
+
Our AI-powered tool automates the background removal and biometric cropping, ensuring that anyone can create a compliant photo from their home using just a smartphone.
|
| 28 |
+
</p>
|
| 29 |
+
</section>
|
| 30 |
+
|
| 31 |
+
<section className="mb-12">
|
| 32 |
+
<h2 className="text-2xl font-semibold mb-4 text-blue-600">Privacy at Core</h2>
|
| 33 |
+
<p className="mb-4">
|
| 34 |
+
Unlike many other "free" tools, we do not monetize your data. Your photos never touch a hard drive; they exist only in the temporary memory of our server while they are being processed.
|
| 35 |
+
</p>
|
| 36 |
+
</section>
|
| 37 |
+
|
| 38 |
+
<div className="bg-gray-50 p-8 rounded-2xl border border-gray-100">
|
| 39 |
+
<h3 className="text-xl font-bold mb-2">Support the Project</h3>
|
| 40 |
+
<p className="mb-4 text-sm text-gray-600">
|
| 41 |
+
QuickTools is maintained by a small team of independent developers. If you find our tools helpful, consider supporting our work to keep the servers running and the tools free for everyone.
|
| 42 |
+
</p>
|
| 43 |
+
<Link href="https://buymeacoffee.com/realberry" target="_blank" className="text-blue-600 font-semibold hover:underline">
|
| 44 |
+
Buy us a coffee →
|
| 45 |
+
</Link>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
}
|
frontend/src/app/de/page.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - Professionelle Online-Tool-Sammlung",
|
| 6 |
+
description: "Einfache, schnelle und datenschutzfreundliche Online-Tools. Entdecken Sie VisaBerry fรผr Visum- und Passfotos.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/de',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 16 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
openGraph: {
|
| 20 |
+
title: "QuickTools - Professionelle Online-Tool-Sammlung",
|
| 21 |
+
description: "Einfache, schnelle und datenschutzfreundliche Online-Tools.",
|
| 22 |
+
url: 'https://quicktools.dpdns.org/de',
|
| 23 |
+
locale: 'de_DE',
|
| 24 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 25 |
+
},
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const LANGS = [
|
| 29 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 30 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 31 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 32 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 33 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 34 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 35 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 36 |
+
];
|
| 37 |
+
|
| 38 |
+
export default function GermanPortalHome() {
|
| 39 |
+
return (
|
| 40 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 41 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 42 |
+
{LANGS.map((l) => (
|
| 43 |
+
<Link key={l.code} href={l.path} className={`px-3 py-1 rounded-full text-xs font-medium border ${l.code === 'de' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}>{l.label}</Link>
|
| 44 |
+
))}
|
| 45 |
+
</div>
|
| 46 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20 text-center">
|
| 47 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 48 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 49 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 50 |
+
<p className="text-xl leading-8 text-gray-600">Einfach ยท Schnell ยท Datenschutz First<br/>Eine wachsende Sammlung nรผtzlicher Tools fรผr alle.</p>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</header>
|
| 54 |
+
<main className="max-w-7xl mx-auto px-6 py-16 lg:px-8">
|
| 55 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
|
| 56 |
+
<Link href="/de/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 57 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 58 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 59 |
+
<p className="text-gray-600 text-sm text-balance">
|
| 60 |
+
<strong>100% Kostenlos</strong>. Professioneller Generator fรผr Visum- und Passfotos. Automatischer Hintergrund und intelligentes Zuschneiden.
|
| 61 |
+
</p>
|
| 62 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Tool starten <span>→</span></div>
|
| 63 |
+
</Link>
|
| 64 |
+
|
| 65 |
+
<Link href="/en/echo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 66 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ฃ</div>
|
| 67 |
+
<h2 className="text-2xl font-bold mb-3">Echo</h2>
|
| 68 |
+
<p className="text-gray-600 text-center text-sm leading-relaxed text-balance">
|
| 69 |
+
<strong>Free Reddit Assistant</strong>. AI-powered reply assistant to help you boost engagement and karma with high-quality responses on Reddit.
|
| 70 |
+
</p>
|
| 71 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Launch Tool <span>→</span></div>
|
| 72 |
+
</Link>
|
| 73 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 74 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-500 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 75 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">Demnรคchst</h2>
|
| 76 |
+
<p className="text-gray-400 text-center text-sm">Weitere leistungsstarke Tools befinden sich in der Entwicklung. Bleiben Sie dran!</p>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</main>
|
| 80 |
+
<footer className="bg-white border-t border-gray-100 py-12">
|
| 81 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-center">
|
| 82 |
+
<p className="text-sm text-gray-500">© 2025 QuickTools. Alle Rechte vorbehalten.<br/>Datenschutz garantiert, keine Registrierung erforderlich.</p>
|
| 83 |
+
</div>
|
| 84 |
+
</footer>
|
| 85 |
+
</div>
|
| 86 |
+
);
|
| 87 |
+
}
|
frontend/src/app/de/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - Kostenloser Visa-Foto-Generator | Passfoto Online Erstellen",
|
| 6 |
+
description: "Erstellen Sie kostenlos biometrische Visa- und Passfotos online. Automatische Hintergrunderstellung und intelligentes Zuschneiden.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/de/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 16 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
openGraph: {
|
| 20 |
+
title: "VisaBerry - Kostenloser Visa-Foto-Generator",
|
| 21 |
+
description: "Erstellen Sie kostenlos biometrische Visa- und Passfotos online.",
|
| 22 |
+
url: 'https://quicktools.dpdns.org/de/visa-photo',
|
| 23 |
+
locale: 'de_DE',
|
| 24 |
+
type: 'article',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
export default function GermanHome() {
|
| 30 |
+
return <VisaPhotoMaker locale="de" />;
|
| 31 |
+
}
|
frontend/src/app/echo/page.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import EchoAssistant from '@/components/EchoAssistant';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "Echo - ๅ
่ดน Reddit ๅๅคๅฉๆ | Reddit ่ฟ่ฅๆๆๅทฅๅ
ท",
|
| 6 |
+
description: "ไฝฟ็จ Echo ๆบ่ฝๅฉๆๆๅๆจ็ Reddit ไบๅจใAI ้ฉฑๅจ็่ฏญๅขๆๅบๅๅค๏ผๅธฎๅฉๆจ่ฝปๆพๅปบ็ซ็คพๅบๅฝฑๅๅๅนถ่ทๅพ Karmaใ",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/echo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/echo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/echo',
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
openGraph: {
|
| 15 |
+
title: "Echo - ๅ
่ดน Reddit AI ๅฉๆ",
|
| 16 |
+
description: "็ฑ AI ้ฉฑๅจ็ Reddit ไบๅจๅขๅผบๅทฅๅ
ทใๆฐๅ้ซ่ดจ้ๅๅค๏ผ่ฝปๆพๅปบ็ซ็คพๅบๅฃฐๆใ",
|
| 17 |
+
url: 'https://quicktools.dpdns.org/echo',
|
| 18 |
+
locale: 'zh_CN',
|
| 19 |
+
type: 'website',
|
| 20 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 21 |
+
},
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export default function ChineseEchoPage() {
|
| 25 |
+
return (
|
| 26 |
+
<main>
|
| 27 |
+
<EchoAssistant locale="zh" />
|
| 28 |
+
<div className="max-w-4xl mx-auto px-4 pb-12 text-center">
|
| 29 |
+
<p className="text-xs text-gray-400 leading-relaxed border-t border-gray-100 pt-8">
|
| 30 |
+
Echo ๆฏไธๆฌพ่ดๅไบๆๅ Reddit ไบๅจ่ดจ้็ๅ
่ดนๅทฅๅ
ทใๆไปฌๅฉ็จ AI ๆๆฏๅธฎๅฉ็จๆทๆด้ซๆๅฐๅไธ็คพๅบ่ฎจ่ฎบใ
|
| 31 |
+
</p>
|
| 32 |
+
</div>
|
| 33 |
+
</main>
|
| 34 |
+
);
|
| 35 |
+
}
|
frontend/src/app/echo/privacy/page.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from 'next/link';
|
| 2 |
+
|
| 3 |
+
export default function EchoPrivacyPage() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans py-16 px-6 lg:px-8">
|
| 6 |
+
<div className="max-w-3xl mx-auto">
|
| 7 |
+
<Link href="/echo" className="text-blue-600 hover:underline mb-8 inline-block">โ Back to Echo</Link>
|
| 8 |
+
<h1 className="text-4xl font-bold mb-8">Echo Privacy Policy</h1>
|
| 9 |
+
|
| 10 |
+
<div className="prose prose-blue max-w-none text-gray-600">
|
| 11 |
+
<p className="mb-4">Last updated: February 8, 2026</p>
|
| 12 |
+
|
| 13 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">1. Introduction</h2>
|
| 14 |
+
<p className="mb-4">
|
| 15 |
+
Echo ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your information when you use the Echo browser extension and website.
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">2. Data Collection</h2>
|
| 19 |
+
<p className="mb-4">
|
| 20 |
+
<strong>Personal Data:</strong> We use Firebase Authentication to manage user login states. While we do not store your Reddit password, we may process unique user identifiers to provide personalized features. We do not collect personal identification information such as your name or email address.
|
| 21 |
+
</p>
|
| 22 |
+
<p className="mb-4">
|
| 23 |
+
<strong>Usage Data:</strong> We collect activity logs (such as clicks and feature interactions) to improve performance and debug errors. We may also collect anonymous usage statistics and store AI-generated responses as well as the final responses provided by users to improve our AI models and service quality.
|
| 24 |
+
</p>
|
| 25 |
+
|
| 26 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">3. Data Processing</h2>
|
| 27 |
+
<p className="mb-4">
|
| 28 |
+
When you request a reply suggestion, the content of the post you are viewing is processed by our AI servers to generate a relevant response. We store the final AI-generated responses to monitor the quality of our service and to further refine our AI models for better context-aware suggestions. However, this data is not linked to your personal identity.
|
| 29 |
+
</p>
|
| 30 |
+
|
| 31 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">4. Third-Party Services</h2>
|
| 32 |
+
<p className="mb-4">
|
| 33 |
+
We use AI models from third-party providers. No personally identifiable information is shared with these providers.
|
| 34 |
+
</p>
|
| 35 |
+
|
| 36 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">5. Your Rights</h2>
|
| 37 |
+
<p className="mb-4">
|
| 38 |
+
Since we do not store personal data, there is no data to export or delete. You can stop all data collection by simply uninstalling the browser extension.
|
| 39 |
+
</p>
|
| 40 |
+
|
| 41 |
+
<h2 className="text-2xl font-bold text-gray-900 mt-8 mb-4">6. Contact Us</h2>
|
| 42 |
+
<p className="mb-4">
|
| 43 |
+
If you have any questions about this Privacy Policy, please contact us through the main QuickTools contact channels.
|
| 44 |
+
</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
);
|
| 49 |
+
}
|
frontend/src/app/en/echo/page.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import EchoAssistant from '@/components/EchoAssistant';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "Echo - Free Reddit Reply Assistant | AI Reddit Growth Tool",
|
| 6 |
+
description: "Boost your Reddit engagement with Echo. AI-powered context-aware replies to help you build community presence and karma effortlessly.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/en/echo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/echo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/echo',
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
openGraph: {
|
| 15 |
+
title: "Echo - Free AI Reddit Assistant",
|
| 16 |
+
description: "AI-powered Reddit engagement tool. Craft high-quality replies and build community presence effortlessly.",
|
| 17 |
+
url: 'https://quicktools.dpdns.org/en/echo',
|
| 18 |
+
locale: 'en_US',
|
| 19 |
+
type: 'website',
|
| 20 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 21 |
+
},
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
export default function EnglishEchoPage() {
|
| 25 |
+
return (
|
| 26 |
+
<main>
|
| 27 |
+
<EchoAssistant locale="en" />
|
| 28 |
+
<div className="max-w-4xl mx-auto px-4 pb-12 text-center">
|
| 29 |
+
<p className="text-xs text-gray-400 leading-relaxed border-t border-gray-100 pt-8">
|
| 30 |
+
Echo is a free tool dedicated to improving Reddit engagement quality. We use AI technology to help users participate in community discussions more efficiently.
|
| 31 |
+
</p>
|
| 32 |
+
</div>
|
| 33 |
+
</main>
|
| 34 |
+
);
|
| 35 |
+
}
|
frontend/src/app/en/page.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - Professional Online Tool Collection",
|
| 6 |
+
description: "A collection of simple, fast, and privacy-first online tools for daily tasks. Explore VisaBerry for passport photos and more.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/en',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "QuickTools - Professional Online Tool Collection",
|
| 22 |
+
description: "A collection of simple, fast, and privacy-first online tools for daily tasks.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/en',
|
| 24 |
+
locale: 'en_US',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LANGS = [
|
| 30 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 31 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 32 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 33 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 34 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 35 |
+
{ code: 'ru', label: 'ะ ัััะบะธะน', path: '/ru' },
|
| 36 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 37 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
export default function EnglishPortalHome() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 43 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 44 |
+
{LANGS.map((l) => (
|
| 45 |
+
<Link
|
| 46 |
+
key={l.code}
|
| 47 |
+
href={l.path}
|
| 48 |
+
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors border
|
| 49 |
+
${l.code === 'en' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}
|
| 50 |
+
>
|
| 51 |
+
{l.label}
|
| 52 |
+
</Link>
|
| 53 |
+
))}
|
| 54 |
+
</div>
|
| 55 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20">
|
| 56 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 57 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 58 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 59 |
+
<p className="text-xl leading-8 text-gray-600">Simple ยท Fast ยท Privacy-First<br/>A growing collection of useful tools for everyone.</p>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</header>
|
| 63 |
+
<main className="mx-auto max-w-7xl px-6 py-16 lg:px-8">
|
| 64 |
+
<div className="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
|
| 65 |
+
<Link href="/en/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 66 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 67 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 68 |
+
<p className="text-gray-600 text-center text-sm leading-relaxed text-balance">
|
| 69 |
+
<strong>100% Free</strong> Professional Visa & Passport photo maker. No registration required. Auto background removal and biometric cropping.
|
| 70 |
+
</p>
|
| 71 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Launch Tool <span>→</span></div>
|
| 72 |
+
</Link>
|
| 73 |
+
|
| 74 |
+
<Link href="/en/echo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 75 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ฃ</div>
|
| 76 |
+
<h2 className="text-2xl font-bold mb-3">Echo</h2>
|
| 77 |
+
<p className="text-gray-600 text-center text-sm leading-relaxed text-balance">
|
| 78 |
+
<strong>Free Reddit Assistant</strong>. AI-powered reply assistant to help you boost engagement and karma with high-quality responses on Reddit.
|
| 79 |
+
</p>
|
| 80 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Launch Tool <span>→</span></div>
|
| 81 |
+
</Link>
|
| 82 |
+
|
| 83 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 84 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-500 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 85 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">Coming Soon</h2>
|
| 86 |
+
<p className="text-gray-500 text-center text-sm">More powerful tools are currently under development. Stay tuned!</p>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
</main>
|
| 90 |
+
</div>
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
|
frontend/src/app/en/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - Free Visa Photo Maker | Passport Photo Generator Online",
|
| 6 |
+
description: "Create standard biometric visa and passport photos for free. Supports US 2x2 inch, Schengen 35x45mm, China, Japan, and more. Automatic background removal and cropping.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/en/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru/visa-photo',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "VisaBerry - Free Visa Photo Maker Online",
|
| 22 |
+
description: "Create standard biometric visa and passport photos for free. AI background removal and automatic cropping.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/en/visa-photo',
|
| 24 |
+
locale: 'en_US',
|
| 25 |
+
type: 'article',
|
| 26 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default function EnglishHome() {
|
| 31 |
+
return (
|
| 32 |
+
<main>
|
| 33 |
+
<VisaPhotoMaker locale="en" />
|
| 34 |
+
<div className="max-w-4xl mx-auto px-4 pb-12 text-center">
|
| 35 |
+
<p className="text-xs text-gray-400 leading-relaxed border-t border-gray-100 pt-8">
|
| 36 |
+
VisaBerry AI provides free online tools to create standard biometric visa and passport photos. Supports US 2x2 inch, Schengen 35x45mm, China, and more with automatic background removal and smart cropping.
|
| 37 |
+
</p>
|
| 38 |
+
</div>
|
| 39 |
+
</main>
|
| 40 |
+
);
|
| 41 |
+
}
|
frontend/src/app/es/page.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - Colecciรณn de Herramientas Online Profesionales",
|
| 6 |
+
description: "Herramientas online simples, rรกpidas y privadas. Explora VisaBerry para fotos de pasaporte y mรกs.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/es',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "QuickTools - Colecciรณn de Herramientas Online Profesionales",
|
| 22 |
+
description: "Herramientas online simples, rรกpidas y privadas.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/es',
|
| 24 |
+
locale: 'es_ES',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LANGS = [
|
| 30 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 31 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 32 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 33 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 34 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 35 |
+
{ code: 'ru', label: 'ะ ัััะบะธะน', path: '/ru' },
|
| 36 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 37 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
export default function SpanishPortalHome() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 43 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 44 |
+
{LANGS.map((l) => (
|
| 45 |
+
<Link key={l.code} href={l.path} className={`px-3 py-1 rounded-full text-xs font-medium border ${l.code === 'es' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}>{l.label}</Link>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20 text-center">
|
| 49 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 50 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 51 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 52 |
+
<p className="text-xl leading-8 text-gray-600">Simple ยท Rรกpido ยท Privacidad Primero<br/>Una colecciรณn creciente de herramientas รบtiles para todos.</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</header>
|
| 56 |
+
<main className="max-w-7xl mx-auto px-6 py-16 lg:px-8">
|
| 57 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
|
| 58 |
+
<Link href="/es/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 59 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 60 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 61 |
+
<p className="text-gray-600 text-sm text-balance">
|
| 62 |
+
<strong>100% Gratis</strong>. Creador profesional de fotos para visas y pasaportes. Fondo automรกtico y recorte inteligente.
|
| 63 |
+
</p>
|
| 64 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Abrir Herramienta <span>→</span></div>
|
| 65 |
+
</Link>
|
| 66 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 67 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-400 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 68 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">Prรณximamente</h2>
|
| 69 |
+
<p className="text-gray-400 text-center text-sm">Se estรกn desarrollando mรกs herramientas. ยกMantente informado!</p>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</main>
|
| 73 |
+
</div>
|
| 74 |
+
);
|
| 75 |
+
}
|
frontend/src/app/es/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - Generador de Fotos de Visa Gratis | Foto de Pasaporte en Lรญnea",
|
| 6 |
+
description: "Crea fotos biomรฉtricas para pasaportes y visas gratis en lรญnea. Compatible con EE. UU., Espaรฑa, Mรฉxico y mรกs. Eliminaciรณn de fondo automรกtica y recorte inteligente.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/es/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru/visa-photo',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "VisaBerry - Generador de Fotos de Visa Gratis",
|
| 22 |
+
description: "Crea fotos biomรฉtricas para pasaportes y visas gratis en lรญnea.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/es/visa-photo',
|
| 24 |
+
locale: 'es_ES',
|
| 25 |
+
type: 'article',
|
| 26 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default function SpanishHome() {
|
| 31 |
+
return <VisaPhotoMaker locale="es" />;
|
| 32 |
+
}
|
frontend/src/app/favicon.ico
ADDED
|
|
frontend/src/app/fr/page.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - Collection d'Outils en Ligne Professionnels",
|
| 6 |
+
description: "Outils en ligne simples, rapides et privรฉs. Dรฉcouvrez VisaBerry pour vos photos de passeport et plus encore.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/fr',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "QuickTools - Collection d'Outils en Ligne Professionnels",
|
| 22 |
+
description: "Outils en ligne simples, rapides et privรฉs.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/fr',
|
| 24 |
+
locale: 'fr_FR',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LANGS = [
|
| 30 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 31 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 32 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 33 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 34 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 35 |
+
{ code: 'ru', label: 'ะ ัััะบะธะน', path: '/ru' },
|
| 36 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 37 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
export default function FrenchPortalHome() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 43 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 44 |
+
{LANGS.map((l) => (
|
| 45 |
+
<Link key={l.code} href={l.path} className={`px-3 py-1 rounded-full text-xs font-medium border ${l.code === 'fr' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}>{l.label}</Link>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20 text-center">
|
| 49 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 50 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 51 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 52 |
+
<p className="text-xl leading-8 text-gray-600">Simple ยท Rapide ยท Confidentialitรฉ Avant Tout<br/>Une collection croissante d'outils utiles pour tous.</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</header>
|
| 56 |
+
<main className="max-w-7xl mx-auto px-6 py-16 lg:px-8">
|
| 57 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
|
| 58 |
+
<Link href="/fr/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 59 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 60 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 61 |
+
<p className="text-gray-600 text-sm text-balance">
|
| 62 |
+
<strong>100% Gratuit</strong>. Crรฉateur professionnel de photos de passeport et de visa. Suppression du fond et recadrage intelligent.
|
| 63 |
+
</p>
|
| 64 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Lancer l'outil <span>→</span></div>
|
| 65 |
+
</Link>
|
| 66 |
+
|
| 67 |
+
<Link href="/en/echo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 68 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ฃ</div>
|
| 69 |
+
<h2 className="text-2xl font-bold mb-3">Echo</h2>
|
| 70 |
+
<p className="text-gray-600 text-center text-sm leading-relaxed text-balance">
|
| 71 |
+
<strong>Free Reddit Assistant</strong>. AI-powered reply assistant to help you boost engagement and karma with high-quality responses on Reddit.
|
| 72 |
+
</p>
|
| 73 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Launch Tool <span>→</span></div>
|
| 74 |
+
</Link>
|
| 75 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 76 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-400 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 77 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">Prochainement</h2>
|
| 78 |
+
<p className="text-gray-400 text-center text-sm">D'autres outils sont en cours de dรฉveloppement. Restez ร l'รฉcoute !</p>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</main>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
}
|
frontend/src/app/fr/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - Gรฉnรฉrateur de Photos de Visa Gratuit | Photo de Passeport en Ligne",
|
| 6 |
+
description: "Crรฉez des photos de passeport et de visa biomรฉtriques gratuitement en ligne. Compatible avec les รtats-Unis, la France, le Canada, etc. Suppression automatique de l'arriรจre-plan.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru/visa-photo',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "VisaBerry - Gรฉnรฉrateur de Photos de Visa Gratuit",
|
| 22 |
+
description: "Crรฉez des photos de passeport et de visa biomรฉtriques gratuitement en ligne.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 24 |
+
locale: 'fr_FR',
|
| 25 |
+
type: 'article',
|
| 26 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default function FrenchHome() {
|
| 31 |
+
return <VisaPhotoMaker locale="fr" />;
|
| 32 |
+
}
|
frontend/src/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #171717;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: var(--font-geist-sans);
|
| 12 |
+
--font-mono: var(--font-geist-mono);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root {
|
| 17 |
+
--background: #0a0a0a;
|
| 18 |
+
--foreground: #ededed;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
background: var(--background);
|
| 24 |
+
color: var(--foreground);
|
| 25 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
+
}
|
frontend/src/app/ja/page.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - ใใญไปๆงใชใณใฉใคใณใใผใซใณใฌใฏใทใงใณ",
|
| 6 |
+
description: "ใทใณใใซใ้ซ้ใใใฉใคใใทใผใ้่ฆใใใชใณใฉใคใณใใผใซใใใถใปใในใใผใๅ็ไฝๆใฎVisaBerryใชใฉใ",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/ja',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "QuickTools - ใใญไปๆงใชใณใฉใคใณใใผใซใณใฌใฏใทใงใณ",
|
| 22 |
+
description: "ใทใณใใซใ้ซ้ใใใฉใคใใทใผใ้่ฆใใใชใณใฉใคใณใใผใซใ",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/ja',
|
| 24 |
+
locale: 'ja_JP',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LANGS = [
|
| 30 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 31 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 32 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 33 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 34 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 35 |
+
{ code: 'ru', label: 'ะ ัััะบะธะน', path: '/ru' },
|
| 36 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 37 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
export default function JapanesePortalHome() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 43 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 44 |
+
{LANGS.map((l) => (
|
| 45 |
+
<Link key={l.code} href={l.path} className={`px-3 py-1 rounded-full text-xs font-medium border ${l.code === 'ja' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}>{l.label}</Link>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20 text-center">
|
| 49 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 50 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 51 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 52 |
+
<p className="text-xl leading-8 text-gray-600">ใทใณใใซ ใป ้ซ้ ใป ใใฉใคใใทใผ้่ฆ<br/>่ชฐใใไฝฟใใใ้ฒๅใ็ถใใไพฟๅฉใชใใผใซใณใฌใฏใทใงใณใ</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</header>
|
| 56 |
+
<main className="max-w-7xl mx-auto px-6 py-16 lg:px-8">
|
| 57 |
+
<div className="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
|
| 58 |
+
<Link href="/ja/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 59 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 60 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 61 |
+
<p className="text-gray-600 text-sm text-balance">
|
| 62 |
+
<strong>100% ๅฎๅ
จ็กๆ</strong>ใใใญไปๆง of ใใถใปใในใใผใๅ็ไฝๆใใผใซใ่ชๅ่ๆฏ้คๅปใจในใใผใใใชใใณใฐใ
|
| 63 |
+
</p>
|
| 64 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">ใใผใซใ่ตทๅ <span>→</span></div>
|
| 65 |
+
</Link>
|
| 66 |
+
|
| 67 |
+
<Link href="/en/echo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 68 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ฃ</div>
|
| 69 |
+
<h2 className="text-2xl font-bold mb-3">Echo</h2>
|
| 70 |
+
<p className="text-gray-600 text-center text-sm leading-relaxed text-balance">
|
| 71 |
+
<strong>Free Reddit Assistant</strong>. AI-powered reply assistant to help you boost engagement and karma with high-quality responses on Reddit.
|
| 72 |
+
</p>
|
| 73 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">Launch Tool <span>→</span></div>
|
| 74 |
+
</Link>
|
| 75 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 76 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-400 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 77 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">่ฟๆฅๅ
ฌ้</h2>
|
| 78 |
+
<p className="text-gray-400 text-center text-sm">ใใใซๅผทๅใชใใผใซใ้็บไธญใงใใใๆฅฝใใฟใซ๏ผ</p>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</main>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
}
|
frontend/src/app/ja/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - ็กๆ ่จผๆๅ็ใกใผใซใผ | ใชใณใฉใคใณใใถใปใในใใผใๅ็ไฝๆ",
|
| 6 |
+
description: "ใชใณใฉใคใณใง็กๆใงใใถใใในใใผใใฎ่จผๆๅ็ใไฝๆใ็ฑณๅฝใๆฅๆฌใไธญๅฝใชใฉใซๅฏพๅฟใ่ชๅ่ๆฏ้คๅปใจใใชใใณใฐๆฉ่ฝไปใใ",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru/visa-photo',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "VisaBerry - ็กๆ ่จผๆๅ็ใกใผใซใผ",
|
| 22 |
+
description: "ใชใณใฉใคใณใง็กๆใงใใถใใในใใผใใฎ่จผๆๅ็ใไฝๆใ",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 24 |
+
locale: 'ja_JP',
|
| 25 |
+
type: 'article',
|
| 26 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default function JapaneseHome() {
|
| 31 |
+
return <VisaPhotoMaker locale="ja" />;
|
| 32 |
+
}
|
frontend/src/app/ko/page.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "QuickTools - ์ ๋ฌธ ์จ๋ผ์ธ ๋๊ตฌ ์ปฌ๋ ์
",
|
| 6 |
+
description: "๊ฐํธํ๊ณ ๋น ๋ฅด๋ฉฐ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ณดํธํ๋ ์จ๋ผ์ธ ๋๊ตฌ. ์ฌ๊ถ ์ฌ์ง ์ ์์ ์ํ VisaBerry ๋ฑ์ ์ดํด๋ณด์ธ์.",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/ko',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "QuickTools - ์ ๋ฌธ ์จ๋ผ์ธ ๋๊ตฌ ์ปฌ๋ ์
",
|
| 22 |
+
description: "๊ฐํธํ๊ณ ๋น ๋ฅด๋ฉฐ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ณดํธํ๋ ์จ๋ผ์ธ ๋๊ตฌ.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/ko',
|
| 24 |
+
locale: 'ko_KR',
|
| 25 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const LANGS = [
|
| 30 |
+
{ code: 'zh', label: 'ไธญๆ', path: '/' },
|
| 31 |
+
{ code: 'en', label: 'English', path: '/en' },
|
| 32 |
+
{ code: 'de', label: 'Deutsch', path: '/de' },
|
| 33 |
+
{ code: 'es', label: 'Espaรฑol', path: '/es' },
|
| 34 |
+
{ code: 'fr', label: 'Franรงais', path: '/fr' },
|
| 35 |
+
{ code: 'ru', label: 'ะ ัััะบะธะน', path: '/ru' },
|
| 36 |
+
{ code: 'ja', label: 'ๆฅๆฌ่ช', path: '/ja' },
|
| 37 |
+
{ code: 'ko', label: 'ํ๊ตญ์ด', path: '/ko' }
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
export default function KoreanPortalHome() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
| 43 |
+
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 justify-end max-w-xs">
|
| 44 |
+
{LANGS.map((l) => (
|
| 45 |
+
<Link key={l.code} href={l.path} className={`px-3 py-1 rounded-full text-xs font-medium border ${l.code === 'ko' ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-600 border-gray-200 hover:bg-gray-100'}`}>{l.label}</Link>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
<header className="relative overflow-hidden bg-gray-50 pt-12 pb-16 sm:pt-16 sm:pb-20 text-center">
|
| 49 |
+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
| 50 |
+
<div className="mx-auto max-w-2xl text-center">
|
| 51 |
+
<h1 className="text-5xl font-extrabold tracking-tight text-blue-600 sm:text-6xl mb-4">QuickTools</h1>
|
| 52 |
+
<p className="text-xl leading-8 text-gray-600">๊ฐํธํจ ยท ์ ์ํจ ยท ๊ฐ์ธ์ ๋ณด ๋ณดํธ<br/>๋ชจ๋๋ฅผ ์ํ ๊ณ์ํด์ ์ฑ์ฅํ๋ ์ ์ฉํ ๋๊ตฌ ๋ชจ์ใ</p>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</header>
|
| 56 |
+
<main className="max-w-7xl mx-auto px-6 py-16 lg:px-8">
|
| 57 |
+
<div className="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
|
| 58 |
+
<Link href="/ko/visa-photo" className="group flex flex-col items-center p-8 bg-white rounded-3xl border border-gray-200 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 group-hover:border-blue-400 text-center">
|
| 59 |
+
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">๐ธ</div>
|
| 60 |
+
<h2 className="text-2xl font-bold mb-3">VisaBerry</h2>
|
| 61 |
+
<p className="text-gray-600 text-sm text-balance">
|
| 62 |
+
<strong>100% ๋ฌด๋ฃ</strong>. ์ ๋ฌธ์ ์ธ ๋น์ ๋ฐ ์ฌ๊ถ ์ฌ์ง ์ ์ ๋๊ตฌ. ์๋ ๋ฐฐ๊ฒฝ ์ ๊ฑฐ ๋ฐ ์ค๋งํธ ํฌ๊ธฐ ์กฐ์ .
|
| 63 |
+
</p>
|
| 64 |
+
<div className="mt-6 flex items-center text-blue-600 font-semibold group-hover:gap-2 transition-all">๋๊ตฌ ์์ <span>→</span></div>
|
| 65 |
+
</Link>
|
| 66 |
+
<div className="flex flex-col items-center justify-center p-8 bg-gray-50 rounded-3xl border border-dashed border-gray-300 opacity-60 text-center">
|
| 67 |
+
<div className="w-16 h-16 bg-gray-200 text-gray-400 rounded-2xl flex items-center justify-center text-3xl mb-6">๐</div>
|
| 68 |
+
<h2 className="text-2xl font-bold mb-3 text-gray-400">๊ณง ์ถ์ ์์ </h2>
|
| 69 |
+
<p className="text-gray-400 text-center text-sm">๋์ฑ ๊ฐ๋ ฅํ ๋๊ตฌ๋ค์ด ๊ฐ๋ฐ ์ค์
๋๋ค. ๊ธฐ๋ํด ์ฃผ์ธ์!</p>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</main>
|
| 73 |
+
</div>
|
| 74 |
+
);
|
| 75 |
+
}
|
frontend/src/app/ko/visa-photo/page.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import VisaPhotoMaker from '@/components/VisaPhotoMaker';
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "VisaBerry - ๋ฌด๋ฃ ๋น์ ์ฌ์ง ๋ฉ์ด์ปค | ์จ๋ผ์ธ ์ฌ๊ถ ์ฌ์ง ์์ฑ๊ธฐ",
|
| 6 |
+
description: "์จ๋ผ์ธ์์ ๋ฌด๋ฃ๋ก ๋น์ ๋ฐ ์ฌ๊ถ ์ฌ์ง์ ๋ง๋์ธ์. ๋ฏธ๊ตญ, ํ๊ตญ, ์ค๊ตญ ๋ฑ ์ง์. ์๋ ๋ฐฐ๊ฒฝ ์ ๊ฑฐ ๋ฐ ํฌ๊ธฐ ์กฐ์ .",
|
| 7 |
+
alternates: {
|
| 8 |
+
canonical: 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 9 |
+
languages: {
|
| 10 |
+
'zh-CN': 'https://quicktools.dpdns.org/visa-photo',
|
| 11 |
+
'en-US': 'https://quicktools.dpdns.org/en/visa-photo',
|
| 12 |
+
'de-DE': 'https://quicktools.dpdns.org/de/visa-photo',
|
| 13 |
+
'es-ES': 'https://quicktools.dpdns.org/es/visa-photo',
|
| 14 |
+
'fr-FR': 'https://quicktools.dpdns.org/fr/visa-photo',
|
| 15 |
+
'ru-RU': 'https://quicktools.dpdns.org/ru/visa-photo',
|
| 16 |
+
'ja-JP': 'https://quicktools.dpdns.org/ja/visa-photo',
|
| 17 |
+
'ko-KR': 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
openGraph: {
|
| 21 |
+
title: "VisaBerry - ๋ฌด๋ฃ ๋น์ ์ฌ์ง ๋ฉ์ด์ปค",
|
| 22 |
+
description: "์จ๋ผ์ธ์์ ๋ฌด๋ฃ๋ก ๋น์ ๋ฐ ์ฌ๊ถ ์ฌ์ง์ ๋ง๋์ธ์.",
|
| 23 |
+
url: 'https://quicktools.dpdns.org/ko/visa-photo',
|
| 24 |
+
locale: 'ko_KR',
|
| 25 |
+
type: 'article',
|
| 26 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
export default function KoreanHome() {
|
| 31 |
+
return <VisaPhotoMaker locale="ko" />;
|
| 32 |
+
}
|
frontend/src/app/layout.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
import Analytics from "@/components/Analytics";
|
| 5 |
+
import Footer from "@/components/Footer";
|
| 6 |
+
|
| 7 |
+
const geistSans = Geist({
|
| 8 |
+
variable: "--font-geist-sans",
|
| 9 |
+
subsets: ["latin"],
|
| 10 |
+
display: 'swap',
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
const geistMono = Geist_Mono({
|
| 14 |
+
variable: "--font-geist-mono",
|
| 15 |
+
subsets: ["latin"],
|
| 16 |
+
display: 'swap',
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
export const metadata: Metadata = {
|
| 20 |
+
title: "QuickTools - Fast, Simple, Privacy-First Online Tools",
|
| 21 |
+
description: "A collection of professional online tools for visa photos, document processing and more. Built with privacy and speed in mind.",
|
| 22 |
+
openGraph: {
|
| 23 |
+
siteName: 'QuickTools',
|
| 24 |
+
type: 'website',
|
| 25 |
+
locale: 'en_US',
|
| 26 |
+
images: [{
|
| 27 |
+
url: 'https://quicktools.dpdns.org/next.svg',
|
| 28 |
+
width: 800,
|
| 29 |
+
height: 600,
|
| 30 |
+
}],
|
| 31 |
+
},
|
| 32 |
+
twitter: {
|
| 33 |
+
card: 'summary_large_image',
|
| 34 |
+
title: 'QuickTools',
|
| 35 |
+
description: 'Fast, Simple, Privacy-First Online Tools',
|
| 36 |
+
images: ['https://quicktools.dpdns.org/next.svg'],
|
| 37 |
+
},
|
| 38 |
+
icons: {
|
| 39 |
+
icon: 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐ธ</text></svg>',
|
| 40 |
+
},
|
| 41 |
+
manifest: '/manifest.json',
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
export default function RootLayout({
|
| 45 |
+
children,
|
| 46 |
+
}: Readonly<{
|
| 47 |
+
children: React.ReactNode;
|
| 48 |
+
}>) {
|
| 49 |
+
// Simple heuristic: in a real app we'd use usePathname but this is a Server Component.
|
| 50 |
+
// Next.js handles 'lang' best via i18n routing, but for this structure, we'll keep it simple.
|
| 51 |
+
return (
|
| 52 |
+
<html lang="en" suppressHydrationWarning>
|
| 53 |
+
<head>
|
| 54 |
+
<link rel="preconnect" href="https://api-gateway.umami.dev" />
|
| 55 |
+
</head>
|
| 56 |
+
<body
|
| 57 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
| 58 |
+
suppressHydrationWarning
|
| 59 |
+
>
|
| 60 |
+
<Analytics />
|
| 61 |
+
<main className="flex-1">
|
| 62 |
+
{children}
|
| 63 |
+
</main>
|
| 64 |
+
<Footer />
|
| 65 |
+
</body>
|
| 66 |
+
</html>
|
| 67 |
+
);
|
| 68 |
+
}
|