Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cc1e49ba6 | |||
| 258eb9464e | |||
| 015386ba4a | |||
| 49abaaf2fd | |||
| 7e9005e338 | |||
| bbcc7d159c | |||
| 39f5bd3986 | |||
| 944d369d43 | |||
| 6aaf8ac44f | |||
| 2097b571f3 | |||
| aeb1814a34 | |||
| 57e093ea54 | |||
| 76e25d03b9 |
@@ -118,151 +118,7 @@ jobs:
|
||||
echo "target=skip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
needs: [prepare, deploy]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
# This handles root package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
|
||||
# Handle apps/web/package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
|
||||
echo "Testing API with token..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $T" https://git.infra.mintel.me/api/v1/user || echo "failed")
|
||||
echo "API returned: $HTTP_CODE"
|
||||
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VALID_TOKEN" ]; then
|
||||
echo "❌ All token/user combinations failed to authenticate!"
|
||||
T=$(echo "$TOKENS" | awk '{print $1}')
|
||||
echo "Attempting open diagnostic login with first token and user mmintel..."
|
||||
echo "$T" | docker login git.infra.mintel.me -u "mmintel" --password-stdin || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "::add-mask::$TOKEN"
|
||||
echo "token=$TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "user=$VALID_USER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
|
||||
# Also export for pnpm to pick it up from env if needed
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: 🏗️ Compile Sibling Monorepo
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "=== Compile Sibling Monorepo ===" >> ci-logs/summary.txt
|
||||
cp .npmrc _at-mintel/
|
||||
cd _at-mintel
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
pnpm --filter "...@mintel/payload-ai" \
|
||||
--filter @mintel/pdf... \
|
||||
--filter @mintel/concept-engine... \
|
||||
--filter @mintel/estimation-engine... \
|
||||
--filter @mintel/meme-generator... \
|
||||
build --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
- name: Install dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Install dependencies (Root) ===" >> ci-logs/summary.txt
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: 🧪 Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Test (@mintel/web) ===" >> ci-logs/summary.txt
|
||||
pnpm --filter @mintel/web test --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: Inspect on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "==== runner state ===="
|
||||
ls -la
|
||||
echo "==== _at-mintel state ===="
|
||||
ls -la _at-mintel || true
|
||||
echo "==== .npmrc check ===="
|
||||
cat .npmrc | sed -E 's/authToken=[a-f0-9]{5}.*/authToken=REDACTED/'
|
||||
echo "==== pnpm debug logs ===="
|
||||
[ -f pnpm-debug.log ] && tail -n 100 pnpm-debug.log || echo "No root pnpm-debug.log"
|
||||
[ -f _at-mintel/pnpm-debug.log ] && tail -n 100 _at-mintel/pnpm-debug.log || echo "No sibling pnpm-debug.log"
|
||||
- name: Extract QA Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "QA Failure Report" > ci-logs/summary.txt
|
||||
ls -R >> ci-logs/summary.txt
|
||||
[ -f pnpm-debug.log ] && cp pnpm-debug.log ci-logs/ || true
|
||||
[ -f _at-mintel/pnpm-debug.log ] && cp _at-mintel/pnpm-debug.log ci-logs/at-mintel-pnpm-debug.log || true
|
||||
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" root@alpha.mintel.me "mkdir -p ~/logs"
|
||||
scp -r -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" ci-logs/* root@alpha.mintel.me:~/logs/ || true
|
||||
rm "$SSH_KEY_FILE"
|
||||
# (JOB 2: QA was removed to reduce pipeline noise)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
@@ -310,6 +166,15 @@ jobs:
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
S3_ENDPOINT=${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
S3_BUCKET=${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||
S3_REGION=${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
S3_PREFIX=${{ secrets.S3_PREFIX || vars.S3_PREFIX || 'mintel.me' }}
|
||||
DATABASE_URI=${{ secrets.DATABASE_URI || (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD && format('postgres://directus:{0}@postgres-db:5432/directus', secrets.TESTING_DIRECTUS_DB_PASSWORD)) || 'postgres://payload:payload@postgres-db:5432/payload' }}
|
||||
PAYLOAD_SECRET=${{ secrets.PAYLOAD_SECRET || 'secret' }}
|
||||
BUILD_ID=${{ github.sha }}
|
||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||
@@ -526,127 +391,41 @@ jobs:
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans
|
||||
"
|
||||
|
||||
- name: 🧹 Purge S3 Cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing rclone..."
|
||||
curl -s -O https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
||||
sudo dpkg -i rclone-current-linux-amd64.deb > /dev/null 2>&1
|
||||
|
||||
echo "Configuring rclone..."
|
||||
cat > rclone.conf <<EOF
|
||||
[mintel-s3]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
secret_access_key = ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
endpoint = ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
region = ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
EOF
|
||||
|
||||
echo "Purging S3 cache for ${{ env.S3_PREFIX }} ..."
|
||||
rclone --config rclone.conf delete mintel-s3:${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }}/cache/ --include "*" || true
|
||||
|
||||
rm rclone.conf rclone-current-linux-amd64.deb
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
post_deploy_checks:
|
||||
name: 🧪 Post-Deploy Verification
|
||||
needs: [prepare, deploy, qa]
|
||||
if: success() || failure() # Run even if QA fails (due to E2E noise)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
for TOKEN_RAW in $TOKENS; do
|
||||
if [ -n "$TOKEN_RAW" ]; then
|
||||
TOKEN=$(echo "$TOKEN_RAW" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$TOKEN"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: 🏥 App Health Check
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: |
|
||||
echo "Waiting for app to start at $DEPLOY_URL ..."
|
||||
for i in {1..30}; do
|
||||
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' "$DEPLOY_URL" 2>&1) || true
|
||||
echo "Attempt $i: HTTP $HTTP_CODE"
|
||||
if [[ "$HTTP_CODE" =~ ^2 ]]; then
|
||||
echo "✅ App is up (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
echo "⏳ Waiting... (got $HTTP_CODE)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ App health check failed after 30 attempts"
|
||||
exit 1
|
||||
- name: 🚀 OG Image Check
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: pnpm --filter @mintel/web check:og
|
||||
- name: 📝 E2E Smoke Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: |
|
||||
# Install system Chromium + dependencies (KLZ pattern)
|
||||
# Ubuntu's default 'chromium' is a snap wrapper, so we use xtradeb PPA for native binary
|
||||
sudo apt-get update && sudo apt-get install -y gnupg wget ca-certificates
|
||||
|
||||
# Setup xtradeb PPA for native chromium
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | sudo gpg --dearmor -o /etc/apt/keyrings/xtradeb.gpg || true
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" | sudo tee /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" | sudo tee /etc/apt/preferences.d/xtradeb
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 || sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||
|
||||
[ -f /usr/bin/chromium ] && sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
pnpm --filter @mintel/web check:forms
|
||||
# (JOB 5: Post-Deploy Verification was removed to reduce pipeline noise)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
notifications:
|
||||
name: 🔔 Notify
|
||||
needs: [prepare, deploy, post_deploy_checks]
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -659,7 +438,7 @@ jobs:
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
|
||||
if [[ "$DEPLOY" == "success" ]] && [[ "$SMOKE" == "success" || "$SMOKE" == "skipped" ]]; then
|
||||
if [[ "$DEPLOY" == "success" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="✅"
|
||||
else
|
||||
@@ -669,5 +448,5 @@ jobs:
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
||||
-F "message=Deploy: $DEPLOY | Smoke: $SMOKE" \
|
||||
-F "message=Deploy: $DEPLOY" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
34
Dockerfile
34
Dockerfile
@@ -7,12 +7,30 @@ ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NPM_TOKEN
|
||||
ARG S3_ENDPOINT
|
||||
ARG S3_ACCESS_KEY
|
||||
ARG S3_SECRET_KEY
|
||||
ARG S3_BUCKET
|
||||
ARG S3_REGION
|
||||
ARG S3_PREFIX
|
||||
ARG DATABASE_URI
|
||||
ARG PAYLOAD_SECRET
|
||||
ARG BUILD_ID
|
||||
|
||||
# Environment variables for Next.js build
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV S3_ENDPOINT=$S3_ENDPOINT
|
||||
ENV S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
ENV S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
ENV S3_BUCKET=$S3_BUCKET
|
||||
ENV S3_REGION=$S3_REGION
|
||||
ENV S3_PREFIX=$S3_PREFIX
|
||||
ENV DATABASE_URI=$DATABASE_URI
|
||||
ENV PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
ENV NEXT_BUILD_WORKERS=1
|
||||
ENV CI=true
|
||||
|
||||
# Copy manifest files specifically for better layer caching
|
||||
@@ -35,10 +53,14 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
rm .npmrc
|
||||
|
||||
# Copy source code
|
||||
# We use BUILD_ID here to ensure that if the commit changes, we always COPY the latest files
|
||||
# even if Docker's metadata-based fingerprinting for the public directory fails.
|
||||
ARG BUILD_ID
|
||||
RUN echo "Building with ID: ${BUILD_ID}"
|
||||
COPY . .
|
||||
|
||||
# Build application (monorepo filter)
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
ENV NODE_OPTIONS="--max_old_space_size=8192"
|
||||
RUN pnpm --filter @mintel/web build
|
||||
|
||||
# Stage 2: Runner
|
||||
@@ -53,6 +75,14 @@ COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
||||
|
||||
# Explicitly copy Payload dynamically generated importMap.js excluded by Standalone tracing
|
||||
COPY --from=builder /app/apps/web/app/(payload)/admin/importMap.js ./apps/web/app/(payload)/admin/importMap.js
|
||||
|
||||
# Fix permissions for the non-root user (Standard uid/gid from base image)
|
||||
# We do this as root before switching users
|
||||
USER root
|
||||
RUN chown -R 1001:65533 /app
|
||||
USER nextjs
|
||||
|
||||
# Start from the app directory to ensure references solve correctly
|
||||
WORKDIR /app/apps/web
|
||||
# In Standalone mode, Next.js expects node_modules and public relative to the server.js
|
||||
WORKDIR /app
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install essential build tools if needed (e.g., for node-gyp)
|
||||
RUN apk add --no-cache libc6-compat python3 make g++
|
||||
RUN apk add --no-cache libc6-compat git python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/paylo
|
||||
import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector";
|
||||
import { ConvertInquiryButton as ConvertInquiryButton_09fd670bce023a947ab66e4eebea5168 } from "@/src/payload/components/ConvertInquiryButton";
|
||||
import { AiAnalyzeButton as AiAnalyzeButton_51a6009c2b12d068d736ffd2b8182c71 } from "@/src/payload/components/AiAnalyzeButton";
|
||||
import { BulkMailButton as BulkMailButton_ebc54eabb8a07878581b0043bd687708 } from "@/src/payload/components/BulkMailButton";
|
||||
import { GanttChartView as GanttChartView_0162b82db971e8f1e27fbdd0aaa2f1f4 } from "@/src/payload/views/GanttChart";
|
||||
import { ChatWindowProvider as ChatWindowProvider_258e2d0901cb901e46c3eeed91676211 } from "@mintel/payload-ai/components/ChatWindow/index";
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from "@payloadcms/storage-s3/client";
|
||||
@@ -106,6 +107,8 @@ export const importMap = {
|
||||
ConvertInquiryButton_09fd670bce023a947ab66e4eebea5168,
|
||||
"@/src/payload/components/AiAnalyzeButton#AiAnalyzeButton":
|
||||
AiAnalyzeButton_51a6009c2b12d068d736ffd2b8182c71,
|
||||
"@/src/payload/components/BulkMailButton#BulkMailButton":
|
||||
BulkMailButton_ebc54eabb8a07878581b0043bd687708,
|
||||
"@/src/payload/views/GanttChart#GanttChartView":
|
||||
GanttChartView_0162b82db971e8f1e27fbdd0aaa2f1f4,
|
||||
"@mintel/payload-ai/components/ChatWindow/index#ChatWindowProvider":
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-8xl leading-[1.1] md:leading-[0.95] mb-6 md:mb-12;
|
||||
@apply text-4xl md:text-6xl lg:text-8xl leading-[1.1] md:leading-[1] lg:leading-[0.95] mb-6 md:mb-8 lg:mb-12;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl md:text-6xl leading-tight mb-4 md:mb-8 mt-12 md:mt-16;
|
||||
@apply text-2xl md:text-4xl lg:text-6xl leading-tight mb-4 md:mb-6 lg:mb-8 mt-12 lg:mt-16;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl md:text-5xl leading-tight mb-3 md:mb-6 mt-8 md:mt-12;
|
||||
@apply text-xl md:text-3xl lg:text-5xl leading-tight mb-3 md:mb-4 lg:mb-6 mt-8 lg:mt-12;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-lg md:text-3xl leading-tight mb-3 md:mb-4 mt-6 md:mt-8;
|
||||
@apply text-lg md:text-2xl lg:text-3xl leading-tight mb-3 lg:mb-4 mt-6 lg:mt-8;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
.lead {
|
||||
@apply text-base md:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
@apply text-base md:text-xl lg:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -92,15 +92,15 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-w-6xl mx-auto px-5 md:px-6 py-8 md:py-12;
|
||||
@apply max-w-6xl mx-auto px-5 md:px-6 lg:px-8 py-8 md:py-10 lg:py-12;
|
||||
}
|
||||
|
||||
.wide-container {
|
||||
@apply max-w-7xl mx-auto px-5 md:px-6 py-10 md:py-16;
|
||||
@apply max-w-7xl mx-auto px-5 md:px-6 lg:px-8 py-10 md:py-12 lg:py-16;
|
||||
}
|
||||
|
||||
.narrow-container {
|
||||
@apply max-w-4xl mx-auto px-5 md:px-6 py-6 md:py-10;
|
||||
@apply max-w-4xl mx-auto px-5 md:px-6 lg:px-8 py-6 md:py-8 lg:py-10;
|
||||
}
|
||||
|
||||
.highlighter-tag {
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function LandingPage() {
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 relative z-10">
|
||||
{[
|
||||
{
|
||||
icon: <ConceptCommunication className="w-8 h-8" />,
|
||||
@@ -136,7 +136,7 @@ export default function LandingPage() {
|
||||
|
||||
{/* Section 04: Target Group */}
|
||||
<Section number="04" title="Für wen" borderTop>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-10 relative z-10">
|
||||
<Reveal>
|
||||
<Card variant="glass" padding="normal" techBorder className="group">
|
||||
<div className="space-y-4 md:space-y-6 relative overflow-hidden">
|
||||
@@ -215,8 +215,8 @@ export default function LandingPage() {
|
||||
},
|
||||
].map((service, i) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.15}>
|
||||
<div className="group py-8 md:py-16 border-b border-slate-100 last:border-b-0 cursor-pointer transition-all duration-500">
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-6 md:gap-16">
|
||||
<div className="group py-8 md:py-12 lg:py-16 border-b border-slate-100 last:border-b-0 cursor-pointer transition-all duration-500">
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-6 md:gap-10 lg:gap-16">
|
||||
{/* Number + Binary */}
|
||||
<div className="shrink-0 flex md:block items-baseline gap-4">
|
||||
<span className="text-4xl md:text-6xl font-black text-slate-100 group-hover:text-slate-200 transition-colors duration-500 tracking-tighter block leading-none">
|
||||
@@ -280,13 +280,13 @@ export default function LandingPage() {
|
||||
<Section number="06" title="Kontakt" borderTop>
|
||||
<div className="relative py-4 md:py-12" id="contact">
|
||||
<Reveal>
|
||||
<div className="space-y-8 md:space-y-16">
|
||||
<H1 className="text-3xl md:text-8xl">
|
||||
<div className="space-y-8 md:space-y-12 lg:space-y-16">
|
||||
<H1 className="text-4xl md:text-6xl lg:text-8xl">
|
||||
Lassen Sie uns <br />
|
||||
<span className="text-slate-400">starten.</span>
|
||||
</H1>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 md:gap-16 items-start relative z-10">
|
||||
<div className="flex flex-col md:flex-row gap-6 md:gap-10 lg:gap-16 items-start relative z-10">
|
||||
<div className="space-y-4 md:space-y-8 flex-1">
|
||||
<LeadText className="text-lg md:text-3xl text-slate-400">
|
||||
Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "}
|
||||
|
||||
@@ -287,6 +287,9 @@ export interface Inquiry {
|
||||
email: string;
|
||||
companyName?: string | null;
|
||||
projectType?: string | null;
|
||||
phone?: string | null;
|
||||
role?: string | null;
|
||||
deadline?: string | null;
|
||||
message?: string | null;
|
||||
isFreeText?: boolean | null;
|
||||
/**
|
||||
@@ -808,7 +811,10 @@ export interface InquiriesSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
email?: T;
|
||||
companyName?: T;
|
||||
phone?: T;
|
||||
role?: T;
|
||||
projectType?: T;
|
||||
deadline?: T;
|
||||
message?: T;
|
||||
isFreeText?: T;
|
||||
config?: T;
|
||||
|
||||
@@ -11,8 +11,11 @@ import configPromise from "@payload-config";
|
||||
export async function sendContactInquiry(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
role?: string;
|
||||
companyName: string;
|
||||
projectType: string;
|
||||
deadline?: string;
|
||||
message: string;
|
||||
isFreeText: boolean;
|
||||
config?: any;
|
||||
@@ -25,8 +28,11 @@ export async function sendContactInquiry(data: {
|
||||
data: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
role: data.role,
|
||||
companyName: data.companyName,
|
||||
projectType: data.projectType,
|
||||
deadline: data.deadline,
|
||||
message: data.message,
|
||||
isFreeText: data.isFreeText,
|
||||
config: data.config || null,
|
||||
|
||||
@@ -62,7 +62,7 @@ export function ContactForm({
|
||||
initialStepIndex = 0,
|
||||
initialState: injectedState,
|
||||
}: ContactFormProps) {
|
||||
const [flow, setFlow] = useState<FlowState>("discovery");
|
||||
const [flow, setFlow] = useState<FlowState>("direct-message");
|
||||
const [stepIndex, setStepIndex] = useState(initialStepIndex);
|
||||
const [state, setState] = useState<FormState>({
|
||||
...initialState,
|
||||
@@ -125,8 +125,11 @@ export function ContactForm({
|
||||
const result = await sendContactInquiry({
|
||||
name: state.name,
|
||||
email: state.email,
|
||||
phone: state.phone,
|
||||
role: state.role,
|
||||
companyName: state.companyName,
|
||||
projectType: state.projectType,
|
||||
deadline: state.deadline,
|
||||
message: state.message,
|
||||
isFreeText: flow === "direct-message",
|
||||
config: flow === "configurator" ? state : undefined,
|
||||
@@ -190,7 +193,7 @@ export function ContactForm({
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSubmitted(false);
|
||||
setFlow("discovery");
|
||||
setFlow("direct-message"); // Reset back to dm
|
||||
setStepIndex(0);
|
||||
setState(initialState);
|
||||
}}
|
||||
@@ -204,7 +207,7 @@ export function ContactForm({
|
||||
);
|
||||
}
|
||||
|
||||
// Gateway Flow
|
||||
// Gateway Flow (Disabled but kept logically if needed, can just not render)
|
||||
if (flow === "discovery") {
|
||||
return (
|
||||
<ContactGateway
|
||||
@@ -225,13 +228,23 @@ export function ContactForm({
|
||||
return (
|
||||
<DirectMessageFlow
|
||||
name={state.name}
|
||||
setName={(v) => updateState({ name: v })}
|
||||
email={state.email}
|
||||
setEmail={(v) => updateState({ email: v })}
|
||||
phone={state.phone}
|
||||
setPhone={(v) => updateState({ phone: v })}
|
||||
role={state.role}
|
||||
setRole={(v) => updateState({ role: v })}
|
||||
company={state.companyName}
|
||||
setCompany={(v) => updateState({ companyName: v })}
|
||||
projectType={state.projectType}
|
||||
setProjectType={(v) => updateState({ projectType: v })}
|
||||
deadline={state.deadline}
|
||||
setDeadline={(v) => updateState({ deadline: v })}
|
||||
message={state.message}
|
||||
setMessage={(v) => updateState({ message: v })}
|
||||
onBack={() => setFlow("discovery")}
|
||||
onSubmit={handleSubmit}
|
||||
onBack={() => setFlow("discovery")} // Can keep, but won't be seen if we hide button
|
||||
onSubmit={() => handleSubmit()}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -88,41 +88,40 @@ export const ContactGateway = ({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||
{/* Configurator Path */}
|
||||
<Reveal width="100%" delay={0.3} direction="up">
|
||||
<button
|
||||
onClick={onChooseConfigurator}
|
||||
disabled={!name}
|
||||
className={cn(
|
||||
"group relative flex flex-col items-start p-8 rounded-3xl border text-left transition-all duration-500 overflow-hidden",
|
||||
name
|
||||
? "bg-slate-900 border-slate-800 text-white shadow-2xl hover:-translate-y-2"
|
||||
: "bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Settings2 size={120} />
|
||||
</div>
|
||||
<div className="relative group p-[1px] rounded-3xl overflow-hidden transition-all duration-500">
|
||||
{/* Disabled Overlay Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-100 dark:from-slate-800 dark:to-slate-900 opacity-50" />
|
||||
|
||||
<Settings2 size={24} className="mb-6 text-green-400" />
|
||||
<h3 className="text-2xl font-bold mb-2 tracking-tight">
|
||||
System-Konfigurator
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 font-medium mb-8 max-w-[280px]">
|
||||
Konfigurieren Sie Ihr Projekt modular für eine präzise
|
||||
Aufwandsschätzung.
|
||||
</p>
|
||||
<button
|
||||
disabled
|
||||
className={cn(
|
||||
"w-full h-full relative flex flex-col items-start p-8 rounded-[23px] border text-left bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5">
|
||||
<Settings2 size={120} />
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold">
|
||||
<span>Sitzung starten</span>
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings2 size={24} className="text-slate-300" />
|
||||
<span className="px-2 py-0.5 rounded-full bg-slate-200 text-[8px] font-bold uppercase tracking-wider text-slate-500">
|
||||
Wartungsmodus
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!name && (
|
||||
<div className="absolute inset-0 bg-slate-50/60 backdrop-blur-[6px] z-20" />
|
||||
)}
|
||||
</button>
|
||||
<h3 className="text-2xl font-bold mb-2 tracking-tight opacity-50">
|
||||
System-Konfigurator
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 font-medium mb-8 max-w-[280px] opacity-70">
|
||||
Dieser Modus wird aktuell optimiert und steht in Kürze wieder
|
||||
zur Verfügung.
|
||||
</p>
|
||||
|
||||
<div className="mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold opacity-30">
|
||||
<span>Konfigurator offline</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Direct Mail Path */}
|
||||
|
||||
@@ -3,13 +3,33 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "../../utils/cn";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Mail, MessageSquare, ArrowLeft, Send } from "lucide-react";
|
||||
import { ProjectType } from "./types";
|
||||
import {
|
||||
Mail,
|
||||
MessageSquare,
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Phone,
|
||||
User as UserIcon,
|
||||
Calendar,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DirectMessageFlowProps {
|
||||
name: string;
|
||||
setName: (val: string) => void;
|
||||
email: string;
|
||||
setEmail: (val: string) => void;
|
||||
phone: string;
|
||||
setPhone: (val: string) => void;
|
||||
role: string;
|
||||
setRole: (val: string) => void;
|
||||
company: string;
|
||||
setCompany: (val: string) => void;
|
||||
projectType: ProjectType;
|
||||
setProjectType: (val: ProjectType) => void;
|
||||
deadline: string;
|
||||
setDeadline: (val: string) => void;
|
||||
message: string;
|
||||
setMessage: (val: string) => void;
|
||||
onBack: () => void;
|
||||
@@ -19,9 +39,19 @@ interface DirectMessageFlowProps {
|
||||
|
||||
export const DirectMessageFlow = ({
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
phone,
|
||||
setPhone,
|
||||
role,
|
||||
setRole,
|
||||
company,
|
||||
setCompany,
|
||||
projectType,
|
||||
setProjectType,
|
||||
deadline,
|
||||
setDeadline,
|
||||
message,
|
||||
setMessage,
|
||||
onBack,
|
||||
@@ -30,72 +60,167 @@ export const DirectMessageFlow = ({
|
||||
}: DirectMessageFlowProps) => {
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto px-4 py-12">
|
||||
<Reveal width="100%" delay={0.1}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors mb-12"
|
||||
>
|
||||
<ArrowLeft size={14} /> Zurück zur Auswahl
|
||||
</button>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-12">
|
||||
<Reveal width="100%" delay={0.2}>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||
DIREKTNACHRICHT // MODUS_AKTIVIERT
|
||||
DIREKTANFRAGE // SCHRITT_01
|
||||
</span>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||
Wie kann ich helfen, {name.split(" ")[0]}?
|
||||
<h2 className="text-3xl md:text-5xl font-bold tracking-tight text-slate-900 line-clamp-2">
|
||||
Lassen Sie uns sprechen.
|
||||
</h2>
|
||||
<p className="text-slate-500 font-medium">
|
||||
Sende mir eine Nachricht zu {company || "deinem Projekt"} und ich
|
||||
melde mich in Kürze.
|
||||
<p className="text-slate-500 font-medium text-lg">
|
||||
Senden Sie mir eine Nachricht und ich melde mich zeitnah zurück.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Email Input */}
|
||||
<div className="space-y-12">
|
||||
{/* Section: Mission Focus */}
|
||||
<Reveal width="100%" delay={0.3} direction="up">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Mail size={12} /> Rückantwort an
|
||||
<Layers size={12} /> Mission // Projekt-Typ
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="ihre@email.de"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{(["website", "web-app", "ecommerce"] as ProjectType[]).map(
|
||||
(type) => {
|
||||
const labels = {
|
||||
website: "Website",
|
||||
"web-app": "Web Application",
|
||||
ecommerce: "E-Commerce",
|
||||
};
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setProjectType(type)}
|
||||
className={cn(
|
||||
"px-6 py-4 rounded-xl border font-bold text-sm transition-all duration-200 text-left",
|
||||
projectType === type
|
||||
? "bg-slate-900 text-white border-slate-900 shadow-lg"
|
||||
: "bg-white border-slate-100 text-slate-500 hover:border-slate-300",
|
||||
)}
|
||||
>
|
||||
{labels[type]}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Message Input */}
|
||||
{/* Section: Identity Details */}
|
||||
<Reveal width="100%" delay={0.4} direction="up">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<UserIcon size={12} /> Ihr Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Max Mustermann"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Layers size={12} /> Unternehmen (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
placeholder="Beispiel GmbH"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Mail size={12} /> E-Mail Adresse
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@firma.de"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Phone size={12} /> Rückruf-Nummer (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+49 123 456789"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Section: Additional Context */}
|
||||
<Reveal width="100%" delay={0.5} direction="up">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<UserIcon size={12} /> Ihre Position
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
placeholder="z.B. Gründer, Marketing Lead..."
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Calendar size={12} /> Zeitfenster
|
||||
</label>
|
||||
<select
|
||||
value={deadline}
|
||||
onChange={(e) => setDeadline(e.target.value)}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="asap">ASAP (Sofort)</option>
|
||||
<option value="1month">< 1 Monat (Priorität)</option>
|
||||
<option value="3months">1-3 Monate (Standard)</option>
|
||||
<option value="flexible">Flexibel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Section: Payload */}
|
||||
<Reveal width="100%" delay={0.6} direction="up">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<MessageSquare size={12} /> Ihre Nachricht
|
||||
<MessageSquare size={12} /> Nachricht // Briefing
|
||||
</label>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Beschreiben Sie kurz Ihr Anliegen..."
|
||||
rows={6}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all resize-none"
|
||||
placeholder="Beschreiben Sie kurz Ihr Anliegen oder hinterlassen Sie einen Link zum Briefing..."
|
||||
rows={5}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all resize-none"
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Reveal width="100%" delay={0.5} direction="up">
|
||||
<Reveal width="100%" delay={0.7} direction="up">
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting || !email || !message}
|
||||
disabled={isSubmitting || !email || !message || !name}
|
||||
className={cn(
|
||||
"group relative w-full py-5 rounded-2xl font-bold text-lg transition-all duration-300 flex items-center justify-center gap-3 overflow-hidden",
|
||||
isSubmitting || !email || !message
|
||||
isSubmitting || !email || !message || !name
|
||||
? "bg-slate-100 text-slate-400 cursor-not-allowed"
|
||||
: "bg-slate-900 text-white shadow-xl hover:shadow-2xl hover:-translate-y-1 active:scale-[0.98]",
|
||||
)}
|
||||
@@ -109,7 +234,7 @@ export const DirectMessageFlow = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Nachricht absenden</span>
|
||||
<span>Anfrage senden</span>
|
||||
<Send
|
||||
size={20}
|
||||
className="group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform"
|
||||
|
||||
@@ -21,15 +21,19 @@ export const getInquiryEmailHtml = (data: any) => `
|
||||
<div class="title">NEUE_ANFRAGE_INPUT</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">ABSENDER</div>
|
||||
<div class="value">${data.name} (${data.email})</div>
|
||||
|
||||
${data.phone ? `<div class="label">TELEFON</div><div class="value">${data.phone}</div>` : ""}
|
||||
${data.role ? `<div class="label">POSITION</div><div class="value">${data.role}</div>` : ""}
|
||||
|
||||
<div class="label">UNTERNEHMEN</div>
|
||||
<div class="value">${data.companyName || "N/A"}</div>
|
||||
|
||||
<div class="label">PROJEKT_TYP</div>
|
||||
<div class="value">${data.projectType}</div>
|
||||
|
||||
${data.deadline ? `<div class="label">ZEITRAUM</div><div class="value">${data.deadline}</div>` : ""}
|
||||
</div>
|
||||
|
||||
${
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface FormState {
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: File | null;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const HeroSection: React.FC = () => {
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1, delay: 0.2 }}
|
||||
className="mb-4 md:mb-10 inline-flex items-center gap-3 md:gap-4 px-4 md:px-6 py-2 border border-slate-100 bg-white/40 backdrop-blur-sm rounded-full"
|
||||
className="mb-4 md:mb-8 lg:mb-10 inline-flex items-center gap-3 md:gap-4 px-4 md:px-6 py-2 border border-slate-100 bg-white/40 backdrop-blur-sm rounded-full"
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
|
||||
@@ -59,7 +59,7 @@ export const HeroSection: React.FC = () => {
|
||||
</motion.div>
|
||||
|
||||
{/* Headline */}
|
||||
<h1 className="text-3xl md:text-[11rem] font-black tracking-tighter leading-[0.9] md:leading-[0.8] text-slate-900 mb-6 md:mb-12 uppercase">
|
||||
<h1 className="text-4xl md:text-8xl lg:text-[10rem] xl:text-[11rem] font-black tracking-tighter leading-[0.9] md:leading-[0.85] lg:leading-[0.8] text-slate-900 mb-6 md:mb-8 lg:mb-12 uppercase">
|
||||
<div className="block">
|
||||
<GlitchText delay={0.5} duration={1.2}>
|
||||
Websites
|
||||
@@ -80,9 +80,9 @@ export const HeroSection: React.FC = () => {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.8 }}
|
||||
className="flex flex-col items-center gap-6 md:gap-12"
|
||||
className="flex flex-col items-center gap-6 md:gap-8 lg:gap-12"
|
||||
>
|
||||
<p className="text-base md:text-3xl text-slate-400 font-medium max-w-2xl leading-relaxed px-4">
|
||||
<p className="text-base md:text-xl lg:text-3xl text-slate-400 font-medium max-w-2xl leading-relaxed px-4">
|
||||
Ein Entwickler. Ein Ansprechpartner.{" "}
|
||||
<br className="hidden md:block" />
|
||||
<span className="text-slate-900 font-bold tracking-tight">
|
||||
|
||||
@@ -196,11 +196,13 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
|
||||
const updateScrollState = React.useCallback(() => {
|
||||
try {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
const doc = iframeRef.current?.contentDocument?.documentElement;
|
||||
if (doc) {
|
||||
const atTop = doc.scrollTop <= 5;
|
||||
if (doc && win) {
|
||||
const scrollTop = doc.scrollTop || win.scrollY || 0;
|
||||
const atTop = scrollTop <= 5;
|
||||
const atBottom =
|
||||
doc.scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||
setScrollState({ atTop, atBottom, isScrollable });
|
||||
}
|
||||
@@ -493,7 +495,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
style.textContent = `
|
||||
*::-webkit-scrollbar { display: none !important; }
|
||||
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
||||
body { background: transparent !important; }
|
||||
`;
|
||||
iframe.contentDocument.head.appendChild(style);
|
||||
setTimeout(updateAmbilight, 600);
|
||||
|
||||
@@ -82,9 +82,9 @@ export const Section: React.FC<SectionProps> = ({
|
||||
|
||||
<div className={cn("relative z-10", containerClass)}>
|
||||
{hasSidebar ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-12 lg:gap-24">
|
||||
{/* Sidebar: Number & Title */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="md:col-span-4 lg:col-span-3">
|
||||
<div className="md:sticky md:top-40 flex flex-col gap-4 md:gap-8">
|
||||
<div className="flex items-end md:flex-col md:items-start gap-6 md:gap-8">
|
||||
{number && (
|
||||
@@ -124,7 +124,7 @@ export const Section: React.FC<SectionProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="md:col-span-9">{children}</div>
|
||||
<div className="md:col-span-8 lg:col-span-9">{children}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
@@ -8,8 +8,8 @@ const envExtension = {
|
||||
// Mail Configuration
|
||||
MAIL_HOST: z.string().optional(),
|
||||
MAIL_PORT: z.coerce.number().optional().default(587),
|
||||
MAIL_USER: z.string().optional(),
|
||||
MAIL_PASS: z.string().optional(),
|
||||
MAIL_USERNAME: z.string().optional(),
|
||||
MAIL_PASSWORD: z.string().optional(),
|
||||
MAIL_FROM: z.string().optional().default("marc@mintel.me"),
|
||||
MAIL_RECIPIENTS: z.string().optional().default("marc@mintel.me"),
|
||||
|
||||
@@ -21,6 +21,14 @@ const envExtension = {
|
||||
.optional()
|
||||
.default("https://analytics.infra.mintel.me"),
|
||||
|
||||
// S3 Storage (Required for importMap at build-time)
|
||||
S3_ENDPOINT: z.string().optional(),
|
||||
S3_ACCESS_KEY: z.string().optional(),
|
||||
S3_SECRET_KEY: z.string().optional(),
|
||||
S3_BUCKET: z.string().optional(),
|
||||
S3_REGION: z.string().optional(),
|
||||
S3_PREFIX: z.string().optional(),
|
||||
|
||||
// Error Tracking
|
||||
SENTRY_DSN: z.string().optional(),
|
||||
};
|
||||
|
||||
@@ -24,8 +24,8 @@ function getTransporter() {
|
||||
port: env.MAIL_PORT,
|
||||
secure: env.MAIL_PORT === 465,
|
||||
auth: {
|
||||
user: env.MAIL_USER,
|
||||
pass: env.MAIL_PASS,
|
||||
user: env.MAIL_USERNAME,
|
||||
pass: env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export const initialState: FormState = {
|
||||
storageExpansion: 0,
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
role: "",
|
||||
message: "",
|
||||
sitemapFile: null,
|
||||
|
||||
@@ -1,89 +1,90 @@
|
||||
export type ProjectType = 'website' | 'web-app';
|
||||
export type ProjectType = "website" | "web-app";
|
||||
|
||||
export interface FormState {
|
||||
projectType: ProjectType;
|
||||
// Company
|
||||
companyName: string;
|
||||
employeeCount: string;
|
||||
// Existing Presence
|
||||
existingWebsite: string;
|
||||
socialMedia: string[];
|
||||
socialMediaUrls: Record<string, string>;
|
||||
existingDomain: string;
|
||||
wishedDomain: string;
|
||||
// Project
|
||||
websiteTopic: string;
|
||||
selectedPages: string[];
|
||||
otherPages: string[];
|
||||
otherPagesCount: number;
|
||||
features: string[];
|
||||
otherFeatures: string[];
|
||||
otherFeaturesCount: number;
|
||||
functions: string[];
|
||||
otherFunctions: string[];
|
||||
otherFunctionsCount: number;
|
||||
apiSystems: string[];
|
||||
otherTech: string[];
|
||||
otherTechCount: number;
|
||||
assets: string[];
|
||||
otherAssets: string[];
|
||||
otherAssetsCount: number;
|
||||
newDatasets: number;
|
||||
cmsSetup: boolean;
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: any; // Using any for File/null to be CLI-compatible
|
||||
contactFiles: any[]; // Using any[] for File[]
|
||||
// Design
|
||||
designVibe: string;
|
||||
colorScheme: string[];
|
||||
references: string[];
|
||||
designWishes: string;
|
||||
// Maintenance
|
||||
expectedAdjustments: string;
|
||||
languagesList: string[];
|
||||
// Timeline
|
||||
deadline: string;
|
||||
// Web App specific
|
||||
targetAudience: string;
|
||||
userRoles: string[];
|
||||
dataSensitivity: string;
|
||||
platformType: string;
|
||||
// Meta
|
||||
dontKnows: string[];
|
||||
visualStaging: string;
|
||||
complexInteractions: string;
|
||||
gridDontKnows?: Record<string, string>;
|
||||
briefingSummary?: string;
|
||||
companyAddress?: string;
|
||||
companyPhone?: string;
|
||||
personName?: string;
|
||||
taxId?: string;
|
||||
designVision?: string;
|
||||
positionDescriptions?: Record<string, string>;
|
||||
sitemap?: {
|
||||
category: string;
|
||||
pages: { title: string; desc: string }[];
|
||||
}[];
|
||||
projectType: ProjectType;
|
||||
// Company
|
||||
companyName: string;
|
||||
employeeCount: string;
|
||||
// Existing Presence
|
||||
existingWebsite: string;
|
||||
socialMedia: string[];
|
||||
socialMediaUrls: Record<string, string>;
|
||||
existingDomain: string;
|
||||
wishedDomain: string;
|
||||
// Project
|
||||
websiteTopic: string;
|
||||
selectedPages: string[];
|
||||
otherPages: string[];
|
||||
otherPagesCount: number;
|
||||
features: string[];
|
||||
otherFeatures: string[];
|
||||
otherFeaturesCount: number;
|
||||
functions: string[];
|
||||
otherFunctions: string[];
|
||||
otherFunctionsCount: number;
|
||||
apiSystems: string[];
|
||||
otherTech: string[];
|
||||
otherTechCount: number;
|
||||
assets: string[];
|
||||
otherAssets: string[];
|
||||
otherAssetsCount: number;
|
||||
newDatasets: number;
|
||||
cmsSetup: boolean;
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: any; // Using any for File/null to be CLI-compatible
|
||||
contactFiles: any[]; // Using any[] for File[]
|
||||
// Design
|
||||
designVibe: string;
|
||||
colorScheme: string[];
|
||||
references: string[];
|
||||
designWishes: string;
|
||||
// Maintenance
|
||||
expectedAdjustments: string;
|
||||
languagesList: string[];
|
||||
// Timeline
|
||||
deadline: string;
|
||||
// Web App specific
|
||||
targetAudience: string;
|
||||
userRoles: string[];
|
||||
dataSensitivity: string;
|
||||
platformType: string;
|
||||
// Meta
|
||||
dontKnows: string[];
|
||||
visualStaging: string;
|
||||
complexInteractions: string;
|
||||
gridDontKnows?: Record<string, string>;
|
||||
briefingSummary?: string;
|
||||
companyAddress?: string;
|
||||
companyPhone?: string;
|
||||
personName?: string;
|
||||
taxId?: string;
|
||||
designVision?: string;
|
||||
positionDescriptions?: Record<string, string>;
|
||||
sitemap?: {
|
||||
category: string;
|
||||
pages: { title: string; desc: string }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
pos: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
isRecurring?: boolean;
|
||||
pos: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
isRecurring?: boolean;
|
||||
}
|
||||
export interface Totals {
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
}
|
||||
|
||||
@@ -62,10 +62,22 @@ export const Inquiries: CollectionConfig = {
|
||||
name: "companyName",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "phone",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "projectType",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "deadline",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "message",
|
||||
type: "textarea",
|
||||
|
||||
18
apps/web/test-env.ts
Normal file
18
apps/web/test-env.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
import { validateMintelEnv } from "@mintel/next-utils";
|
||||
|
||||
const envExtension = {
|
||||
SENTRY_DSN: z.string().url().nullish().or(z.literal("")),
|
||||
};
|
||||
|
||||
process.env.SENTRY_DSN = " ";
|
||||
console.log("Empty Space:");
|
||||
try {
|
||||
validateMintelEnv(envExtension);
|
||||
} catch (e) {}
|
||||
|
||||
process.env.SENTRY_DSN = "\n";
|
||||
console.log("Newline:");
|
||||
try {
|
||||
validateMintelEnv(envExtension);
|
||||
} catch (e) {}
|
||||
@@ -31,6 +31,7 @@ services:
|
||||
# - CI=true
|
||||
- NPM_TOKEN=${NPM_TOKEN:-}
|
||||
- DATABASE_URI=postgres://${postgres_DB_USER:-payload}:${postgres_DB_PASSWORD:-payload}@postgres-db:5432/${postgres_DB_NAME:-payload}
|
||||
- QDRANT_URL=http://qdrant:6333
|
||||
- PAYLOAD_SECRET=dev-secret
|
||||
command: >
|
||||
sh -c "pnpm install --no-frozen-lockfile && pnpm --filter @mintel/web dev"
|
||||
|
||||
36
scripts/visual_debug.mjs
Normal file
36
scripts/visual_debug.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
(async () => {
|
||||
// Launch browser
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Set viewport
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
try {
|
||||
console.log("Navigating to case study page...");
|
||||
await page.goto('https://mintel.me/case-studies/klz-cables', { waitUntil: 'networkidle2' });
|
||||
|
||||
console.log("Waiting a bit for iframes to load...");
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
|
||||
// Let's get console logs from the page too
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
|
||||
console.log("Taking screenshot...");
|
||||
await page.screenshot({ path: '/Users/marcmintel/Projects/mintel.me/screenshot_case_study.png', fullPage: true });
|
||||
|
||||
// Also take a screenshot of the actual showcase HTML page directly to compare
|
||||
console.log("Navigating directly to showcase HTML...");
|
||||
await page.goto('https://mintel.me/showcase/klz-cables.com/power-cables-medium-voltage-cables.html', { waitUntil: 'networkidle2' });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await page.screenshot({ path: '/Users/marcmintel/Projects/mintel.me/screenshot_showcase.png', fullPage: true });
|
||||
|
||||
console.log("Done!");
|
||||
} catch (e) {
|
||||
console.error("Error:", e);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})();
|
||||
1
wget_out.txt
Normal file
1
wget_out.txt
Normal file
@@ -0,0 +1 @@
|
||||
zsh:1: command not found: wget
|
||||
Reference in New Issue
Block a user