diff --git a/frontend/portal/.gitignore b/frontend/portal/.gitignore new file mode 100644 index 0000000..ee81761 --- /dev/null +++ b/frontend/portal/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +out/ +.env*.local diff --git a/frontend/portal/Dockerfile b/frontend/portal/Dockerfile new file mode 100644 index 0000000..1056f58 --- /dev/null +++ b/frontend/portal/Dockerfile @@ -0,0 +1,47 @@ +# 阶段1: 依赖安装 +FROM node:20-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json ./ +COPY package-lock.json* ./ + +RUN npm config set registry https://registry.npmmirror.com && \ + if [ -f package-lock.json ]; then npm ci; else npm install; fi + +# 阶段2: 构建 +FROM node:20-alpine AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN npm run build + +# 阶段3: 生产运行 +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN apk add --no-cache curl + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/frontend/portal/deploy.sh b/frontend/portal/deploy.sh new file mode 100644 index 0000000..6ad3e9d --- /dev/null +++ b/frontend/portal/deploy.sh @@ -0,0 +1,249 @@ +#!/bin/bash + +# ============================================================================= +# Genex Portal - 部署管理脚本 +# ============================================================================= +# +# Docker 命令: +# ./deploy.sh build 仅构建 Docker 镜像 +# ./deploy.sh start 构建并启动服务 (默认) +# ./deploy.sh stop 停止服务 +# ./deploy.sh restart 重启服务 +# ./deploy.sh logs 查看服务日志 +# ./deploy.sh status 查看服务状态 +# ./deploy.sh clean 清理容器和镜像 +# +# Nginx + SSL 命令 (需要 root 权限): +# sudo ./deploy.sh nginx install [domain] [email] 安装 Nginx + SSL 证书 +# sudo ./deploy.sh nginx ssl [domain] [email] 仅申请/续期 SSL 证书 +# ./deploy.sh nginx status 查看 Nginx 状态 +# ./deploy.sh nginx reload 重载 Nginx 配置 +# +# 默认域名: gogenex.com +# 默认邮箱: admin@gogenex.com +# +# ============================================================================= + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +PROJECT_NAME="genex-portal" +IMAGE_NAME="genex-portal" +CONTAINER_NAME="genex-portal" +DEFAULT_PORT=3001 +DEFAULT_DOMAIN="gogenex.com" +DEFAULT_EMAIL="admin@gogenex.com" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +check_docker() { + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装" + exit 1 + fi + if ! docker info &> /dev/null; then + log_error "Docker 服务未运行" + exit 1 + fi +} + +check_docker_compose() { + if docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" + elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + log_error "Docker Compose 未安装" + exit 1 + fi +} + +build() { + log_info "构建 Docker 镜像..." + $COMPOSE_CMD build --no-cache + log_success "镜像构建完成" +} + +start() { + log_info "部署 Genex Portal..." + PORT=${PORT:-$DEFAULT_PORT} + if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + log_warn "端口 $PORT 已被占用,停止旧服务..." + stop + fi + $COMPOSE_CMD up -d --build + log_info "等待服务启动..." + sleep 5 + if docker ps | grep -q $CONTAINER_NAME; then + log_success "服务部署成功! http://localhost:$PORT" + else + log_error "服务启动失败,查看日志: ./deploy.sh logs" + exit 1 + fi +} + +stop() { + log_info "停止服务..." + $COMPOSE_CMD down + log_success "服务已停止" +} + +restart() { stop; start; } + +logs() { $COMPOSE_CMD logs -f; } + +clean() { + log_info "清理容器和镜像..." + $COMPOSE_CMD down --rmi local --volumes --remove-orphans + docker image prune -f + log_success "清理完成" +} + +status() { + log_info "服务状态:" + docker ps -a --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +} + +cmd_nginx_install() { + local domain="${1:-$DEFAULT_DOMAIN}" + local email="${2:-$DEFAULT_EMAIL}" + local conf_file="$SCRIPT_DIR/nginx/${domain}.conf" + + log_info "为 $domain 安装 Nginx + SSL..." + + if [ ! -f "$conf_file" ]; then + log_error "配置文件不存在: $conf_file" + exit 1 + fi + + if [ "$EUID" -ne 0 ]; then + log_error "需要 root 权限: sudo ./deploy.sh nginx install $domain $email" + exit 1 + fi + + # 安装依赖 + log_info "[1/5] 检查依赖..." + command -v nginx &> /dev/null || { apt update && apt install -y nginx; systemctl enable nginx; systemctl start nginx; } + command -v certbot &> /dev/null || apt install -y certbot python3-certbot-nginx + + # HTTP 临时配置 + log_info "[2/5] 部署 HTTP 临时配置..." + mkdir -p /var/www/certbot + cat > /etc/nginx/sites-available/$domain << HTTPEOF +server { + listen 80; + listen [::]:80; + server_name $domain www.$domain; + location /.well-known/acme-challenge/ { root /var/www/certbot; } + location / { + proxy_pass http://127.0.0.1:${DEFAULT_PORT}; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + } +} +HTTPEOF + ln -sf /etc/nginx/sites-available/$domain /etc/nginx/sites-enabled/ + nginx -t && systemctl reload nginx + + # SSL 证书 + log_info "[3/5] 申请 SSL 证书..." + if [ -d "/etc/letsencrypt/live/$domain" ]; then + log_warn "证书已存在,跳过" + else + certbot certonly --webroot --webroot-path=/var/www/certbot \ + --email $email --agree-tos --no-eff-email \ + -d $domain -d www.$domain + log_success "SSL 证书申请成功!" + fi + + # HTTPS 完整配置 + log_info "[4/5] 部署 HTTPS 配置..." + cp "$conf_file" /etc/nginx/sites-available/$domain + nginx -t && systemctl reload nginx + + # 自动续期 + log_info "[5/5] 配置自动续期..." + if [ ! -f /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh ]; then + mkdir -p /etc/letsencrypt/renewal-hooks/deploy + cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'HOOKEOF' +#!/bin/bash +systemctl reload nginx +HOOKEOF + chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh + fi + + log_success "完成! https://$domain" +} + +cmd_nginx_ssl() { + local domain="${1:-$DEFAULT_DOMAIN}" + local email="${2:-$DEFAULT_EMAIL}" + [ "$EUID" -ne 0 ] && { log_error "需要 root 权限"; exit 1; } + mkdir -p /var/www/certbot + if [ -d "/etc/letsencrypt/live/$domain" ]; then + certbot renew --cert-name $domain + else + certbot certonly --webroot --webroot-path=/var/www/certbot \ + --email $email --agree-tos --no-eff-email \ + -d $domain -d www.$domain + fi + log_success "SSL 证书完成" +} + +cmd_nginx_status() { + systemctl is-active nginx &>/dev/null && echo -e "Nginx: ${GREEN}运行中${NC}" || echo -e "Nginx: ${RED}未运行${NC}" + command -v certbot &>/dev/null && certbot certificates 2>/dev/null | grep -E "(Certificate Name|Expiry Date|Domains)" | sed 's/^/ /' +} + +cmd_nginx_reload() { + [ "$EUID" -ne 0 ] && { log_error "需要 root 权限"; exit 1; } + nginx -t && systemctl reload nginx + log_success "Nginx 已重载" +} + +show_help() { + echo "" + echo "Genex Portal 部署脚本" + echo "" + echo "Docker: build | start | stop | restart | logs | status | clean" + echo "Nginx: nginx install [domain] [email] | nginx ssl | nginx status | nginx reload" + echo "" + echo "默认域名: $DEFAULT_DOMAIN 端口: $DEFAULT_PORT" + echo "" +} + +main() { + cd "$SCRIPT_DIR" + case "${1:-start}" in + build) check_docker; check_docker_compose; build ;; + start) check_docker; check_docker_compose; start ;; + stop) check_docker; check_docker_compose; stop ;; + restart) check_docker; check_docker_compose; restart ;; + logs) check_docker; check_docker_compose; logs ;; + status) check_docker; status ;; + clean) check_docker; check_docker_compose; clean ;; + nginx) + case "${2:-install}" in + install) cmd_nginx_install "$3" "$4" ;; + ssl) cmd_nginx_ssl "$3" "$4" ;; + status) cmd_nginx_status ;; + reload) cmd_nginx_reload ;; + *) log_error "未知命令: $2"; exit 1 ;; + esac + ;; + help|--help|-h) show_help ;; + *) log_error "未知命令: $1"; show_help; exit 1 ;; + esac +} + +main "$@" diff --git a/frontend/portal/docker-compose.yml b/frontend/portal/docker-compose.yml new file mode 100644 index 0000000..cf53265 --- /dev/null +++ b/frontend/portal/docker-compose.yml @@ -0,0 +1,26 @@ +services: + portal: + build: + context: . + dockerfile: Dockerfile + image: genex-portal:latest + container_name: genex-portal + restart: unless-stopped + ports: + - "${PORT:-4080}:3000" + environment: + - TZ=Asia/Shanghai + - NODE_ENV=production + - NEXT_TELEMETRY_DISABLED=1 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 40s + networks: + - genex-network + +networks: + genex-network: + driver: bridge diff --git a/frontend/portal/next-env.d.ts b/frontend/portal/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/frontend/portal/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/portal/next.config.ts b/frontend/portal/next.config.ts new file mode 100644 index 0000000..94647ad --- /dev/null +++ b/frontend/portal/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + output: 'standalone', +}; + +export default nextConfig; diff --git a/frontend/portal/nginx/gogenex.com.conf b/frontend/portal/nginx/gogenex.com.conf new file mode 100644 index 0000000..3c3eeff --- /dev/null +++ b/frontend/portal/nginx/gogenex.com.conf @@ -0,0 +1,95 @@ +# ============================================================================= +# Genex Portal - gogenex.com / www.gogenex.com +# 官网门户 (Next.js SSG) +# 部署端口: 3001 (Docker 容器内部 3000) +# ============================================================================= + +# HTTP → HTTPS 重定向 +server { + listen 80; + listen [::]:80; + server_name gogenex.com www.gogenex.com; + + # Let's Encrypt ACME 验证 + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# www → 裸域重定向 +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name www.gogenex.com; + + ssl_certificate /etc/letsencrypt/live/gogenex.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gogenex.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + return 301 https://gogenex.com$request_uri; +} + +# 主站 HTTPS +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name gogenex.com; + + # ---- SSL ---- + ssl_certificate /etc/letsencrypt/live/gogenex.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gogenex.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # ---- 安全头 ---- + add_header Strict-Transport-Security "max-age=63072000" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # ---- Gzip ---- + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml; + + # ---- 日志 ---- + access_log /var/log/nginx/gogenex.com.access.log; + error_log /var/log/nginx/gogenex.com.error.log; + + # ---- Next.js 静态资源 (长期缓存) ---- + location /_next/static/ { + proxy_pass http://127.0.0.1:4080; + proxy_cache_valid 200 365d; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # ---- 反向代理到 Docker ---- + location / { + proxy_pass http://127.0.0.1:4080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # ---- 健康检查 ---- + location = /nginx-health { + access_log off; + return 200 '{"status":"ok","service":"genex-portal-nginx"}'; + add_header Content-Type application/json; + } +} diff --git a/frontend/portal/package-lock.json b/frontend/portal/package-lock.json new file mode 100644 index 0000000..c527215 --- /dev/null +++ b/frontend/portal/package-lock.json @@ -0,0 +1,1021 @@ +{ + "name": "genex-portal", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "genex-portal", + "version": "1.0.0", + "dependencies": { + "framer-motion": "^11.0.0", + "next": "15.1.11", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.1.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.11.tgz", + "integrity": "sha512-yp++FVldfLglEG5LoS2rXhGypPyoSOyY0kxZQJ2vnlYJeP8o318t5DrDu5Tqzr03qAhDWllAID/kOCsXNLcwKw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.9.tgz", + "integrity": "sha512-sQF6MfW4nk0PwMYYq8xNgqyxZJGIJV16QqNDgaZ5ze9YoVzm4/YNx17X0exZudayjL9PF0/5RGffDtzXapch0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.9.tgz", + "integrity": "sha512-fp0c1rB6jZvdSDhprOur36xzQvqelAkNRXM/An92sKjjtaJxjlqJR8jiQLQImPsClIu8amQn+ZzFwl1lsEf62w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.9.tgz", + "integrity": "sha512-77rYykF6UtaXvxh9YyRIKoaYPI6/YX6cy8j1DL5/1XkjbfOwFDfTEhH7YGPqG/ePl+emBcbDYC2elgEqY2e+ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.9.tgz", + "integrity": "sha512-uZ1HazKcyWC7RA6j+S/8aYgvxmDqwnG+gE5S9MhY7BTMj7ahXKunpKuX8/BA2M7OvINLv7LTzoobQbw928p3WA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.9.tgz", + "integrity": "sha512-gQIX1d3ct2RBlgbbWOrp+SHExmtmFm/HSW1Do5sSGMDyzbkYhS2sdq5LRDJWWsQu+/MqpgJHqJT6ORolKp/U1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.9.tgz", + "integrity": "sha512-fJOwxAbCeq6Vo7pXZGDP6iA4+yIBGshp7ie2Evvge7S7lywyg7b/SGqcvWq/jYcmd0EbXdb7hBfdqSQwTtGTPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.9.tgz", + "integrity": "sha512-crfbUkAd9PVg9nGfyjSzQbz82dPvc4pb1TeP0ZaAdGzTH6OfTU9kxidpFIogw0DYIEadI7hRSvuihy2NezkaNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.9", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.9.tgz", + "integrity": "sha512-SBB0oA4E2a0axUrUwLqXlLkSn+bRx9OWU6LheqmRrO53QEAJP7JquKh3kF0jRzmlYOWFZtQwyIWJMEJMtvvDcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.1.11", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.11.tgz", + "integrity": "sha512-UiVJaOGhKST58AadwbFUZThlNBmYhKqaCs8bVtm4plTxsgKq0mJ0zTsp7t7j/rzsbAEj9WcAMdZCztjByi4EoQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.1.11", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.9", + "@next/swc-darwin-x64": "15.1.9", + "@next/swc-linux-arm64-gnu": "15.1.9", + "@next/swc-linux-arm64-musl": "15.1.9", + "@next/swc-linux-x64-gnu": "15.1.9", + "@next/swc-linux-x64-musl": "15.1.9", + "@next/swc-win32-arm64-msvc": "15.1.9", + "@next/swc-win32-x64-msvc": "15.1.9", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/portal/package.json b/frontend/portal/package.json new file mode 100644 index 0000000..52af4d4 --- /dev/null +++ b/frontend/portal/package.json @@ -0,0 +1,23 @@ +{ + "name": "genex-portal", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "next": "15.1.11", + "framer-motion": "^11.0.0" + }, + "devDependencies": { + "typescript": "^5.9.3", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/node": "^20.0.0" + } +} diff --git a/frontend/portal/public/favicon.ico b/frontend/portal/public/favicon.ico new file mode 100644 index 0000000..60c6454 Binary files /dev/null and b/frontend/portal/public/favicon.ico differ diff --git a/frontend/portal/public/favicon.png b/frontend/portal/public/favicon.png new file mode 100644 index 0000000..1cb0132 Binary files /dev/null and b/frontend/portal/public/favicon.png differ diff --git a/frontend/portal/public/logo-full.svg b/frontend/portal/public/logo-full.svg new file mode 100644 index 0000000..456769e --- /dev/null +++ b/frontend/portal/public/logo-full.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + GENEX + + diff --git a/frontend/portal/public/logo.svg b/frontend/portal/public/logo.svg new file mode 100644 index 0000000..189530e --- /dev/null +++ b/frontend/portal/public/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/portal/src/app/about/page.module.css b/frontend/portal/src/app/about/page.module.css new file mode 100644 index 0000000..93f2a99 --- /dev/null +++ b/frontend/portal/src/app/about/page.module.css @@ -0,0 +1,159 @@ +/* ---- About Hero ---- */ +.hero { + padding: 160px 24px 80px; + text-align: center; + background: linear-gradient(180deg, var(--color-primary-surface) 0%, var(--color-surface) 100%); +} + +.title { + font-size: 48px; + font-weight: 700; + margin-bottom: 16px; +} + +.subtitle { + font-size: 20px; + color: var(--color-text-secondary); +} + +/* ---- Vision / Mission ---- */ +.visionSection { + padding: 80px 24px; + max-width: var(--max-width); + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; +} + +.visionCard { + background: var(--color-gray-50); + border-radius: var(--radius-lg); + padding: 48px 40px; +} + +.visionTitle { + font-size: 24px; + font-weight: 600; + margin-bottom: 16px; + color: var(--color-primary); +} + +.visionDesc { + font-size: 16px; + color: var(--color-text-secondary); + line-height: 1.8; +} + +/* ---- Values ---- */ +.values { + padding: 80px 24px; + background: var(--color-gray-50); +} + +.valuesTitle { + font-size: 36px; + font-weight: 700; + text-align: center; + margin-bottom: 48px; +} + +.valuesGrid { + max-width: var(--max-width); + margin: 0 auto; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; +} + +.valueCard { + background: var(--color-surface); + border-radius: var(--radius-lg); + padding: 32px 24px; + text-align: center; + border: 1px solid var(--color-border-light); +} + +.valueIcon { + font-size: 40px; + margin-bottom: 16px; +} + +.valueName { + font-size: 18px; + font-weight: 600; + margin-bottom: 8px; +} + +.valueDesc { + font-size: 14px; + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* ---- Timeline ---- */ +.timeline { + padding: 80px 24px; +} + +.timelineTitle { + font-size: 36px; + font-weight: 700; + text-align: center; + margin-bottom: 64px; +} + +.timelineTrack { + max-width: 700px; + margin: 0 auto; + position: relative; + padding-left: 40px; +} + +.timelineTrack::before { + content: ''; + position: absolute; + left: 11px; + top: 0; + bottom: 0; + width: 2px; + background: var(--color-primary-container); +} + +.timelineItem { + position: relative; + padding-bottom: 40px; +} + +.timelineItem:last-child { + padding-bottom: 0; +} + +.timelineDot { + position: absolute; + left: -33px; + top: 4px; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--color-primary); + border: 3px solid var(--color-primary-surface); +} + +.timelineDate { + font-size: 14px; + font-weight: 600; + color: var(--color-primary); + margin-bottom: 4px; +} + +.timelineText { + font-size: 16px; + color: var(--color-text-secondary); +} + +@media (max-width: 768px) { + .title { font-size: 32px; } + .visionSection { grid-template-columns: 1fr; } + .valuesGrid { grid-template-columns: 1fr 1fr; } +} diff --git a/frontend/portal/src/app/about/page.tsx b/frontend/portal/src/app/about/page.tsx new file mode 100644 index 0000000..57e77c3 --- /dev/null +++ b/frontend/portal/src/app/about/page.tsx @@ -0,0 +1,82 @@ +'use client'; + +import React from 'react'; +import { useT } from '@/i18n'; +import AnimateOnScroll from '@/components/AnimateOnScroll'; +import s from './page.module.css'; + +export default function AboutPage() { + const t = useT(); + + const values = [ + { icon: '\u{1F465}', nameKey: 'about_value_1', descKey: 'about_value_1_desc' }, + { icon: '\u{1F50D}', nameKey: 'about_value_2', descKey: 'about_value_2_desc' }, + { icon: '\u{1F680}', nameKey: 'about_value_3', descKey: 'about_value_3_desc' }, + { icon: '\u{1F3DB}', nameKey: 'about_value_4', descKey: 'about_value_4_desc' }, + ]; + + const milestones = [ + { date: '2025 Q1', key: 'about_milestone_2025_q1' }, + { date: '2025 Q2', key: 'about_milestone_2025_q2' }, + { date: '2025 Q3', key: 'about_milestone_2025_q3' }, + { date: '2025 Q4', key: 'about_milestone_2025_q4' }, + { date: '2026 Q1', key: 'about_milestone_2026_q1' }, + { date: '2026 Q2', key: 'about_milestone_2026_q2' }, + ]; + + return ( + <> +
+ +

