Compare commits
19 Commits
feature/ai
...
v1.15.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 49abaaf2fd | |||
| 7e9005e338 | |||
| bbcc7d159c | |||
| 39f5bd3986 | |||
| 944d369d43 | |||
| 6aaf8ac44f | |||
| 2097b571f3 | |||
| aeb1814a34 | |||
| 57e093ea54 | |||
| 76e25d03b9 | |||
| 19ef042fb6 | |||
| 6032394ac9 | |||
| 033a9ad65e | |||
| 3f6fa36f9b | |||
| c52a132d62 | |||
| 11f735bbdf | |||
| c81dae0b7b | |||
| 716ece1c6c | |||
| e0ccf1cdfb |
@@ -1,9 +1,13 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
dist
|
||||
*.log
|
||||
.git
|
||||
# Exclude all binary/dependency folders recursively
|
||||
**/node_modules
|
||||
**/.pnpm-store
|
||||
**/.git
|
||||
**/.next
|
||||
**/dist
|
||||
**/out
|
||||
**/*.log
|
||||
|
||||
# Specific exclusions for this project
|
||||
.DS_Store
|
||||
cloned-websites
|
||||
storage
|
||||
@@ -11,3 +15,11 @@ storage
|
||||
verify_ci
|
||||
pnpm_install_log.txt
|
||||
full_tree.json
|
||||
backups
|
||||
data
|
||||
|
||||
# Ensure we don't copy the sibling's build artifacts either
|
||||
_at-mintel/**/node_modules
|
||||
_at-mintel/**/dist
|
||||
_at-mintel/**/.next
|
||||
_at-mintel/.git
|
||||
|
||||
@@ -310,6 +310,13 @@ 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' }}
|
||||
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
|
||||
@@ -325,17 +332,16 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "Re-running docker build with plain progress to capture exact logs..."
|
||||
echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.user }}" --password-stdin > login.log 2>&1
|
||||
echo "${{ steps.discover_token.outputs.token }}" > /tmp/npm_token.txt
|
||||
echo "${{ secrets.NPM_TOKEN }}" > /tmp/npm_token.txt
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg NPM_TOKEN=${{ steps.discover_token.outputs.token }} \
|
||||
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
|
||||
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
||||
--progress plain \
|
||||
-t temp-image . > docker_build_failed.log 2>&1
|
||||
cat login.log >> docker_build_failed.log
|
||||
cat login.log >> docker_build_failed.log || true
|
||||
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
||||
# JOB 4: Deploy
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -527,6 +533,29 @@ 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"
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -56,4 +56,8 @@ apps/web/out/estimations/
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
.turbo
|
||||
.turbo
|
||||
# Manual build artifacts
|
||||
_at-mintel/
|
||||
local_build_*.log
|
||||
*.tar
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -7,11 +7,24 @@ 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 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 SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
ENV CI=true
|
||||
|
||||
@@ -19,24 +32,30 @@ ENV CI=true
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||
COPY apps/web/package.json ./apps/web/package.json
|
||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||
COPY _at-mintel* ./_at-mintel/
|
||||
# Placing it inside /app so relative links like ../../_at-mintel resolve correctly!
|
||||
COPY _at-mintel* /app/_at-mintel/
|
||||
|
||||
|
||||
# Install dependencies with cache mount and dynamic .npmrc (High Fidelity pattern)
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
--mount=type=secret,id=NPM_TOKEN \
|
||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc && \
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\"\${NPM_TOKEN}\"" >> .npmrc && \
|
||||
echo "always-auth=true" >> .npmrc && \
|
||||
cd _at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app/_at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app && pnpm install --no-frozen-lockfile && \
|
||||
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
|
||||
@@ -49,6 +68,14 @@ COPY --from=builder /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder /app/apps/web/.next/standalone ./
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1,120 @@
|
||||
export const importMap = {};
|
||||
import { AiMediaButtons as AiMediaButtons_1d402a78164f07306f77dce953e62e11 } from "@mintel/payload-ai/components/AiMediaButtons";
|
||||
import { OptimizeButton as OptimizeButton_338ff118e214cff355f6d710d1a381fb } from "@mintel/payload-ai/components/OptimizeButton";
|
||||
import { GenerateSlugButton as GenerateSlugButton_5baeea8510d263708dd253e86d55e0b4 } from "@mintel/payload-ai/components/FieldGenerators/GenerateSlugButton";
|
||||
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
|
||||
import { GenerateThumbnailButton as GenerateThumbnailButton_e5e8c00f1c031f15175fef0ff67513dc } from "@mintel/payload-ai/components/FieldGenerators/GenerateThumbnailButton";
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { AiFieldButton as AiFieldButton_9125bc0af442fbb1889d8de3dff98501 } from "@mintel/payload-ai/components/FieldGenerators/AiFieldButton";
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker";
|
||||
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";
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||
|
||||
export const importMap = {
|
||||
"@mintel/payload-ai/components/AiMediaButtons#AiMediaButtons":
|
||||
AiMediaButtons_1d402a78164f07306f77dce953e62e11,
|
||||
"@mintel/payload-ai/components/OptimizeButton#OptimizeButton":
|
||||
OptimizeButton_338ff118e214cff355f6d710d1a381fb,
|
||||
"@mintel/payload-ai/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton":
|
||||
GenerateSlugButton_5baeea8510d263708dd253e86d55e0b4,
|
||||
"@/src/payload/components/TagSelector#default":
|
||||
default_76cec558bd86098fa1dab70b12eb818f,
|
||||
"@mintel/payload-ai/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton":
|
||||
GenerateThumbnailButton_e5e8c00f1c031f15175fef0ff67513dc,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
|
||||
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
|
||||
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
|
||||
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient":
|
||||
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton":
|
||||
AiFieldButton_9125bc0af442fbb1889d8de3dff98501,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
|
||||
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
|
||||
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
|
||||
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
|
||||
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
|
||||
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
|
||||
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
|
||||
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
|
||||
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
|
||||
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
|
||||
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
|
||||
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
|
||||
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
|
||||
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
|
||||
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
|
||||
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
|
||||
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
|
||||
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
|
||||
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
|
||||
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
|
||||
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@/src/payload/components/ColorPicker#default":
|
||||
default_2ebf44fdf8ebc607cf0de30cff485248,
|
||||
"@/src/payload/components/IconSelector#default":
|
||||
default_a1c6da8fb7dd9846a8b07123ff256d09,
|
||||
"@/src/payload/components/ConvertInquiryButton#ConvertInquiryButton":
|
||||
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":
|
||||
ChatWindowProvider_258e2d0901cb901e46c3eeed91676211,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler":
|
||||
S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
"@payloadcms/next/rsc#CollectionCards":
|
||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
|
||||
@@ -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{" "}
|
||||
|
||||
@@ -10,17 +10,21 @@ const dirname = path.dirname(filename);
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
serverExternalPackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/payload-ai',
|
||||
'@mintel/pdf',
|
||||
'canvas',
|
||||
'sharp',
|
||||
'puppeteer',
|
||||
'require-in-the-middle',
|
||||
'import-in-the-middle' // Sentry 10+ instrumentation dependencies
|
||||
],
|
||||
transpilePackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/meme-generator',
|
||||
'@mintel/payload-ai',
|
||||
'@mintel/pdf',
|
||||
'@mintel/thumbnail-generator'
|
||||
],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "pnpm run seed:context && next dev --webpack --hostname 0.0.0.0",
|
||||
"dev:native": "DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "node --import tsx --experimental-loader ./ignore-css.mjs ./seed-context.ts",
|
||||
"build": "next build --webpack",
|
||||
"build": "payload generate:importmap && next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src scripts video",
|
||||
"test": "echo \"No tests configured\"",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
|
||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||
"@mintel/payload-ai": "^1.9.15",
|
||||
"@mintel/payload-ai": "link:../../../at-mintel/packages/payload-ai",
|
||||
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
|
||||
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
||||
"@next/mdx": "^16.1.6",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,17 +22,34 @@ import { CrmContacts } from "./src/payload/collections/CrmContacts";
|
||||
import { CrmInteractions } from "./src/payload/collections/CrmInteractions";
|
||||
import { CrmTopics } from "./src/payload/collections/CrmTopics";
|
||||
import { Projects } from "./src/payload/collections/Projects";
|
||||
import { payloadChatPlugin } from "@mintel/payload-ai";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
const isCLI =
|
||||
process.argv.includes("migrate") ||
|
||||
process.argv.includes("generate:importmap");
|
||||
let aiPlugin: any;
|
||||
if (!isCLI) {
|
||||
const { payloadChatPlugin } = await import("@mintel/payload-ai");
|
||||
aiPlugin = payloadChatPlugin({
|
||||
enabled: true,
|
||||
mcpServers: [],
|
||||
renderChatBubble: false, // disable dynamic injection since it's added statically below
|
||||
});
|
||||
}
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
providers: [
|
||||
"@mintel/payload-ai/components/ChatWindow/index#ChatWindowProvider",
|
||||
],
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
Users,
|
||||
@@ -80,34 +97,32 @@ export default buildConfig({
|
||||
connectionString:
|
||||
process.env.DATABASE_URI || process.env.POSTGRES_URI || "",
|
||||
},
|
||||
push: false,
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
? [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||
},
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || "",
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
bucket: process.env.S3_BUCKET || "",
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
]
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
payloadChatPlugin({
|
||||
enabled: true,
|
||||
mcpServers: [],
|
||||
}),
|
||||
...(aiPlugin ? [aiPlugin] : []),
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
|
||||
@@ -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,8 +21,16 @@ 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().url().optional(),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
@@ -31,7 +31,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
@@ -43,7 +43,7 @@ export const ArticleBlockquoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ArticleMemeBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für alt ein.",
|
||||
@@ -37,7 +37,7 @@ export const ArticleMemeBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
@@ -39,7 +39,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
@@ -51,7 +51,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
@@ -63,7 +63,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
@@ -75,7 +75,7 @@ export const ArticleQuoteBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für sourceUrl ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
description: "e.g. 53% or 2.5M€",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -38,7 +38,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -50,7 +50,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
@@ -62,7 +62,7 @@ export const BoldNumberBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für sourceUrl ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ButtonBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -38,7 +38,7 @@ export const CarouselBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
description: "Optional overarching description for the comparison.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeLabel ein.",
|
||||
@@ -57,7 +57,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeText ein.",
|
||||
@@ -71,7 +71,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für positiveLabel ein.",
|
||||
@@ -84,7 +84,7 @@ export const ComparisonRowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für positiveText ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramFlowBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramGanttBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramPieBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramSequenceBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramStateBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const DiagramTimelineBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ExternalLinkBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const H2Block: MintelBlock = {
|
||||
description: "Geben Sie den Text für die H2-Überschrift ein.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export const H3Block: MintelBlock = {
|
||||
description: "Geben Sie den Text für die H3-Überschrift ein.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const HeadingBlock: MintelBlock = {
|
||||
description: "Der Text der Überschrift.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export const IconListBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
@@ -56,7 +56,7 @@ export const IconListBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für description ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ImageTextBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
description: "The strong headline for the Call-to-Action",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -40,7 +40,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
description: "The value proposition text.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -53,7 +53,7 @@ export const LeadMagnetBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für buttonText ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const LeadParagraphBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const MarkerBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für text ein.",
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MemeCardBlock: MintelBlock = {
|
||||
"The template ID from memegen.link (e.g. 'drake', 'disastergirl')",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -42,7 +42,7 @@ export const MemeCardBlock: MintelBlock = {
|
||||
"Pipe-separated captions for the meme (e.g. 'Legacy Code|Mintel Stack'). Maximum 6 words per line.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ export const MermaidBlock: MintelBlock = {
|
||||
description: "Optional title displayed above the diagram.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -59,7 +59,7 @@ export const MermaidBlock: MintelBlock = {
|
||||
"The raw Mermaid.js syntax (e.g. graph TD... shadowing, loops, etc.).",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ export const MetricBarBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -44,7 +44,7 @@ export const MetricBarBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für unit ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ParagraphBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const PerformanceChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const PremiumComparisonChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
@@ -30,7 +30,7 @@ export const PremiumComparisonChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für subtitle ein.",
|
||||
@@ -47,7 +47,7 @@ export const PremiumComparisonChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -75,7 +75,7 @@ export const PremiumComparisonChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für unit ein.",
|
||||
@@ -95,7 +95,7 @@ export const PremiumComparisonChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für description ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const RevenueLossCalculatorBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SectionBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const StatsDisplayBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -31,7 +31,7 @@ export const StatsDisplayBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für value ein.",
|
||||
@@ -43,7 +43,7 @@ export const StatsDisplayBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für subtext ein.",
|
||||
|
||||
@@ -23,7 +23,7 @@ export const StatsGridBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
@@ -36,7 +36,7 @@ export const StatsGridBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für value ein.",
|
||||
|
||||
@@ -26,7 +26,7 @@ export const TLDRBlock: MintelBlock = {
|
||||
description: "The summary content for the TLDR box.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const TrackedLinkBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const WaterfallChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
@@ -35,7 +35,7 @@ export const WaterfallChartBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
|
||||
@@ -36,7 +36,7 @@ export const WebVitalsScoreBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für description ein.",
|
||||
|
||||
@@ -31,7 +31,7 @@ export const YouTubeEmbedBlock: MintelBlock = {
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
import { bulkMailEndpointHandler } from "../endpoints/bulkMailEndpoint";
|
||||
export const CrmContacts: CollectionConfig = {
|
||||
slug: "crm-contacts",
|
||||
labels: {
|
||||
@@ -12,7 +12,21 @@ export const CrmContacts: CollectionConfig = {
|
||||
group: "CRM",
|
||||
description:
|
||||
"Contacts are the individual people linked to an Account. A person should only be created once and can be assigned to a company here.",
|
||||
components: {
|
||||
views: {
|
||||
list: {
|
||||
actions: ["@/src/payload/components/BulkMailButton#BulkMailButton"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: "/bulk-mail",
|
||||
method: "post",
|
||||
handler: bulkMailEndpointHandler,
|
||||
},
|
||||
],
|
||||
access: {
|
||||
read: ({ req: { user } }) => Boolean(user),
|
||||
create: ({ req: { user } }) => Boolean(user),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { replicateMediaHandler } from "@mintel/payload-ai/endpoints/replicateMediaEndpoint.js";
|
||||
import { replicateMediaHandler } from "@mintel/payload-ai/endpoints/replicateMediaEndpoint";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
@@ -39,7 +39,7 @@ export const Posts: CollectionConfig = {
|
||||
position: "sidebar",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/GenerateSlugButton#GenerateSlugButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -100,7 +100,7 @@ export const Posts: CollectionConfig = {
|
||||
position: "sidebar",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/GenerateThumbnailButton#GenerateThumbnailButton",
|
||||
"@mintel/payload-ai/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
147
apps/web/src/payload/components/BulkMailButton.tsx
Normal file
147
apps/web/src/payload/components/BulkMailButton.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useSelection, Button, toast } from "@payloadcms/ui";
|
||||
|
||||
export const BulkMailButton: React.FC = () => {
|
||||
const { selected } = useSelection();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isTest, setIsTest] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const selectedCount = Object.keys(selected).length;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleBulkMail = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const contactIds = Object.keys(selected);
|
||||
|
||||
const response = await fetch("/api/crm-contacts/bulk-mail", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contactIds,
|
||||
instructions: prompt,
|
||||
isTest,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
`Successfully sent emails to ${contactIds.length} contacts.`,
|
||||
);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
toast.error(`Failed: ${data.error}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(`An error occurred: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "inline-block", marginLeft: "1rem" }}>
|
||||
<Button
|
||||
onClick={() => setIsOpen(true)}
|
||||
buttonStyle="primary"
|
||||
size="small"
|
||||
>
|
||||
🤖 AI Bulk Mail ({selectedCount})
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
zIndex: 9999,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "var(--theme-elevation-100, #fff)",
|
||||
color: "var(--theme-text, #000)",
|
||||
padding: "2rem",
|
||||
borderRadius: "8px",
|
||||
width: "500px",
|
||||
maxWidth: "90vw",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ marginTop: 0 }}>AI Bulk Mail</h2>
|
||||
<p>
|
||||
Generate and send personalized emails to {selectedCount} contacts.
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||
<strong>Prompt / Instructions for AI</strong>
|
||||
</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="e.g. Mache ein Angebot für ein neues Messe-Design"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100px",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid var(--theme-elevation-400, #ccc)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label
|
||||
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isTest}
|
||||
onChange={(e) => setIsTest(e.target.checked)}
|
||||
/>
|
||||
<strong>Test Mode</strong> (Send all emails to yourself)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1rem",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={() => setIsOpen(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleBulkMail} disabled={isLoading || !prompt}>
|
||||
{isLoading ? "Sending..." : "Send Emails"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
191
apps/web/src/payload/endpoints/bulkMailEndpoint.ts
Normal file
191
apps/web/src/payload/endpoints/bulkMailEndpoint.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { PayloadRequest } from "payload";
|
||||
|
||||
export const bulkMailEndpointHandler = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
if (req.body) {
|
||||
try {
|
||||
body = (await req.json?.()) || {};
|
||||
} catch (_e) {
|
||||
body = req.body;
|
||||
}
|
||||
}
|
||||
|
||||
const { contactIds, instructions, isTest } = body;
|
||||
|
||||
if (!req.user) {
|
||||
return Response.json(
|
||||
{ success: false, error: "Unauthorized" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
||||
return Response.json(
|
||||
{ success: false, error: "No contacts selected" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
if (!OPENROUTER_KEY) {
|
||||
return Response.json(
|
||||
{ success: false, error: "Missing OPENROUTER_API_KEY" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const sentEmails = [];
|
||||
const interactions = [];
|
||||
|
||||
for (const contactId of contactIds) {
|
||||
// Fetch contact with account populated
|
||||
const contact = await req.payload.findByID({
|
||||
collection: "crm-contacts",
|
||||
id: contactId,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
if (!contact || !contact.email) continue;
|
||||
|
||||
const account = contact.account as any;
|
||||
const accountInfo = account
|
||||
? `
|
||||
Company Name: ${account.name || "Unknown"}
|
||||
Website: ${account.website || "Unknown"}
|
||||
Industry: ${account.industry || "Unknown"}
|
||||
Website Status: ${account.websiteStatus || "Unknown"}
|
||||
Internal Notes: ${account.notes || "None"}
|
||||
`
|
||||
: "No company information available.";
|
||||
|
||||
const prompt = `You are an expert sales/business development AI assistant. Write a professional, personalized German B2B outreach email ("Anschreiben").
|
||||
|
||||
CONTEXT ABOUT THE CONTACT:
|
||||
Name: ${contact.fullName || contact.firstName || "Unknown"}
|
||||
Role: ${contact.role || "Unknown"}
|
||||
|
||||
CONTEXT ABOUT THEIR COMPANY:
|
||||
${accountInfo}
|
||||
|
||||
USER INSTRUCTIONS / GOAL OF THE EMAIL:
|
||||
${instructions}
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. Write the email subject on the FIRST line as: "SUBJECT: <Your Subject>"
|
||||
2. The rest of the message should be the email body.
|
||||
3. Keep it professional and natural in German (Sie-form unless you think Du is appropriate based on the industry, but prefer Sie).
|
||||
4. Output only the email text, no markdown code blocks around it. No extra chit-chat.
|
||||
`;
|
||||
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "google/gemini-3-flash-preview",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
const generatedText = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
|
||||
if (!generatedText) {
|
||||
console.error("AI Generation failed for contact", contactId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse subject and body
|
||||
const lines = generatedText.split("\n");
|
||||
let subject = "Information von Mintel";
|
||||
let bodyText = generatedText;
|
||||
|
||||
if (lines[0].startsWith("SUBJECT:")) {
|
||||
subject = lines[0].replace("SUBJECT:", "").trim();
|
||||
bodyText = lines.slice(1).join("\n").trim();
|
||||
}
|
||||
|
||||
// Use a simple HTML wrapper to preserve line breaks
|
||||
const htmlBody = `<p>${bodyText.replace(/\n/g, "<br/>")}</p>`;
|
||||
|
||||
const targetEmail = isTest ? req.user.email : contact.email;
|
||||
|
||||
// Send the email
|
||||
await req.payload.sendEmail({
|
||||
to: targetEmail,
|
||||
subject: (isTest ? "[TEST] " : "") + subject,
|
||||
html: htmlBody,
|
||||
});
|
||||
|
||||
sentEmails.push({ contactId, email: targetEmail });
|
||||
|
||||
if (!isTest) {
|
||||
// Log interaction
|
||||
const interaction = await req.payload.create({
|
||||
collection: "crm-interactions",
|
||||
data: {
|
||||
type: "email",
|
||||
subject: `Outbound AI Mail: ${subject}`,
|
||||
date: new Date().toISOString(),
|
||||
contact: contact.id,
|
||||
account: account?.id,
|
||||
content: {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
children: [
|
||||
{
|
||||
type: "paragraph",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
mode: "normal",
|
||||
style: "",
|
||||
detail: 0,
|
||||
format: 0,
|
||||
text: bodyText,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
interactions.push(interaction.id);
|
||||
|
||||
// Update lead temperature to "warm" if it's currently cold and the status is lead
|
||||
if (
|
||||
account &&
|
||||
account.status === "lead" &&
|
||||
account.leadTemperature === "cold"
|
||||
) {
|
||||
await req.payload.update({
|
||||
collection: "crm-accounts",
|
||||
id: account.id,
|
||||
data: {
|
||||
leadTemperature: "warm",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ success: true, sentEmails, testMode: isTest });
|
||||
} catch (e: any) {
|
||||
console.error("Bulk Mail error", e);
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
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"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "bash -c 'trap \"COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up app postgres-db --remove-orphans'",
|
||||
"dev:docker": "docker network create infra 2>/dev/null || true && echo \"\\n🚀 Dockerized Environment Starting...\\n\\n📱 App: http://mintel.localhost\\n🚦 Caddy Proxy: http://localhost:80\\n\" && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up app postgres-db",
|
||||
"dev:clean": "pnpm dev:stop && rm -rf apps/web/.next apps/web/node_modules && pnpm install && pnpm dev",
|
||||
"dev:clean": "pnpm dev:stop && rm -rf apps/web/.next apps/web/node_modules && pnpm install && CI=true pnpm dev",
|
||||
"dev:stop": "COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down",
|
||||
"dev:local": "pnpm -r dev",
|
||||
"build": "pnpm -r build",
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -119,8 +119,8 @@ importers:
|
||||
specifier: link:../../../at-mintel/packages/meme-generator
|
||||
version: link:../../../at-mintel/packages/meme-generator
|
||||
"@mintel/payload-ai":
|
||||
specifier: ^1.9.15
|
||||
version: 1.9.15(@payloadcms/next@3.77.0(graphql@16.12.0)(monaco-editor@0.55.1)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@payloadcms/ui@3.77.0(monaco-editor@0.55.1)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(ws@8.19.0)
|
||||
specifier: link:../../../at-mintel/packages/payload-ai
|
||||
version: link:../../../at-mintel/packages/payload-ai
|
||||
"@mintel/pdf":
|
||||
specifier: link:../../../at-mintel/packages/pdf-library
|
||||
version: link:../../../at-mintel/packages/pdf-library
|
||||
@@ -314,7 +314,7 @@ importers:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
"@eslint/eslintrc":
|
||||
|
||||
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