{t('about_title')}

+

{t('about_subtitle')}

+
+
+ +
+ +
+

{t('about_vision_title')}

+

{t('about_vision_desc')}

+
+
+ +
+

{t('about_mission_title')}

+

{t('about_mission_desc')}

+
+
+
+ +
+

{t('about_value_title')}

+
+ {values.map((v, i) => ( + +
+
{v.icon}
+

{t(v.nameKey)}

+

{t(v.descKey)}

+
+
+ ))} +
+
+ +
+

{t('about_milestone_title')}

+
+ {milestones.map((m, i) => ( + +
+
+
{m.date}
+
{t(m.key)}
+
+ + ))} +
+
+ + ); +} diff --git a/frontend/portal/src/app/blog/page.tsx b/frontend/portal/src/app/blog/page.tsx new file mode 100644 index 0000000..328fe92 --- /dev/null +++ b/frontend/portal/src/app/blog/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import React from 'react'; +import { useT } from '@/i18n'; +import AnimateOnScroll from '@/components/AnimateOnScroll'; +import s from '../content.module.css'; + +export default function BlogPage() { + const t = useT(); + const posts = [ + { icon: '\u{1F680}', dateKey: 'blog_p1_date', titleKey: 'blog_p1_title', excerptKey: 'blog_p1_excerpt' }, + { icon: '\u{26D3}', dateKey: 'blog_p2_date', titleKey: 'blog_p2_title', excerptKey: 'blog_p2_excerpt' }, + { icon: '\u{1F4B0}', dateKey: 'blog_p3_date', titleKey: 'blog_p3_title', excerptKey: 'blog_p3_excerpt' }, + { icon: '\u{2B50}', dateKey: 'blog_p4_date', titleKey: 'blog_p4_title', excerptKey: 'blog_p4_excerpt' }, + { icon: '\u{1F916}', dateKey: 'blog_p5_date', titleKey: 'blog_p5_title', excerptKey: 'blog_p5_excerpt' }, + { icon: '\u{1F30F}', dateKey: 'blog_p6_date', titleKey: 'blog_p6_title', excerptKey: 'blog_p6_excerpt' }, + ]; + return ( + <> +
+

{t('blog_title')}

+

{t('blog_subtitle')}

+
+
+ {posts.map((p, i) => ( + +
+
{p.icon}
+
+
{t(p.dateKey)}
+

{t(p.titleKey)}

+

{t(p.excerptKey)}

+
+
+
+ ))} +
+ + ); +} diff --git a/frontend/portal/src/app/careers/page.tsx b/frontend/portal/src/app/careers/page.tsx new file mode 100644 index 0000000..a900dd0 --- /dev/null +++ b/frontend/portal/src/app/careers/page.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React from 'react'; +import { useT } from '@/i18n'; +import AnimateOnScroll from '@/components/AnimateOnScroll'; +import s from '../content.module.css'; + +export default function CareersPage() { + const t = useT(); + const jobs = [ + { icon: '\u{1F4BB}', titleKey: 'careers_job1_title', descKey: 'careers_job1_desc' }, + { icon: '\u{26D3}', titleKey: 'careers_job2_title', descKey: 'careers_job2_desc' }, + { icon: '\u{1F4F1}', titleKey: 'careers_job3_title', descKey: 'careers_job3_desc' }, + { icon: '\u{1F6E1}', titleKey: 'careers_job4_title', descKey: 'careers_job4_desc' }, + { icon: '\u{1F4CA}', titleKey: 'careers_job5_title', descKey: 'careers_job5_desc' }, + { icon: '\u{1F3A8}', titleKey: 'careers_job6_title', descKey: 'careers_job6_desc' }, + ]; + return ( + <> +
+

{t('careers_title')}

+

{t('careers_subtitle')}

+
+
+ {jobs.map((j, i) => ( + +
+
{j.icon}
+

{t(j.titleKey)}

+

{t(j.descKey)}

+ {t('careers_apply')} → +
+
+ ))} +
+ + ); +} diff --git a/frontend/portal/src/app/community/page.tsx b/frontend/portal/src/app/community/page.tsx new file mode 100644 index 0000000..d80da2d --- /dev/null +++ b/frontend/portal/src/app/community/page.tsx @@ -0,0 +1,37 @@ +'use client'; + +import React from 'react'; +import { useT } from '@/i18n'; +import AnimateOnScroll from '@/components/AnimateOnScroll'; +import s from '../content.module.css'; + +export default function CommunityPage() { + const t = useT(); + const channels = [ + { icon: '\u{1F4AC}', titleKey: 'community_c1_title', descKey: 'community_c1_desc', link: '#' }, + { icon: '\u{1F426}', titleKey: 'community_c2_title', descKey: 'community_c2_desc', link: '#' }, + { icon: '\u{1F4E2}', titleKey: 'community_c3_title', descKey: 'community_c3_desc', link: '#' }, + { icon: '\u{1F310}', titleKey: 'community_c4_title', descKey: 'community_c4_desc', link: '#' }, + { icon: '\u{1F4F1}', titleKey: 'community_c5_title', descKey: 'community_c5_desc', link: '#' }, + { icon: '\u{1F3AE}', titleKey: 'community_c6_title', descKey: 'community_c6_desc', link: '#' }, + ]; + return ( + <> +
+

{t('community_title')}

+

{t('community_subtitle')}

+
+
+ {channels.map((ch, i) => ( + +
+
{ch.icon}
+

{t(ch.titleKey)}

+

{t(ch.descKey)}

+
+
+ ))} +
+ + ); +} diff --git a/frontend/portal/src/app/contact/page.module.css b/frontend/portal/src/app/contact/page.module.css new file mode 100644 index 0000000..e87873b --- /dev/null +++ b/frontend/portal/src/app/contact/page.module.css @@ -0,0 +1,137 @@ +.hero { + padding: 160px 24px 80px; + text-align: center; + background: linear-gradient(180deg, var(--color-primary-surface) 0%, var(--color-surface) 100%); +} + +.title { + font-size: 48px; + font-weight: 700; + margin-bottom: 16px; +} + +.subtitle { + font-size: 20px; + color: var(--color-text-secondary); +} + +.content { + max-width: var(--max-width); + margin: 0 auto; + padding: 80px 24px; + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 64px; +} + +/* ---- Form ---- */ +.form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.label { + font-size: 14px; + font-weight: 500; + color: var(--color-text-secondary); + margin-bottom: 6px; + display: block; +} + +.input { + width: 100%; + padding: 12px 16px; + font-size: 15px; + font-family: var(--font-family); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + outline: none; + transition: border-color 0.2s; +} + +.input:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-surface); +} + +.textarea { + width: 100%; + padding: 12px 16px; + font-size: 15px; + font-family: var(--font-family); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + outline: none; + min-height: 140px; + resize: vertical; + transition: border-color 0.2s; +} + +.textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-surface); +} + +.submitBtn { + background: var(--color-primary); + color: #fff; + border: none; + border-radius: var(--radius-sm); + padding: 14px 32px; + font-size: 16px; + font-weight: 600; + transition: all 0.2s; + align-self: flex-start; +} + +.submitBtn:hover { + background: var(--color-primary-dark); + box-shadow: var(--shadow-primary); +} + +.successMsg { + font-size: 15px; + color: var(--color-success); + font-weight: 500; +} + +/* ---- Info ---- */ +.info { + display: flex; + flex-direction: column; + gap: 40px; +} + +.infoCard { + display: flex; + gap: 16px; +} + +.infoIcon { + width: 48px; + height: 48px; + border-radius: 14px; + background: var(--color-primary-surface); + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + flex-shrink: 0; +} + +.infoTitle { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; +} + +.infoText { + font-size: 15px; + color: var(--color-text-secondary); +} + +@media (max-width: 768px) { + .title { font-size: 32px; } + .content { grid-template-columns: 1fr; gap: 48px; } +} diff --git a/frontend/portal/src/app/contact/page.tsx b/frontend/portal/src/app/contact/page.tsx new file mode 100644 index 0000000..b9a9c41 --- /dev/null +++ b/frontend/portal/src/app/contact/page.tsx @@ -0,0 +1,83 @@ +'use client'; + +import React, { useState, FormEvent } from 'react'; +import { useT } from '@/i18n'; +import AnimateOnScroll from '@/components/AnimateOnScroll'; +import s from './page.module.css'; + +export default function ContactPage() { + const t = useT(); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + setSubmitted(true); + }; + + return ( + <> +
+ +

{t('contact_title')}

+

{t('contact_subtitle')}

+
+
+ +
+ + {submitted ? ( +

{t('contact_form_success')}

+ ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ +