Compare commits

..

No commits in common. "main" and "namds/implement-best-seller" have entirely different histories.

16 changed files with 87 additions and 1984 deletions

View File

@ -1,244 +0,0 @@
name: Claude Code Issue Handler
on:
issue_comment:
types: [created]
env:
CLAUDE_MAX_TURNS: "30"
jobs:
handle-issue:
# Only run on issue comments, not PR comments
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Classify issue by labels
id: classify
run: |
LABELS='${{ toJson(github.event.issue.labels) }}'
echo "Raw labels: $LABELS"
IS_BUG=$(echo "$LABELS" | jq '[.[].name | ascii_downcase] | any(. == "bug")' 2>/dev/null || echo "false")
IS_ENHANCEMENT=$(echo "$LABELS" | jq '[.[].name | ascii_downcase] | any(. == "enhancement")' 2>/dev/null || echo "false")
echo "is_bug=$IS_BUG" >> $GITHUB_OUTPUT
echo "is_enhancement=$IS_ENHANCEMENT" >> $GITHUB_OUTPUT
echo "is_bug=$IS_BUG | is_enhancement=$IS_ENHANCEMENT"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Authenticate Claude Code CLI
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude config set apiKey "$ANTHROPIC_API_KEY"
claude config set autoUpdaterStatus disabled
- name: Configure Git
run: |
git config --global user.email "claude@vibentec-it.io"
git config --global user.name "Claude Code Bot"
git config --global url."https://oauth2:${{ secrets.GITEA_TOKEN }}@gitea.vibentec-it.io/".insteadOf "https://gitea.vibentec-it.io/"
# ─────────────────────────────────────────────
# BUG FLOW: research + fix + PR
# ─────────────────────────────────────────────
- name: "[Bug] Checkout fix branch"
if: steps.classify.outputs.is_bug == 'true'
run: |
BRANCH_NAME="fix/issue-${{ github.event.issue.number }}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: "[Bug] Run Claude Code to research and fix"
if: steps.classify.outputs.is_bug == 'true'
id: claude-bug
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_COMMENT: ${{ github.event.comment.body }}
run: |
PROMPT=$(printf '%s\n' \
"You are a software engineer fixing a bug reported in issue #${ISSUE_NUMBER}: \"${ISSUE_TITLE}\"." \
"Issue description: ${ISSUE_BODY}" \
"Comment that triggered this action: ${ISSUE_COMMENT}" \
"Your tasks:" \
"1. Read and understand the codebase relevant to this bug." \
"2. Identify the root cause and which files need to change." \
"3. Implement minimal, focused fixes — do not refactor unrelated code." \
"4. After making code changes, run \"cd homepage && npm run lint\" via the Bash tool" \
" and immediately fix every lint error or syntax warning it reports." \
"5. Do NOT run Jenkinsfile or any Jenkins-related checks." \
"6. Do NOT run git commands yourself; the CI pipeline will commit your changes." \
"After completing your investigation, fixes, and lint pass, output a brief plain-text" \
"summary of the root cause, what you changed, and the lint result. Keep it under 200 words." \
)
claude \
--allowedTools "Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch" \
--max-turns "$CLAUDE_MAX_TURNS" \
--output-format text \
-p "$PROMPT" > /tmp/claude_bug_output.txt 2>&1 || true
echo "--- Claude output ---"
cat /tmp/claude_bug_output.txt
- name: "[Bug] Check for file changes"
if: steps.classify.outputs.is_bug == 'true'
id: git-status
run: |
if git diff --quiet && git diff --cached --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No file changes detected."
else
echo "has_changes=true" >> $GITHUB_OUTPUT
git diff --stat
fi
- name: "[Bug] Commit changes"
if: steps.classify.outputs.is_bug == 'true' && steps.git-status.outputs.has_changes == 'true'
run: |
git add -A
git commit -m "fix: resolve issue #${{ github.event.issue.number }} - ${{ github.event.issue.title }}"
- name: "[Bug] Push branch"
if: steps.classify.outputs.is_bug == 'true' && steps.git-status.outputs.has_changes == 'true'
run: git push origin "$BRANCH_NAME"
- name: "[Bug] Create Pull Request"
if: steps.classify.outputs.is_bug == 'true' && steps.git-status.outputs.has_changes == 'true'
id: create-pr
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
BASE_URL: ${{ github.server_url }}
run: |
CLAUDE_SUMMARY=$(cat /tmp/claude_bug_output.txt | tail -n 50)
PR_TITLE="fix: resolve issue #${ISSUE_NUMBER} - ${ISSUE_TITLE}"
PR_BODY=$(printf '## Summary\n\nThis PR addresses the bug reported in issue #%s.\n\n### Claude Code Analysis\n%s\n\n---\nCloses #%s' \
"$ISSUE_NUMBER" "$CLAUDE_SUMMARY" "$ISSUE_NUMBER")
PAYLOAD=$(jq -n \
--arg title "$PR_TITLE" \
--arg body "$PR_BODY" \
--arg head "$BRANCH_NAME" \
--arg base "main" \
'{title: $title, body: $body, head: $head, base: $base}')
PR_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"${BASE_URL}/api/v1/repos/${REPO}/pulls" \
-d "$PAYLOAD")
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number // empty')
PR_URL=$(echo "$PR_RESPONSE" | jq -r '.html_url // empty')
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
echo "Created PR #$PR_NUMBER: $PR_URL"
- name: "[Bug] Comment on issue — PR created"
if: >-
steps.classify.outputs.is_bug == 'true' &&
steps.git-status.outputs.has_changes == 'true' &&
steps.create-pr.outputs.pr_url != ''
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
BASE_URL: ${{ github.server_url }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
PR_URL: ${{ steps.create-pr.outputs.pr_url }}
run: |
COMMENT=$(printf "I've analyzed this bug and implemented a fix.\n\n**Pull Request:** [PR #%s](%s)\n\nPlease review the changes and merge when ready. If the fix doesn't fully address the issue, feel free to leave a comment and I'll take another look." \
"$PR_NUMBER" "$PR_URL")
PAYLOAD=$(jq -n --arg body "$COMMENT" '{body: $body}')
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"${BASE_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-d "$PAYLOAD"
- name: "[Bug] Comment on issue — no code changes needed"
if: steps.classify.outputs.is_bug == 'true' && steps.git-status.outputs.has_changes == 'false'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
BASE_URL: ${{ github.server_url }}
run: |
ANALYSIS=$(cat /tmp/claude_bug_output.txt 2>/dev/null || echo "No analysis output available.")
COMMENT=$(printf "After researching this issue, **no code changes appear to be necessary** at this time.\n\n### Analysis\n%s\n\nIf you believe something was missed, please provide more details and I'll investigate further." \
"$ANALYSIS")
PAYLOAD=$(jq -n --arg body "$COMMENT" '{body: $body}')
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"${BASE_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-d "$PAYLOAD"
# ─────────────────────────────────────────────
# ENHANCEMENT FLOW: research + comment only
# ─────────────────────────────────────────────
- name: "[Enhancement] Run Claude Code to research"
if: steps.classify.outputs.is_enhancement == 'true'
id: claude-enhancement
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_COMMENT: ${{ github.event.comment.body }}
run: |
PROMPT=$(printf '%s\n' \
"You are a software architect reviewing a feature idea from issue #${ISSUE_NUMBER}: \"${ISSUE_TITLE}\"." \
"Idea description: ${ISSUE_BODY}" \
"Comment that triggered this action: ${ISSUE_COMMENT}" \
"Your tasks:" \
"1. Explore the existing codebase to understand what already exists related to this idea." \
"2. Research best practices and possible implementation approaches." \
"3. Identify technical challenges, risks, or dependencies." \
"4. Provide a structured Markdown response suitable for posting as an issue comment." \
"Format your response with these sections:" \
"- **Current State** (what already exists)" \
"- **Proposed Approach** (recommended implementation path)" \
"- **Considerations** (risks, trade-offs, dependencies)" \
"- **Next Steps** (actionable recommendations)" \
"Keep the total response under 500 words." \
)
claude \
--allowedTools "Read,Glob,Grep,WebSearch,WebFetch" \
--max-turns 15 \
--output-format text \
-p "$PROMPT" > /tmp/claude_enhancement_output.txt 2>&1 || true
echo "--- Claude output ---"
cat /tmp/claude_enhancement_output.txt
- name: "[Enhancement] Comment research findings on issue"
if: steps.classify.outputs.is_enhancement == 'true'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
BASE_URL: ${{ github.server_url }}
run: |
RESEARCH=$(cat /tmp/claude_enhancement_output.txt 2>/dev/null || echo "Research output unavailable.")
COMMENT=$(printf "## Research Findings\n\nI've explored the codebase and researched this idea. Here's what I found:\n\n%s\n\n---\n*This research was performed automatically by Claude Code. Feel free to discuss further in the comments.*" \
"$RESEARCH")
PAYLOAD=$(jq -n --arg body "$COMMENT" '{body: $body}')
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"${BASE_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}/comments" \
-d "$PAYLOAD"

View File

@ -14,30 +14,9 @@
"className": "h-12 bg-[#009b93] text-[#fff] gap-12", "className": "h-12 bg-[#009b93] text-[#fff] gap-12",
"speed": 24, "speed": 24,
"items": [ "items": [
{ { "Link": { "config": { "label": "NEU: Overnight Oats Sallys Nussecke 😍", "href": "/" } } },
"Link": { { "Link": { "config": { "label": "Versandkostenfrei ab 45 € 💛", "href": "/" } } },
"config": { { "Link": { "config": { "label": "Gratis Geschenk ab 60 € Warenkorbwert 🎁", "href": "/" } } }
"label": "NEU: Overnight Oats Sallys Nussecke 😍",
"href": "/"
}
}
},
{
"Link": {
"config": {
"label": "Versandkostenfrei ab 45 € 💛",
"href": "/"
}
}
},
{
"Link": {
"config": {
"label": "Gratis Geschenk ab 60 € Warenkorbwert 🎁",
"href": "/"
}
}
}
] ]
} }
} }
@ -47,130 +26,18 @@
"config": { "config": {
"className": "h-24 bg-white text-[#003F31] gap-12", "className": "h-24 bg-white text-[#003F31] gap-12",
"left": [ "left": [
{ { "Logo": { "config": { "src": "/3bear-logo.png", "alt": "MyShop", "className": "h-[150px] w-[180px]", "objectFit": "contain" } } },
"Logo": { { "VtMegaMenu": { "config": { "navLabel": { "text": "Shop", "className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]", "isShowArrow": true } } } },
"config": { { "Dropdown": { "config": { "trigger": { "text": "Über Uns", "className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]", "isShowArrow": true }, "items": [ { "text": "Unser Unternehmen", "href": "/" }, { "text": "Loren ipsum", "href": "/" }, { "text": "Not a Link" } ] } } },
"src": "/3bear-logo.png", { "Dropdown": { "config": { "trigger": { "text": "Über unsere Produkte", "className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]", "isShowArrow": true }, "items": [ { "text": "Unser Unternehmen", "href": "/" }, { "text": "Loren ipsum", "href": "/" }, { "text": "Not a Link" } ] } } },
"alt": "MyShop", { "Link": { "config": { "label": "Rezepte", "href": "/", "className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]" } } },
"className": "h-[150px] w-[180px]", { "Link": { "config": { "label": "Triff Harry Kane", "href": "/", "className": "font-bold text-[1rem] text-[#003F31] flex items-center gap-1 hover:text-[#009b93]" } } }
"objectFit": "contain"
}
}
},
{
"VtMegaMenu": {
"config": {
"navLabel": {
"text": "Shop",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
}
}
}
},
{
"Dropdown": {
"config": {
"trigger": {
"text": "Über Uns",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
},
"items": [
{
"text": "Unser Unternehmen",
"href": "/"
},
{
"text": "Loren ipsum",
"href": "/"
},
{
"text": "Not a Link"
}
]
}
}
},
{
"Dropdown": {
"config": {
"trigger": {
"text": "Über unsere Produkte",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
},
"items": [
{
"text": "Unser Unternehmen",
"href": "/"
},
{
"text": "Loren ipsum",
"href": "/"
},
{
"text": "Not a Link"
}
]
}
}
},
{
"Link": {
"config": {
"label": "Rezepte",
"href": "/",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]"
}
}
},
{
"Link": {
"config": {
"label": "Triff Harry Kane",
"href": "/",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center gap-1 hover:text-[#009b93]"
}
}
}
], ],
"right": [ "right": [
{ { "VtCountryCodeSelect": { "config": { "trigger": { "className": "w-auto font-bold text-[13px] text-[#11314E] flex justify-start items-center gap-1 hover:text-[#009b93] bg-transparent shadow-none hover:bg-transparent", "isFlag": true, "isDisplayFullname": true } } } },
"VtCountryCodeSelect": { { "Button": { "config": { "icon": "MagnifyingGlass", "className": "shadow-none" } } },
"config": { { "AccountButton": { "config": { "icon": "User", "className": " flex items-center gap-1 shadow-none" } } },
"trigger": { { "VtCartButton": { "config": { "icon": "ShoppingBag", "className": "shadow-none bg-transparent text-black w-[50px]" } } }
"className": "w-auto font-bold text-[13px] text-[#11314E] flex justify-start items-center gap-1 hover:text-[#009b93] bg-transparent shadow-none hover:bg-transparent",
"isFlag": true,
"isDisplayFullname": true
}
}
}
},
{
"Button": {
"config": {
"icon": "MagnifyingGlass",
"className": "shadow-none"
}
}
},
{
"AccountButton": {
"config": {
"icon": "User",
"className": " flex items-center gap-1 shadow-none"
}
}
},
{
"VtCartButton": {
"config": {
"icon": "ShoppingBag",
"className": "shadow-none bg-transparent text-black w-[50px]"
}
}
}
] ]
} }
} }
@ -178,9 +45,7 @@
] ]
} }
}, },
{ { "PropsChildren": {} },
"PropsChildren": {}
},
{ {
"Footer": { "Footer": {
"config": { "config": {
@ -189,122 +54,13 @@
"centerClassName": "", "centerClassName": "",
"rightClassName": "flex gap-[10rem] mr-[80px]", "rightClassName": "flex gap-[10rem] mr-[80px]",
"left": [ "left": [
{ { "VtFooterHero": { "config": { "logoClassName": "h-[100px] w-[200px]", "logoSrc": "/3bear-white-logo.avif", "logoAlt": "3Bear", "title": "Melde dich für unsere Oatnews an 💛", "email": { "emailInputClassName": "w-[300px] ml-8" }, "socials": [ { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" } ], "socialsClassName": "ml-8 mt-10", "className": "", "ctaClassName": "ml-8", "titleClassName": "ml-8 text-white w-full", "descriptionClassName": "ml-8" } } }
"VtFooterHero": {
"config": {
"logoClassName": "h-[100px] w-[200px]",
"logoSrc": "/3bear-white-logo.avif",
"logoAlt": "3Bear",
"title": "Melde dich für unsere Oatnews an 💛",
"email": {
"emailInputClassName": "w-[300px] ml-8"
},
"socials": [
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
}
],
"socialsClassName": "ml-8 mt-10",
"className": "",
"ctaClassName": "ml-8",
"titleClassName": "ml-8 text-white w-full",
"descriptionClassName": "ml-8"
}
}
}
], ],
"center": [], "center": [],
"right": [ "right": [
{ { "VtMenuItem": { "config": { "title": "Information", "className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white hover:text-white", "itemClassName": "text-[1rem] font-[400] opacity-70 hover:text-white", "items": [ { "text": "Über Uns", "href": "/" }, { "text": "Placeholder", "href": "/categories/shoes" }, { "text": "Placeholder", "href": "/categories/accessories" } ] } } },
"VtMenuItem": { { "VtMenuItem": { "config": { "title": "Kundendienst", "className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white hover:text-white", "itemClassName": "text-[1rem] font-[400] flex items-center opacity-70 hover:text-white", "items": [ { "text": "Twitter", "href": "/" }, { "text": "Facebook", "href": "/categories/shoes" }, { "text": "Pinterest", "href": "/categories/accessories" } ] } } },
"config": { { "VtMenuItem": { "config": { "title": "Weiteres", "className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white", "itemClassName": "text-[1rem] font-[400] w-[150px] opacity-70 hover:text-white", "items": [ { "text": "Karriere", "href": "/" }, { "text": "Unser Team", "href": "/categories/shoes" }, { "text": "B2B", "href": "/categories/accessories" }, { "text": "Presse", "href": "/categories/accessories" } ] } } }
"title": "Information",
"className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white hover:text-white",
"itemClassName": "text-[1rem] font-[400] opacity-70 hover:text-white",
"items": [
{
"text": "Über Uns",
"href": "/"
},
{
"text": "Placeholder",
"href": "/categories/shoes"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
]
}
}
},
{
"VtMenuItem": {
"config": {
"title": "Kundendienst",
"className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white hover:text-white",
"itemClassName": "text-[1rem] font-[400] flex items-center opacity-70 hover:text-white",
"items": [
{
"text": "Twitter",
"href": "/"
},
{
"text": "Facebook",
"href": "/categories/shoes"
},
{
"text": "Pinterest",
"href": "/categories/accessories"
}
]
}
}
},
{
"VtMenuItem": {
"config": {
"title": "Weiteres",
"className": "flex flex-col gap-y-2 text-[24px] font-semibold text-white",
"itemClassName": "text-[1rem] font-[400] w-[150px] opacity-70 hover:text-white",
"items": [
{
"text": "Karriere",
"href": "/"
},
{
"text": "Unser Team",
"href": "/categories/shoes"
},
{
"text": "B2B",
"href": "/categories/accessories"
},
{
"text": "Presse",
"href": "/categories/accessories"
}
]
}
}
}
] ]
} }
} }
@ -317,22 +73,7 @@
"left": [], "left": [],
"center": [], "center": [],
"right": [ "right": [
{ { "VtFooterBottom": { "config": { "className": " mr-[80px]", "icons": [ "Mastercard", "PayPal", "Visa", "Mastercard", "Mastercard", "Mastercard", "Mastercard" ] } } }
"VtFooterBottom": {
"config": {
"className": " mr-[80px]",
"icons": [
"Mastercard",
"PayPal",
"Visa",
"Mastercard",
"Mastercard",
"Mastercard",
"Mastercard"
]
}
}
}
] ]
} }
} }
@ -344,31 +85,8 @@
"Hero": { "Hero": {
"config": { "config": {
"className": "h-[35rem]", "className": "h-[35rem]",
"ImageDisplayer": { "ImageDisplayer": { "config": { "duration": 0, "images": ["./banner-hero.webp"], "links": ["/account"] } },
"config": { "left": [ { "VtCtaBanner": { "config": { "className": "left-[120px] top-[80px] relative w-full p-12 flex flex-col items-start justify-center text-left bg-transperant border-none shadow-none", "buttonTextClassName": "inline-flex items-center justify-center bg-[#FCEE56] hover:bg-[#FCEE56]/90 text-[#0D382E] px-8 py-3 rounded-full font-bold text-lg shadow-none border-none", "tagTextClassName": "text-[#0D382E] text-lg font-medium mb-2 bg-transparent", "tagText": "So einfach kann Frühstück sein mit unseren leckeren Overnight Oats.", "titleText": "breakfast made easy.", "buttonText": "Jetzt entdecken" } } } ],
"duration": 0,
"images": [
"./banner-hero.webp"
],
"links": [
"/account"
]
}
},
"left": [
{
"VtCtaBanner": {
"config": {
"className": "left-[120px] top-[80px] relative w-full p-12 flex flex-col items-start justify-center text-left bg-transperant border-none shadow-none",
"buttonTextClassName": "inline-flex items-center justify-center bg-[#FCEE56] hover:bg-[#FCEE56]/90 text-[#0D382E] px-8 py-3 rounded-full font-bold text-lg shadow-none border-none",
"tagTextClassName": "text-[#0D382E] text-lg font-medium mb-2 bg-transparent",
"tagText": "So einfach kann Frühstück sein mit unseren leckeren Overnight Oats.",
"titleText": "breakfast made easy.",
"buttonText": "Jetzt entdecken"
}
}
}
],
"center": [], "center": [],
"right": [] "right": []
} }
@ -380,213 +98,26 @@
"title": "best-seller", "title": "best-seller",
"styles": { "styles": {
"container": "content-container py-12 small:py-20", "container": "content-container py-12 small:py-20",
"header": { "header": { "container": "hidden", "title": "hidden", "isShowViewAll": false },
"container": "ml-16",
"title": "text-2xl mb-12",
"isShowViewAll": false
},
"list": "grid grid-cols-2 small:grid-cols-3 gap-x-6 gap-y-24 small:gap-y-36", "list": "grid grid-cols-2 small:grid-cols-3 gap-x-6 gap-y-24 small:gap-y-36",
"productCard": { "productCard": {
"badgeText": "Bestseller", "badgeText": "Bestseller",
"card": "relative flex flex-col items-center bg-transparent shadow-none border-none p-0", "card": "relative flex flex-col items-center bg-transparent shadow-none border-none p-0",
"badge": { "badge": { "container": "absolute top-0 left-0 z-[1]", "text": "px-3 py-1 rounded-full bg-[#009b93] text-white text-[12px] font-semibold" },
"container": "absolute top-0 left-0 z-[1]", "thumbnail": { "className": "rounded-2xl bg-white h-[320px] object-contain shadow-none", "size": "full" },
"text": "px-3 py-1 rounded-full bg-[#009b93] text-white text-[12px] font-semibold"
},
"thumbnail": {
"className": "rounded-2xl bg-white h-[320px] object-contain shadow-none",
"size": "full"
},
"content": "flex flex-col items-center justify-start text-center p-0 mt-6", "content": "flex flex-col items-center justify-start text-center p-0 mt-6",
"title": "text-[#003F31] text-[28px] font-semibold", "title": "text-[#003F31] text-[28px] font-semibold",
"description": "order-3 text-[#003f31b3]", "description": "order-3 text-[#003f31b3]",
"price": "mt-2 text-[#0D382E] text-[24px] font-bold order-2 flex gap-2", "price": "mt-2 text-[#0D382E] text-[24px] font-bold order-2 flex gap-2",
"reviews": { "reviews": { "container": "mt-3 flex items-center gap-2 order-1", "stars": "flex gap-1", "star": "text-[#F59E0B] text-xl leading-none", "emptyStar": "text-[#F59E0B] text-xl opacity-30 leading-none", "text": "text-[#003F31] text-[14px]", "rating": 4.5, "count": 38 },
"container": "mt-3 flex items-center gap-2 order-1", "button": { "addToCart": "hidden", "moreInfo": "hidden", "isShowIcon": false }
"stars": "flex gap-1",
"star": "text-[#F59E0B] text-xl leading-none",
"emptyStar": "text-[#F59E0B] text-xl opacity-30 leading-none",
"text": "text-[#003F31] text-[14px]",
"rating": 4.5,
"count": 38
},
"button": {
"addToCart": "hidden",
"moreInfo": "hidden",
"isShowIcon": false
}
} }
} }
} }
} }
}, },
{ { "CartMismatchBanner": { "config": { "show": true } } },
"CartMismatchBanner": { { "FreeShippingPriceNudge": { "config": { "variant": "popup" } } }
"config": {
"show": true
}
}
},
{
"VtCategoryHighlight": {
"config": {
"title": "Oder lieber stöbern? Hier findest du sicher deine neuen Hafer-Favoriten.",
"className": "content-container py-12 small:py-20",
"gridClassName": "grid grid-cols-4 gap-6 w-full",
"labelClassName": "absolute left-4 bottom-4 text-[#003F31] text-[18px] font-semibold",
"items": [
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/overnight-oats",
"label": "Overnight Oats",
"className": "bg-[#CFECD9] col-start-1 col-end-3 row-start-1 row-end-3",
"imageClassName": "object-contain"
},
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/porridge",
"label": "Porridge",
"className": "bg-[#F9E0B0]",
"imageClassName": "object-contain"
},
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/cereals",
"label": "Cereals",
"className": "bg-[#F59E0B]",
"imageClassName": "object-contain"
},
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/granola",
"label": "Granola",
"className": "bg-[#A7D8F0]",
"imageClassName": "object-contain"
},
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/oat-bars",
"label": "Oat Bars",
"className": "bg-[#EED7F2]",
"imageClassName": "object-contain"
}
]
}
}
},
{
"VtBrand": {
"config": {
"className": "w-full py-12 bg-[#CFECD9]",
"innerClassName": "content-container flex flex-col items-center",
"title": "Trusted By",
"titleClassName": "text-[#003F31] text-[20px] font-semibold mb-8",
"brandsClassName": "flex w-full items-center justify-between gap-12",
"items": [
{
"label": "Men'sHealth",
"containerClassName": "",
"className": "text-[#003F31] text-[36px] font-semibold italic"
},
{
"label": "GQ",
"containerClassName": "",
"className": "text-[#003F31] text-[44px] font-black tracking-tight"
},
{
"label": "BIRCHBOX",
"containerClassName": "",
"className": "text-[#003F31] text-[36px] font-semibold tracking-[0.2em]"
}
]
}
}
},
{
"VtFeedbackCard": {
"config": {
"className": "content-container py-16 bg-[#CFECD9] mt-16",
"title": "Der Hafer-Hype ist real. Finden nicht nur 100.000+ zufriedene 3Bears Fans.",
"gridClassName": "grid grid-cols-1 small:grid-cols-2 xl:grid-cols-4 gap-6",
"cardClassName": "rounded-2xl overflow-hidden",
"imageClassName": "w-full h-[260px] object-cover",
"contentClassName": "p-6",
"nameClassName": "text-[#003F31] text-[20px] font-bold",
"subtitleClassName": "mt-1 text-[#003f31b3] text-[14px]",
"quoteClassName": "mt-4 text-[#003F31] text-[16px]",
"ctaClassName": "mt-6 inline-flex items-center justify-center bg-[#FCEE56] text-[#0D382E] px-6 py-2 rounded-full font-bold",
"items": [
{
"imageSrc": "/overnight-oat.webp",
"name": "Harry Kane",
"subtitle": "Profißballer, Kapitän engl. Nationalmannschaft, Stürmer FC Bayern",
"quote": "Als Sportler ist das Frühstück die wichtigste Mahlzeit für mich, und natürlich achte ich sehr darauf, was ich esse. Als ich 3Bears entdeckt habe, hat mich nachhaltig beeindruckt, dass die Haferflocken auf ein neues Level heben.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Sally Özcan",
"subtitle": "Foodcreatorin & Unternehmerin",
"quote": "Ich liebe Frühstück, weil es für mich der Start in einen guten Tag ist, mit meiner Familie, meinem Team oder unterwegs. Ich mag Produkte, die einfach einen Sinn ergeben, natürlich, lecker und ohne Schnickschnack. Genau das ist 3Bears für mich."
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Sarah Harrison",
"subtitle": "Unternehmerin & Influencerin",
"quote": "3Bears teilt meine Leidenschaft für hochwertige Lebensmittel, die nicht nur mega lecker, sondern auch vollwertig sind. Deswegen war ich so begeistert von der Idee, gemeinsam ein Granola zu entwickeln.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Hendrik Pfeiffer",
"subtitle": "Profi-Läufer & German Champion",
"quote": "Als Profisportler spielt meine bewusste Ernährung eine absolute Schlüsselrolle, um vorne mitmischen zu können. Die Produkte von 3Bears passen dabei wie die Faust aufs Auge.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
}
]
}
}
},
{
"VtSubcription": {
"config": {
"className": "content-container py-12 flex justify-center",
"cardClassName": "rounded-2xl overflow-hidden bg-[#CFECD9] w-[800px] p-10",
"title": "10% für dich!",
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
"highlightClassName": "text-[#003F31] font-bold",
"description": true,
"formClassName": "mt-8 flex flex-col items-center gap-4",
"descriptionClassName": "text-[#003F31] text-[16px] text-center",
"fieldsClassName": "grid grid-cols-1 small:grid-cols-2 gap-4 w-full",
"descriptionPrefix": "Melde dich jetzt zum 3Bears Newsletter an und sichere dir",
"descriptionHighlight": "10% Rabatt auf deinen nächsten Einkauf!",
"subtextClassName": "text-[#003F31] text-[16px] text-center",
"descriptionSuffix": "",
"subtext": "Deinen Rabattcode bekommst du von uns per Mail.",
"firstName": { "placeholder": "Vorname" },
"email": { "placeholder": "E-Mail-Adresse" },
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
"cta": { "label": "Anmelden", "className": "bg-[#FCEE56] text-[#0D382E] px-6 py-2 rounded-full w-fit font-bold" }
}
}
},
{
"FreeShippingPriceNudge": {
"config": {
"variant": "popup"
}
}
}
] ]
} }
} }

View File

@ -131,9 +131,7 @@
] ]
} }
}, },
{ { "PropsChildren": {} },
"PropsChildren": {}
},
{ {
"Footer": { "Footer": {
"config": { "config": {
@ -149,30 +147,12 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white gap-8", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white gap-8",
"itemClassName": "text-[14px] font-[400] mt-3", "itemClassName": "text-[14px] font-[400] mt-3",
"items": [ "items": [
{ { "text": "FAQ", "href": "/" },
"text": "FAQ", { "text": "Track my order", "href": "/categories/shoes" },
"href": "/" { "text": "Placeholder", "href": "/categories/accessories" },
}, { "text": "Placeholder", "href": "/categories/accessories" },
{ { "text": "Placeholder", "href": "/categories/accessories" },
"text": "Track my order", { "text": "Placeholder", "href": "/categories/accessories" }
"href": "/categories/shoes"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -184,34 +164,13 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] flex items-center mt-3", "itemClassName": "text-[14px] font-[400] flex items-center mt-3",
"items": [ "items": [
{ { "text": "Twitter", "href": "/" },
"text": "Twitter", { "text": "Facebook", "href": "/categories/shoes" },
"href": "/" { "text": "Pinterest", "href": "/categories/accessories" },
}, { "text": "Placeholder", "href": "/categories/accessories" },
{ { "text": "Placeholder", "href": "/categories/accessories" },
"text": "Facebook", { "text": "Placeholder", "href": "/categories/accessories" },
"href": "/categories/shoes" { "text": "Placeholder", "href": "/categories/accessories" }
},
{
"text": "Pinterest",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -223,18 +182,9 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] w-[200px] mt-3", "itemClassName": "text-[14px] font-[400] w-[200px] mt-3",
"items": [ "items": [
{ { "text": "The Squatch Difference", "href": "/" },
"text": "The Squatch Difference", { "text": "Why Natural Products", "href": "/categories/shoes" },
"href": "/" { "text": "No Harmful Ingredients", "href": "/categories/accessories" }
},
{
"text": "Why Natural Products",
"href": "/categories/shoes"
},
{
"text": "No Harmful Ingredients",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -264,26 +214,10 @@
"buttonClassName": "bg-[#C4622C] w-[90px]", "buttonClassName": "bg-[#C4622C] w-[90px]",
"socialsClassName": "mt-4 gap-8", "socialsClassName": "mt-4 gap-8",
"socials": [ "socials": [
{ { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"icon": "Twitter", { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"href": "/", { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"className": "w-5 h-5 text-white" { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
}
] ]
} }
} }
@ -299,18 +233,9 @@
"text": "©2025 Vibentec IT. All rights reserved", "text": "©2025 Vibentec IT. All rights reserved",
"linksClassName": "flex items-center text-orange-500 mt-2 pl-2", "linksClassName": "flex items-center text-orange-500 mt-2 pl-2",
"links": [ "links": [
{ { "label": "Privacy Policy", "href": "/" },
"label": "Privacy Policy", { "label": "Terms of Service", "href": "/categories/shoes" },
"href": "/" { "label": "Cookie Policy", "href": "/categories/accessories" }
},
{
"label": "Terms of Service",
"href": "/categories/shoes"
},
{
"label": "Cookie Policy",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -325,12 +250,8 @@
"ImageDisplayer": { "ImageDisplayer": {
"config": { "config": {
"duration": 0, "duration": 0,
"images": [ "images": ["./drsquatch-banner.jpg"],
"./drsquatch-banner.jpg" "links": ["/account"]
],
"links": [
"/account"
]
} }
}, },
"left": [ "left": [
@ -376,14 +297,10 @@
"container": "absolute z-[1] top-0 left-0 pt-4", "container": "absolute z-[1] top-0 left-0 pt-4",
"text": "uppercase px-4 py-2 bg-[#3B6F47] text-white" "text": "uppercase px-4 py-2 bg-[#3B6F47] text-white"
}, },
"thumbnail": { "thumbnail": { "className": "rounded-none h-[300px] shadow-none", "size": "full" },
"className": "rounded-none h-[300px] shadow-none",
"size": "full"
},
"content": " flex flex-col flex-1", "content": " flex flex-col flex-1",
"title": "mt-2 text-[#1f3521] text-[22px] font-bold", "title": "mt-2 text-[#1f3521] text-[22px] font-bold",
"price": "mt-2 text-[#3B6F47] font-bold text-[20px] flex gap-3 flex-row-reverse justify-end", "price": "mt-2 text-[#3B6F47] font-bold text-[20px] flex gap-3 flex-row-reverse justify-end",
"description": "mt-2",
"reviews": { "reviews": {
"container": "mt-3 flex items-center gap-2", "container": "mt-3 flex items-center gap-2",
"stars": "flex gap-1", "stars": "flex gap-1",
@ -402,170 +319,14 @@
} }
} }
}, },
{ { "CartMismatchBanner": { "config": { "show": true } } },
"VtCategoryHighlight": { { "FreeShippingPriceNudge": { "config": { "variant": "popup" } } }
"config": {
"title": "Oder lieber stöbern? Hier findest du sicher deine neuen Hafer-Favoriten.",
"className": "content-container py-12 small:py-20",
"gridClassName": "grid grid-cols-2 gap-6 w-full",
"labelClassName": "absolute left-4 bottom-4 text-[#003F31] text-[18px] font-semibold",
"items": [
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/overnight-oats",
"label": "Overnight Oats",
"className": "bg-[#CFECD9] h-[250px]",
"imageClassName": "object-contain"
},
{
"headingLabel": "The Squatch Difference",
"descriptionLabel": "Learn why men everywhere are loving Dr. Squatch.",
"buttonLabel": "Learn more",
"className": "flex-col bg-[#F9E0B0] p-6 justify-center",
"headingClassName": "text-[#003F31] text-[28px] font-semibold",
"descriptionClassName": "text-[#003f31b3]",
"buttonClassName": "mt-4 text-[#003F31] text-[18px] font-semibold bg-orange-500 py-2 px-16 rounded text-white"
}
]
}
}
},
{
"VtBrand": {
"config": {
"className": "w-full py-12 bg-[#CFECD9]",
"innerClassName": "content-container flex flex-col items-center",
"title": "",
"titleClassName": "text-[#1f3521] text-[20px] font-bold mb-8",
"brandsClassName": "flex w-full items-center justify-between gap-12",
"items": [
{
"imageSrc": "/brand-logo.png",
"alt": "Men's Health",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
},
{
"imageSrc": "/brand-logo.png",
"alt": "GQ",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
},
{
"imageSrc": "/brand-logo.png",
"alt": "Birchbox",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
}
]
}
}
},
{
"VtSubcription": {
"config": {
"className": "w-full",
"cardClassName": "overflow-hidden bg-[#F3EDE3] p-10",
"title": "SUBSCRIBE & SAVE",
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
"description": true,
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
"benefits": [
{
"icon": "🗓",
"title": "Ships Every 3 Months",
"description": "Customize your picks and scents, upgrade anytime, or hit snooze if you want. You're in control."
},
{
"icon": "🚚",
"title": "Free Delivery",
"description": "Subscribe once and relax. All your shipping expenses are covered by Squatch."
},
{
"icon": "⭐",
"title": "Exclusive Benefits",
"description": "Gain exclusive, subscriber-only benefits. Enjoy early access to new products and limited edition releases!"
}
],
"formClassName": "flex justify-center",
"cta": {
"label": "SUBSCRIBE & SAVE",
"className": "w-fit mt-12 px-[30px] h-[56px] rounded-full bg-orange-500 text-white font-bold"
}
}
}
},
{
"VtFeedback": {
"config": {
"title": "100,000+ Reviews From Squatchers",
"className": "content-container py-16",
"titleClassName": "text-[#1f3521] text-[28px] font-bold text-center mb-10",
"duration": 5,
"options": {
"loop": true
},
"itemClassName": "min-w-full px-6",
"starsClassName": "text-[#C4622C] text-xl leading-none",
"reviewTitleClassName": "text-[#1f3521] font-bold",
"reviewTextClassName": "text-[#1f3521]",
"authorClassName": "italic text-[#1f3521]",
"controls": "mt-6 flex items-center justify-center gap-4",
"items": [
{
"rating": 5,
"title": "Ah-freaking-amazing!",
"text": "So I just had my first shower with Dr. Squatch Cool Fresh Aloe. Holy sh*t this stuff is Ah-freaking-amazing! Talk about a life hack!",
"author": "Stephen B."
},
{
"rating": 5,
"title": "Best damn soap ever…period.",
"text": "Best Damn Soap I EVER bought! Super smooth on the skin, smells awesome, makes you feel good showering, and yes…the wife approves.",
"author": "Chris H."
},
{
"rating": 5,
"title": "Hilarious…products awesome too",
"text": "Ok…the Dr. Squatch commercials are just freakin hilarious…plus the products are awesome too! So yes, buy it now and subscribe to it!",
"author": "Mike C."
}
]
}
}
},
{
"CartMismatchBanner": {
"config": {
"show": true
}
}
},
{
"FreeShippingPriceNudge": {
"config": {
"variant": "popup"
}
}
}
], ],
"Product": [ "Product": [
{ { "VtFeaturedProducts": { "config": { "title": "drsquatch-best-seller" } } }
"VtFeaturedProducts": {
"config": {
"title": "drsquatch-best-seller"
}
}
}
], ],
"StorePage": [ "StorePage": [
{ { "VtFeaturedProducts": { "config": { "title": "drsquatch-best-seller" } } }
"VtFeaturedProducts": {
"config": {
"title": "drsquatch-best-seller"
}
}
}
] ]
} }
} }

View File

@ -203,34 +203,6 @@
} }
} }
}, },
{
"VtCategoryHighlight": {
"config": {
"title": "Oder lieber stöbern? Hier findest du sicher deine neuen Hafer-Favoriten.",
"className": "content-container py-12 small:py-20",
"gridClassName": "grid grid-cols-2 gap-6 w-full",
"labelClassName": "absolute left-4 bottom-4 text-[#003F31] text-[18px] font-semibold",
"items": [
{
"imageSrc": "/overnight-oat.webp",
"href": "/categories/overnight-oats",
"label": "Overnight Oats",
"className": "bg-[#CFECD9] h-[250px]",
"imageClassName": "object-contain"
},
{
"headingLabel": "The Squatch Difference",
"descriptionLabel": "Learn why men everywhere are loving Dr. Squatch.",
"buttonLabel": "Learn more",
"className": "flex-col bg-[#F9E0B0] p-6 justify-center",
"headingClassName": "text-[#003F31] text-[28px] font-semibold",
"descriptionClassName": "text-[#003f31b3]",
"buttonClassName": "mt-4 text-[#003F31] text-[18px] font-semibold bg-orange-500 py-2 px-16 rounded text-white"
}
]
}
}
},
{ {
"CartMismatchBanner": { "CartMismatchBanner": {
"config": { "show": true } "config": { "show": true }

View File

@ -342,7 +342,6 @@
"subtitle": "text-ui-fg-subtle text-[14px]", "subtitle": "text-ui-fg-subtle text-[14px]",
"content": "flex flex-col flex-1 justify-between p-4", "content": "flex flex-col flex-1 justify-between p-4",
"title": "text-ui-fg-subtle text-[18px]", "title": "text-ui-fg-subtle text-[18px]",
"description": "mt-2 text-ui-fg-subtle text-[14px]",
"price": "flex items-center gap-x-1 text-[#285A86] font-bold border-b pb-4", "price": "flex items-center gap-x-1 text-[#285A86] font-bold border-b pb-4",
"button": { "button": {
"addToCart": "w-fit h-[40px] bg-black text-white rounded-md", "addToCart": "w-fit h-[40px] bg-black text-white rounded-md",
@ -384,26 +383,6 @@
} }
} }
} }
},
{
"VtSubcription": {
"config": {
"className": "content-container py-12 flex justify-center",
"leftClassName": "w-1/2 bg-[#132437]",
"cardClassName": "overflow-hidden bg-[#132437] w-1/2 p-10 text-left flex flex-col items-start",
"title": "Subscribe our newsletter!",
"titleClassName": "text-white text-[28px] font-bold border-b-2 border-white",
"description": true,
"descriptionPrefix": "Subscribe to our newsletter and be the first to receive insights, updates, and expert tips",
"subtext": "Stay up to date!",
"email": { "placeholder": "E-Mail-Adresse", "className": "border border-white w-full h-[40px] mt-4" },
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
"formClassName": "flex gap-4",
"fieldsClassName": "w-[390px]",
"cta": { "label": "Subcribe", "className": "w-fit h-[40px] px-6 mt-4 bg-white text-[#132437] font-bold rounded-md" },
"subtextSubcribe": { "label": "By subscribing, you agree to our terms.!", "className": "text-white text-[13px]" }
}
}
}, },
{ "CartMismatchBanner": { "config": { "show": true } } }, { "CartMismatchBanner": { "config": { "show": true } } },
{ "FreeShippingPriceNudge": { "config": { "variant": "popup" } } } { "FreeShippingPriceNudge": { "config": { "variant": "popup" } } }

View File

@ -1,340 +0,0 @@
# Shop-Storefront — Current State Document
> **Generated:** 2026-03-27
> **Role:** Brownfield analysis by Senior Staff Engineer / Technical Lead
> **Branch at time of analysis:** `namds/refactor-base-layout`
---
## Table of Contents
1. [Phase 1 — Discovery & Scaffolding](#phase-1--discovery--scaffolding)
2. [Phase 2 — Codebase Triage](#phase-2--codebase-triage)
3. [Phase 3 — Documentation](#phase-3--documentation)
- [High-Level System Overview](#1-high-level-system-overview)
- [Architecture Diagram](#2-architecture-diagram)
- [Local Setup Guide](#3-local-setup-guide)
- [Key Domain Entities](#4-key-domain-entities)
4. [Phase 4 — Maintenance Strategy](#phase-4--maintenance-strategy)
- [Immediate Risks](#1-immediate-risks)
- [Refactoring Opportunities](#2-refactoring-opportunities-low-hanging-fruit)
- [Testing Strategy](#3-testing-strategy)
---
## Phase 1 — Discovery & Scaffolding
### Tech Stack
| Layer | Technology |
| ----------------------- | --------------------------------------------------------- |
| **Framework** | Next.js 15 (App Router, Turbopack) |
| **Language** | TypeScript 5.3 |
| **UI Runtime** | React 19 RC (`19.0.0-rc-66855b96-20241106`) |
| **Styling** | Tailwind CSS 3 + `@medusajs/ui` + Radix UI |
| **Commerce Backend** | Medusa V2 (`@medusajs/js-sdk`) |
| **Payments** | Stripe (`@stripe/react-stripe-js`), PayPal |
| **Package Manager** | Yarn 3 (Berry) |
| **Database (indirect)** | `pg` listed as dependency — Medusa owns the DB connection |
### Entry Points & Critical Files
| File | Role |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `src/middleware.ts` | **True entry point.** Intercepts every request, resolves country code from Vercel geo-header or URL, and redirects to `/{countryCode}/...` |
| `src/app/layout.tsx` | Root Next.js layout (HTML shell, global styles) |
| `src/app/[countryCode]/(main)/layout.tsx` | **Primary layout shell.** Loads the JSON design config, fetches cart + customer, and hands everything to `DynamicLayoutRenderer` |
| `src/vibentec/configloader.ts` | Reads active design JSON from `config/` at request time |
| `src/vibentec/devJsonFileNames.ts` | Hardcoded switch that selects which tenant/design JSON is active |
| `src/lib/config.ts` | Instantiates the Medusa JS SDK singleton |
| `src/lib/data/` | All server-side data fetching (cart, customer, products, orders, regions, etc.) |
---
## Phase 2 — Codebase Triage
### Architectural Pattern
This is a **modular monolith storefront** using Next.js App Router with a custom **JSON-driven dynamic layout layer** on top. There are two distinct halves:
1. **Standard Medusa Storefront** — The base is a Medusa V2 Next.js starter. It follows a clear `modules/``templates/``components/` hierarchy, with server-side data fetching in `lib/data/` feeding React Server Components.
2. **Vibentec UI Builder** — A bespoke system in `src/vibentec/` that allows the entire page shell (header, nav, footer, banners) to be defined declaratively in JSON config files (`/config/`). At runtime, `DynamicLayoutRenderer` walks the JSON node tree and maps keys to React components via `componentMap`. Multiple tenant/brand configs exist: `3bear`, `drsquatch`, `vibentec`, `mds-starter`, `medusa-starter`, `playground`.
### Core Domain
`src/lib/data/` is the data layer. `src/modules/` holds all business-domain UI (account, cart, checkout, products, orders, collections, store). The custom differentiator is `src/vibentec/` — this is Vibentec's intellectual property layered on top of the OSS Medusa starter.
### Technical Debt & Health Assessment
| Severity | Issue |
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
| **High** | `REVALIDATE_SECRET=supersecret` is committed in `.env` — must be rotated before production |
| **High** | A real publishable API key (`pk_65b8a...`) is hardcoded in the committed `.env` file |
| **Medium** | React 19 **RC** (release candidate) is pinned — not a stable release |
| **Medium** | Active tenant config is hardcoded in `src/vibentec/devJsonFileNames.ts` — switching brands requires a code change |
| **Medium** | `LayoutContext` uses `any` for `customer` and `cart` — kills type safety at the renderer boundary |
| **Medium** | `pg` is listed as a direct dependency but no direct DB usage exists in this storefront |
| **Low** | Zero test files found anywhere in the project |
| **Low** | `@types/react-instantsearch-dom` in devDeps but no instantsearch code exists — unused dependency |
---
## Phase 3 — Documentation
### 1. High-Level System Overview
This is a **multi-tenant e-commerce storefront** built on Next.js 15 (App Router), serving as the customer-facing frontend for a Medusa V2 headless commerce backend. The application handles the full retail customer journey: browsing products by category or collection, managing a cart, checking out with Stripe or PayPal, and managing account details and order history. All routes are prefixed with a country code (e.g., `/us/`, `/de/`), which is resolved at the edge by middleware that consults Medusa's region API and Vercel's geo-IP headers to deliver region-appropriate pricing and shipping rules.
The most significant custom layer is the **Vibentec UI Builder** (`src/vibentec/`). Rather than hard-coding the page shell (header, nav, banner bar, footer), the entire layout is declared as a JSON component tree stored in `config/*.design.json` files. At request time, the primary layout server component reads the active JSON file, passes it to `DynamicLayoutRenderer`, which walks the tree and resolves component names to actual React components via `componentMap`. This makes the full layout of the storefront configurable without touching React code — a white-label capability designed to serve multiple brands from a single codebase.
Data flows unidirectionally from the Medusa backend through server-side `"use server"` functions in `src/lib/data/`, which use the Medusa JS SDK with Next.js `force-cache` and tag-based revalidation. There is no client-side global state management (no Redux, Zustand, or React Query). The only React Context used is a minimal `ModalContext` scoped to the modal component. All authentication state is conveyed via cookies and forwarded as HTTP headers to Medusa.
---
### 2. Architecture Diagram
```mermaid
graph TD
subgraph "Browser"
Browser["Customer Browser"]
end
subgraph "Next.js 15 App (Port 8000)"
Middleware["src/middleware.ts\n(Edge Runtime)\nGeo-IP → Country Code\nRegion redirect"]
subgraph "App Router"
RootLayout["app/layout.tsx\nRoot HTML shell"]
MainLayout["app/[countryCode]/(main)/layout.tsx\nLoads design JSON\nFetches cart + customer\nDynamicLayoutRenderer"]
CheckoutLayout["app/[countryCode]/(checkout)/layout.tsx\nCheckout shell"]
subgraph "Pages"
Home["/ (main) page.tsx"]
Store["/store page.tsx"]
Product["/products/[handle]"]
Cart["/cart"]
Checkout["/checkout"]
Account["/account (Parallel Routes)"]
Order["/order/[id]"]
Collections["/collections/[handle]"]
Categories["/categories/[...category]"]
end
end
subgraph "Vibentec UI Builder (src/vibentec/)"
ConfigLoader["configloader.ts\nReads config/*.design.json"]
Renderer["renderer.tsx\nDynamicLayoutRenderer"]
ComponentMap["component-map.tsx\ncomponentMap registry"]
end
subgraph "Modules (src/modules/)"
Layout["layout/\nHeader, Nav, Footer,\nCart Button, Side Menu,\nMega Menu, Country Select"]
Products["products/\nGallery, Actions, Price,\nPreview, Tabs, Related"]
CartMod["cart/\nItems, Summary, Preview"]
CheckoutMod["checkout/\nAddresses, Payment,\nShipping, Review"]
AccountMod["account/\nProfile, Orders,\nAddresses, Login"]
OrderMod["order/\nConfirmation, Details,\nTransfer"]
end
subgraph "Data Layer (src/lib/data/)"
DataFunctions["cart.ts · products.ts\ncustomer.ts · orders.ts\ncategories.ts · collections.ts\nregions.ts · payment.ts\nfulfillment.ts · cookies.ts"]
end
subgraph "Config Files (config/)"
DesignJSON["nam.vibentec.design.json\nnam.3bear.design.json\nnam.drsquatch.design.json\nste.medusa-starter.design.json\n(+ others)"]
end
end
subgraph "External Services"
MedusaBackend["Medusa V2 Backend\n(localhost:9000)\nProducts, Orders, Carts,\nCustomers, Regions,\nPayment Sessions"]
Stripe["Stripe\nPayment Processing"]
PayPal["PayPal\nPayment Processing"]
S3["AWS S3\nProduct Images"]
end
Browser -->|"HTTP Request"| Middleware
Middleware -->|"Resolves region\nSets _medusa_cache_id cookie"| RootLayout
RootLayout --> MainLayout
MainLayout --> ConfigLoader
ConfigLoader -->|"Reads active .design.json"| DesignJSON
MainLayout --> Renderer
Renderer --> ComponentMap
ComponentMap --> Layout
MainLayout -->|"props.children"| Pages
Pages --> Products
Pages --> CartMod
Pages --> CheckoutMod
Pages --> AccountMod
Pages --> OrderMod
DataFunctions -->|"Medusa JS SDK\nforce-cache + tag revalidation"| MedusaBackend
CheckoutMod -->|"Stripe Elements"| Stripe
CheckoutMod -->|"PayPal SDK"| PayPal
MedusaBackend -->|"Image URLs"| S3
Products & CartMod & CheckoutMod & AccountMod & OrderMod --> DataFunctions
```
---
### 3. Local Setup Guide
**Prerequisites:** Node.js ≥ 20, Yarn 3 (Berry), a running Medusa V2 backend with at least one Region configured.
```bash
# 1. Clone and install
git clone <repo-url>
cd Shop-Storefront
yarn install
# 2. Configure environment
cp .env .env.local
# Edit .env.local and set:
# MEDUSA_BACKEND_URL=http://localhost:9000
# NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=<your key from Medusa Admin>
# NEXT_PUBLIC_BASE_URL=http://localhost:8000
# NEXT_PUBLIC_DEFAULT_REGION=us
# REVALIDATE_SECRET=<generate a strong random string never use "supersecret">
# 3. Select the active brand design (defaults to namVibentec)
# Edit src/vibentec/devJsonFileNames.ts:
# const fileName = jsonFileNames.namVibentec; ← change to desired brand key
# 4. Run development server (Turbopack, port 8000)
yarn dev
# 5. Open browser — middleware will redirect to /{DEFAULT_REGION}/
open http://localhost:8000
```
**Build for production:**
```bash
yarn build
yarn start
```
> `next.config.js` calls `checkEnvVariables()` at startup. Missing required env vars will cause a descriptive build failure.
---
### 4. Key Domain Entities
Inferred from `@medusajs/types` (`HttpTypes`) as used by the data layer, plus local types in `src/types/global.ts`.
| Entity | Key Fields | Source |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| **StoreProduct** | `id`, `title`, `handle`, `thumbnail`, `variants[]`, `tags[]`, `metadata`, `collection`, `categories[]` | `lib/data/products.ts` |
| **StoreProductVariant** | `id`, `calculated_price`, `inventory_quantity`, `options[]` | Fetched with `*variants.calculated_price` field selector |
| **StoreCart** | `id`, `region_id`, `items[]`, `shipping_address`, `billing_address`, `email`, `promotions[]`, `shipping_methods[]`, `payment_collection` | `lib/data/cart.ts` |
| **StoreLineItem** | `id`, `variant_id`, `quantity`, `total`, `product`, `variant`, `thumbnail`, `metadata` | Part of StoreCart |
| **StoreRegion** | `id`, `countries[]` (each with `iso_2`) | `middleware.ts`, `lib/data/regions.ts` |
| **StoreCustomer** | `id`, `email`, `first_name`, `last_name`, `addresses[]`, `phone` | `lib/data/customer.ts` |
| **StoreOrder** | `id`, `status`, `items[]`, `shipping_address`, `payment_collections[]`, `fulfillments[]` | `lib/data/orders.ts` |
| **StoreCartShippingOption** | `id`, `name`, `amount`, `provider_id` | `lib/data/cart.ts``listCartOptions()` |
| **FeaturedProduct** _(local)_ | `id`, `title`, `handle`, `thumbnail` | `src/types/global.ts` |
| **VariantPrice** _(local)_ | `calculated_price`, `original_price`, `currency_code`, `price_type`, `percentage_diff` | `src/types/global.ts` |
| **StoreFreeShippingPrice** _(local)_ | extends `StorePrice` + `target_reached`, `target_remaining`, `remaining_percentage` | `src/types/global.ts` |
| **LayoutComponentNode** _(local)_ | `{ [ComponentName]: { config?, children? } }` | `src/vibentec/component-map.tsx` |
---
## Phase 4 — Maintenance Strategy
### 1. Immediate Risks
| Severity | Risk | Location | Action Required |
| ------------ | ---------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Critical** | Secrets committed to repository | `.env` | Rotate `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` immediately. Regenerate `REVALIDATE_SECRET`. Use `.env.local` (already `.gitignore`'d) for real values. |
| **Critical** | Silent bug: missing `await` | `src/lib/data/cart.ts:339` | `const cartId = getCartId()` is missing `await`. `cartId` resolves to a `Promise` (truthy), so the guard `if (!cartId)` never fires. `setAddresses` silently fails to validate cart existence on cold sessions. Fix: `const cartId = await getCartId()` |
| **High** | Empty stub functions silently do nothing | `src/lib/data/cart.ts:279318` | `applyGiftCard`, `removeDiscount`, `removeGiftCard` are exported but completely empty. Any UI that calls them will silently succeed with no effect. Either implement for Medusa V2 or remove the exports. |
| **High** | TypeScript & ESLint errors suppressed at build | `next.config.js:2427` | `ignoreBuildErrors: true` and `ignoreDuringBuilds: true` ship broken types silently. Re-enable both before production and resolve resulting errors. |
| **Medium** | React 19 RC pinned | `package.json` | Pre-release runtime in production. Migrate to stable React 19 once available, or document the risk explicitly. |
---
### 2. Refactoring Opportunities (Low-Hanging Fruit)
Listed in priority order — highest impact for lowest effort first.
**1. Externalize active brand config to an environment variable**
- File: `src/vibentec/devJsonFileNames.ts`
- Change the hardcoded `const fileName = jsonFileNames.namVibentec` to read from `process.env.VIBENTEC_DESIGN_CONFIG`.
- Impact: Enables true multi-tenant deployment (different brands on different deployments) without any code changes. Currently requires a code commit to switch brands.
**2. Type the `LayoutContext` properly**
- File: `src/vibentec/component-map.tsx:3438`
- Replace `customer: any` and `cart: any` with `HttpTypes.StoreCustomer | null` and `HttpTypes.StoreCart | null`.
- Impact: Propagates type safety through the entire renderer pipeline, catching prop mismatches at compile time.
**3. Remove dead dependencies**
- `pg` and `@types/pg` — no direct DB access exists in this storefront.
- `@types/react-instantsearch-dom` — no instantsearch code exists.
- Run: `yarn remove pg @types/pg @types/react-instantsearch-dom`
- Impact: Cleaner dependency graph, faster installs, less confusion for new developers.
**4. Fix hardcoded `data-mode="light"`**
- File: `src/app/layout.tsx:11`
- If dark mode is planned: drive this from a cookie or `prefers-color-scheme`. If not: add a comment documenting the intentional lock to light mode.
**5. Delete commented-out V1 cart code**
- File: `src/lib/data/cart.ts:279318`
- The commented blocks are Medusa V1 patterns. They will never be un-commented as-is. Remove them to reduce noise and cognitive load.
---
### 3. Testing Strategy
Given zero existing tests and the RSC-heavy architecture, the recommended approach is **outside-in, starting at the data boundaries**.
**Step 1 — Integration tests for the data layer** (`src/lib/data/`)
These are pure server functions with no React involved — the easiest place to start. Use **Vitest** with a real (or Docker-composed) Medusa backend in test mode.
```
Priority order: cart.ts → customer.ts → products.ts → regions.ts
```
Start with `cart.ts` — it has the most mutations, the most business logic, and already has a confirmed `await` bug. Writing a test for `setAddresses` would have caught that immediately.
**Step 2 — Component tests for the Vibentec renderer** (`src/vibentec/`)
The `DynamicLayoutRenderer` and `componentMap` are the highest-risk custom code with no safety net. Use **React Testing Library** with `@testing-library/react` (RSC-compatible via Next.js jest transform).
Write snapshot tests that pass a known JSON fixture through the full renderer pipeline. This creates a regression guard before any design JSON changes.
```typescript
// Example fixture test shape
it("renders a Header with a Banner child from JSON", () => {
const nodes = [
{ Header: { children: [{ Banner: { config: { variant: "nav" } } }] } },
]
const { container } = render(
<DynamicLayoutRenderer nodes={nodes} context={mockContext} />
)
expect(container).toMatchSnapshot()
})
```
**Step 3 — E2E tests for critical user journeys** using **Playwright**
Two journeys that must never break:
1. `Browse Store → Product Detail → Add to Cart → Checkout → Order Confirmed`
2. `Login → Account Dashboard → View Order History`
**Step 4 — CI enforcement**
The `.github/` directory currently only contains an issue template. Add a GitHub Actions workflow that runs on every PR:
```yaml
- yarn lint # re-enable ignoreDuringBuilds: false first
- tsc --noEmit # re-enable ignoreBuildErrors: false first
- vitest run # data layer integration tests
- playwright test # critical path E2E
```
No PR merges to `main` without all four passing.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

View File

@ -1,72 +0,0 @@
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default async function VtBrand({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? "Trusted By"
const items = props.items ?? []
const classes = {
container: props.className ?? "w-full py-12 bg-[#CFECD9]",
inner: props.innerClassName ?? "content-container flex flex-col items-center",
title: props.titleClassName ?? "text-[#1f3521] text-[20px] font-bold mb-8",
brands: props.brandsClassName ?? "flex w-full items-center justify-between gap-12",
item: props.itemClassName ?? "opacity-90",
image: props.imageClassName ?? "h-[48px] w-auto object-contain",
label: props.labelClassName ?? "text-[#1f3521] text-[36px] font-semibold",
}
if (!items || items.length === 0) {
return null
}
const renderItem = (brand: any, idx: number) => {
const content = brand.imageSrc ? (
<img
src={brand.imageSrc}
alt={brand.alt ?? brand.label ?? `brand-${idx}`}
className={clx(classes.image, brand.imageClassName)}
/>
) : (
<span className={clx(classes.label, brand.className)}>
{brand.label ?? ""}
</span>
)
return brand.href ? (
<LocalizedClientLink
key={`brand-${idx}`}
href={brand.href}
className={clx(classes.item, brand.containerClassName)}
>
{content}
</LocalizedClientLink>
) : (
<div className={clx(classes.item, brand.containerClassName)} key={`brand-${idx}`}>
{content}
</div>
)
}
return (
<section className={classes.container}>
<div className={classes.inner}>
{title && <div className={classes.title}>{title}</div>}
<div className={classes.brands}>
{items.map((brand: any, idx: number) => renderItem(brand, idx))}
</div>
</div>
</section>
)
}

View File

@ -1,86 +0,0 @@
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default async function VtCategoryHighlight({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? ""
const items = props.items ?? []
const classes = {
container: props.className ?? "content-container py-12",
title:
props.titleClassName ?? "text-[#003F31] text-[28px] font-semibold mb-6",
grid: props.gridClassName ?? "grid grid-cols-3 gap-6 w-full",
tile:
props.tileClassName ??
"relative rounded-2xl overflow-hidden bg-white aspect-square w-full",
label: props.labelClassName ?? "text-[#003F31] text-[18px] font-semibold",
image: props.imageClassName ?? "w-full h-full object-contain",
}
if (!items || items.length === 0) {
return null
}
const renderTile = (tile: any, idx: number) => {
const imageEl = tile.imageSrc ? (
<img
src={tile.imageSrc}
alt={tile.label ?? `category-${idx}`}
className={clx(classes.image, tile.imageClassName)}
/>
) : (
<div
className={clx(
"w-full h-full flex items-center justify-center",
tile.className
)}
>
<div className={clx(tile.headingClassName)}>{tile.headingLabel}</div>
<div className={tile.descriptionClassName}>{tile.descriptionLabel}</div>
<button className={tile.buttonClassName}>{tile.buttonLabel}</button>
</div>
)
const content = (
<div className={clx("relative w-full h-full")}>
{imageEl}
{tile.label && <span className={classes.label}>{tile.label}</span>}
</div>
)
return tile.href ? (
<LocalizedClientLink
key={`tile-${idx}`}
href={tile.href}
className={clx("w-full h-full", tile.className)}
>
{content}
</LocalizedClientLink>
) : (
<div className={clx(tile.className, "w-full h-full")} key={`tile-${idx}`}>
{content}
</div>
)
}
return (
<section className={classes.container}>
{title && <h2 className={classes.title}>{title}</h2>}
<div className={classes.grid}>
{items.map((tile: any, idx: number) => renderTile(tile, idx))}
</div>
</section>
)
}

View File

@ -1,74 +0,0 @@
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default async function VtFeedbackCard({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? ""
const items = props.items ?? []
const classes = {
container: props.className ?? "",
title: props.titleClassName ?? "text-[#003F31] text-[28px] font-semibold mb-10",
grid: props.gridClassName ?? "grid grid-cols-1 small:grid-cols-2 xl:grid-cols-4 gap-6",
card: props.cardClassName ?? "rounded-2xl overflow-hidden bg-[#CFECD9]",
image: props.imageClassName ?? "w-full h-[260px] object-cover",
content: props.contentClassName ?? "p-6",
name: props.nameClassName ?? "text-[#003F31] text-[20px] font-bold",
subtitle: props.subtitleClassName ?? "mt-1 text-[#003f31b3] text-[14px]",
quote: props.quoteClassName ?? "mt-4 text-[#003F31] text-[16px]",
cta: props.ctaClassName ?? "mt-6 inline-flex items-center justify-center bg-[#FCEE56] text-[#0D382E] px-6 py-2 rounded-full font-bold",
}
if (!items || items.length === 0) return null
const renderCard = (entry: any, idx: number) => {
const imageEl = entry.imageSrc ? (
<img
src={entry.imageSrc}
alt={entry.imageAlt ?? entry.name ?? `feedback-card-${idx}`}
className={classes.image}
/>
) : null
const ctaEl = entry.cta?.href ? (
<LocalizedClientLink href={entry.cta.href} className={clx(classes.cta, entry.cta?.className)}>
{entry.cta.label ?? "Mehr erfahren"}
</LocalizedClientLink>
) : entry.cta?.label ? (
<button className={clx(classes.cta, entry.cta?.className)}>{entry.cta.label}</button>
) : null
return (
<div className={clx(classes.card, entry.className)} key={`vt-feedback-card-${idx}`}>
{imageEl}
<div className={clx(classes.content)}>
{entry.name && <div className={clx(classes.name, entry.nameClassName)}>{entry.name}</div>}
{entry.subtitle && (
<div className={clx(classes.subtitle, entry.subtitleClassName)}>{entry.subtitle}</div>
)}
{entry.quote && <div className={clx(classes.quote, entry.quoteClassName)}>{entry.quote}</div>}
{ctaEl}
</div>
</div>
)
}
return (
<section className={classes.container}>
{title && <h2 className={classes.title}>{title}</h2>}
<div className={classes.grid}>{items.map((it: any, idx: number) => renderCard(it, idx))}</div>
</section>
)
}

View File

@ -1,91 +0,0 @@
"use client"
import useEmblaCarousel from "embla-carousel-react"
import Autoplay from "embla-carousel-autoplay"
import { useMemo } from "react"
import { clx } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { NextButton, PrevButton, usePrevNextButtons } from "@modules/layout/templates/vt-carousel/carousel-arrow-button"
export default function VtFeedback({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? ""
const items = props.items ?? []
const durationSeconds: number = props.duration ?? 5
const options = props.options ?? { loop: true }
const plugins = useMemo(() => {
if (!durationSeconds || durationSeconds <= 0) return []
return [
Autoplay({
delay: durationSeconds * 1000,
stopOnInteraction: false,
stopOnMouseEnter: true,
}),
]
}, [durationSeconds])
const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins)
const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = usePrevNextButtons(emblaApi)
const classes = {
container: props.className ?? "content-container py-16",
title: props.titleClassName ?? "text-[#1f3521] text-[28px] font-bold text-center mb-10",
viewport: "relative overflow-hidden",
containerInner: "flex",
slide: props.itemClassName ?? "min-w-full px-6",
slideInner: "flex flex-col items-center text-center gap-3",
stars: props.starsClassName ?? "text-[#C4622C] text-xl leading-none",
reviewTitle: props.reviewTitleClassName ?? "text-[#1f3521] font-bold",
reviewText: props.reviewTextClassName ?? "text-[#1f3521]",
author: props.authorClassName ?? "italic text-[#1f3521]",
controls: props.controls,
}
if (!items || items.length === 0) return null
const showControls = items.length > 1 && classes.controls
const renderStars = (rating?: number) => {
const count = Math.max(0, Math.min(5, Math.round(rating ?? 5)))
return "★★★★★".slice(0, count)
}
return (
<section className={classes.container}>
{title && <h2 className={classes.title}>{title}</h2>}
<div className={classes.viewport} ref={emblaRef}>
<div className={classes.containerInner}>
{items.map((it: any, idx: number) => (
<div className={classes.slide} key={`feedback-${idx}`}>
<div className={classes.slideInner}>
<div className={classes.stars}>{renderStars(it.rating)}</div>
{it.title && <div className={classes.reviewTitle}>{it.title}</div>}
{it.text && <div className={classes.reviewText}>{it.text}</div>}
{it.author && <div className={classes.author}>{it.author}</div>}
</div>
</div>
))}
</div>
{showControls && (
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 flex items-center justify-between px-4">
<div className="pointer-events-auto">
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
</div>
<div className="pointer-events-auto">
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</div>
</div>
)}
</div>
</section>
)
}

View File

@ -1,205 +0,0 @@
"use client"
import { Button } from "@medusajs/ui"
import { clx } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import React, { useState } from "react"
interface BenefitItem {
icon?: string
imgSrc?: string
title?: string
description?: string
className?: string
iconClassName?: string
titleClassName?: string
descriptionClassName?: string
}
export default function VtSubcription({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const [firstName, setFirstName] = useState("")
const [email, setEmail] = useState("")
const [accepted, setAccepted] = useState(false)
const [submitted, setSubmitted] = useState(false)
const classes = {
container: props.className ?? "content-container",
left: props.leftClassName ?? "",
card: props.cardClassName ?? "rounded-2xl bg-[#CFECD9] p-8 small:p-12",
title:
props.titleClassName ??
"text-white text-[28px] font-bold text-center",
description:
props.descriptionClassName ?? "mt-2 text-white",
highlight: props.highlightClassName ?? "font-bold",
form: props.formClassName ?? "mt-8 flex flex-col gap-6",
fields: props.fieldsClassName ?? "grid grid-cols-1 small:grid-cols-2 gap-4",
input:
props.inputClassName ??
"h-[52px] rounded-md border border-[#003F31]/40 px-4 bg-transparent text-white",
checkboxRow: props.checkboxRowClassName ?? "flex items-center gap-3",
checkbox:
props.checkboxClassName ??
"w-5 h-5 rounded-md border border-[#003F31]/60",
checkboxLabel: props.checkboxLabelClassName ?? "text-white text-[16px]",
subtextClass: props.subtextClassName ?? "text-white",
submit:
props.submitClassName ?? "",
success: props.successClassName ?? "mt-4 text-center text-white",
benefits:
props.benefitsClassName ??
"mt-8 grid grid-cols-1 small:grid-cols-3 gap-8",
benefitItem:
props.benefitItemClassName ??
"flex flex-col items-center text-center gap-3",
benefitIcon:
props.benefitIconClassName ??
"w-12 h-12 rounded-full bg-[#003F31] text-white flex items-center justify-center",
benefitTitle: props.benefitTitleClassName ?? "text-white font-semibold",
benefitDesc: props.benefitDescClassName ?? "text-white opacity-80",
subtextSubcribe: props.subtextSubcribe ?? {},
}
const submitConfig = props.cta ?? {}
const policyLabel: string =
props.policyLabel ?? "Ich habe die DSGVO gelesen und akzeptiere sie."
const firstNameField = props.firstName ?? null
const emailField = props.email ?? null
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!accepted) return
setSubmitted(true)
console.log("subscription_submit", { firstName, email, accepted })
}
return (
<section className={classes.container}>
{classes.left && <div className={classes.left}>
half
</div>}
<div className={classes.card}>
{props.title && <h2 className={classes.title}>{props.title}</h2>}
{props.description && (
<p className={classes.description}>
{props.descriptionPrefix}{" "}
<span className={classes.highlight}>
{props.descriptionHighlight}
</span>{" "}
{props.descriptionSuffix}
</p>
)}
{Array.isArray(props.benefits) && props.benefits.length > 0 && (
<div className={classes.benefits}>
{props.benefits.map((b: BenefitItem, i: number) => (
<div
key={`benefit-${i}`}
className={clx(classes.benefitItem, b.className)}
>
{b.imgSrc ? (
<img
src={b.imgSrc}
alt={b.title ?? `benefit-${i}`}
className={clx(classes.benefitIcon, b.iconClassName)}
/>
) : (
<div
className={clx(classes.benefitIcon, b.iconClassName)}
aria-hidden="true"
>
{b.icon ?? ""}
</div>
)}
{b.title && (
<div className={clx(classes.benefitTitle, b.titleClassName)}>
{b.title}
</div>
)}
{b.description && (
<div
className={clx(classes.benefitDesc, b.descriptionClassName)}
>
{b.description}
</div>
)}
</div>
))}
</div>
)}
{props.subtext && (
<p
className={clx(
"mt-2 text-center", classes?.subtextClass ?? "text-[#003F31]"
)}
>
{props.subtext}
</p>
)}
<form className={classes.form} onSubmit={onSubmit}>
<div className={classes.fields}>
{firstNameField && (
<input
type="text"
placeholder={firstNameField.placeholder ?? "Vorname"}
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className={clx(classes.input, firstNameField.className)}
/>
)}
{emailField && (
<input
type="email"
placeholder={emailField.placeholder ?? "E-Mail-Adresse"}
value={email}
onChange={(e) => setEmail(e.target.value)}
className={clx(classes.input, emailField.className)}
required
/>
)}
</div>
{props.newCheckboxRowClassName && (
<label className={classes.checkboxRow}>
<input
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className={classes.checkbox}
/>
<span className={classes.checkboxLabel}>{policyLabel}</span>
</label>
)}
<button
type="submit"
className={clx(classes.submit, submitConfig.className)}
>
{submitConfig.label ?? "Anmelden"}
</button>
</form>
{classes?.subtextSubcribe && (
<div className={props.subtextSubcribe?.className}>
{props.subtextSubcribe?.label}
</div>
)}
{submitted && (
<div className={classes.success}>
{props.successMessage ??
"Danke! Prüfe deine E-Mails für den Rabattcode."}
</div>
)}
</div>
</section>
)
}

View File

@ -57,7 +57,7 @@ export default function ProductCard({
const classes = { const classes = {
card: styles?.card ?? className ?? "", card: styles?.card ?? className ?? "",
badge: { badge: {
container: styles?.badge?.container ?? " pb-4", container: styles?.badge?.container ?? "p-4",
text: text:
styles?.badge?.text ?? styles?.badge?.text ??
"z-20 px-3 py-1 border-[0.5px] rounded bg-[#c9e0f5] txt-compact-small-plus shadow-borders-base text-[#285A86] ", "z-20 px-3 py-1 border-[0.5px] rounded bg-[#c9e0f5] txt-compact-small-plus shadow-borders-base text-[#285A86] ",
@ -68,9 +68,9 @@ export default function ProductCard({
}, },
subtitle: styles?.subtitle ?? "", subtitle: styles?.subtitle ?? "",
content: styles?.content ?? "p-6 flex flex-col flex-1", content: styles?.content ?? "p-6 flex flex-col flex-1",
title: styles?.title ?? "", title: styles?.title ?? "mt-2 text-ui-fg-base",
price: styles?.price ?? "", price: styles?.price ?? "mt-2 flex items-baseline gap-2",
description: styles?.description ?? "", description: styles?.description ?? "txt-small text-[#285A86] my-4",
reviews: { reviews: {
container: styles?.reviews?.container ?? undefined, container: styles?.reviews?.container ?? undefined,
stars: styles?.reviews?.stars ?? "flex gap-1", stars: styles?.reviews?.stars ?? "flex gap-1",
@ -83,18 +83,15 @@ export default function ProductCard({
count: styles?.reviews?.count, count: styles?.reviews?.count,
}, },
button: { button: {
addToCart: styles?.button?.addToCart ?? "", addToCart: styles?.button?.addToCart ?? "flex-1",
moreInfo: styles?.button?.moreInfo ?? "", moreInfo: styles?.button?.moreInfo ?? "w-full",
isShowIcon: styles?.button?.isShowIcon ?? false, isShowIcon: styles?.button?.isShowIcon ?? true,
}, },
} }
return ( return (
<div className={clx(classes.card)}> <div className={clx(classes.card)}>
<LocalizedClientLink <LocalizedClientLink href={`/products/${product.handle}`} className="block">
href={`/products/${product.handle}`}
className="block"
>
<div className="relative"> <div className="relative">
{badgeText && ( {badgeText && (
<div className={classes.badge.container}> <div className={classes.badge.container}>
@ -121,10 +118,7 @@ export default function ProductCard({
</LocalizedClientLink> </LocalizedClientLink>
)} )}
<LocalizedClientLink <LocalizedClientLink href={`/products/${product.handle}`} className="block">
href={`/products/${product.handle}`}
className="block"
>
<Heading <Heading
level="h3" level="h3"
className={classes.title} className={classes.title}
@ -134,11 +128,9 @@ export default function ProductCard({
</Heading> </Heading>
</LocalizedClientLink> </LocalizedClientLink>
{classes.price && (
<div className={classes.price}> <div className={classes.price}>
{cheapestPrice && <PreviewPrice price={cheapestPrice} />} {cheapestPrice && <PreviewPrice price={cheapestPrice} />}
</div> </div>
)}
{(classes.reviews.rating !== undefined || {(classes.reviews.rating !== undefined ||
classes.reviews.count !== undefined) && ( classes.reviews.count !== undefined) && (
@ -190,14 +182,9 @@ export default function ProductCard({
</div> </div>
)} )}
{classes.description && ( <Text className={clx(classes.description, "txt-small my-4")}>{description}</Text>
<Text className={clx(classes.description, "txt-small my-4")}>
{description}
</Text>
)}
<div className="flex gap-3 mt-auto"> <div className="flex gap-3 mt-auto">
{classes.button?.addToCart && (
<Button <Button
formAction={handleAddToCart} formAction={handleAddToCart}
disabled={!inStock} disabled={!inStock}
@ -206,9 +193,6 @@ export default function ProductCard({
> >
Add to cart {classes.button.isShowIcon && <Plus />} Add to cart {classes.button.isShowIcon && <Plus />}
</Button> </Button>
)}
{classes.button?.moreInfo && (
<LocalizedClientLink <LocalizedClientLink
href={`/products/${product.handle}`} href={`/products/${product.handle}`}
className="flex-1" className="flex-1"
@ -217,7 +201,6 @@ export default function ProductCard({
More Info {classes.button.isShowIcon && <ChevronRight />} More Info {classes.button.isShowIcon && <ChevronRight />}
</Button> </Button>
</LocalizedClientLink> </LocalizedClientLink>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -26,11 +26,6 @@ import Hero from "@modules/layout/templates/hero"
import { VtCarousel } from "@modules/layout/templates/vt-carousel" import { VtCarousel } from "@modules/layout/templates/vt-carousel"
import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner" import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner"
import VtFeaturedProducts from "@modules/home/components/vt-featured-products" import VtFeaturedProducts from "@modules/home/components/vt-featured-products"
import VtCategoryHighlight from "@modules/home/components/vt-category-highlight"
import VtBrand from "@modules/home/components/vt-brand"
import VtFeedback from "@modules/home/components/vt-feedback"
import VtFeedbackCard from "@modules/home/components/vt-feedback-card"
import VtSubcription from "@modules/home/components/vt-subcription"
type ComponentConfig = Record<string, any> type ComponentConfig = Record<string, any>
@ -106,11 +101,6 @@ export const componentMap: Record<string, ComponentRenderer> = {
Footer: nodesContextRenderer(VtFooter), Footer: nodesContextRenderer(VtFooter),
ImageDisplayer: nodesContextRenderer(VtCarousel), ImageDisplayer: nodesContextRenderer(VtCarousel),
VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts), VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts),
VtCategoryHighlight: nodesContextRenderer(VtCategoryHighlight),
VtBrand: nodesContextRenderer(VtBrand),
VtFeedback: nodesContextRenderer(VtFeedback),
VtFeedbackCard: nodesContextRenderer(VtFeedbackCard),
VtSubcription: nodesContextRenderer(VtSubcription),
} }
export type ComponentName = keyof typeof componentMap export type ComponentName = keyof typeof componentMap

View File

@ -10,7 +10,6 @@ async function readDesignFile() {
return JSON.parse(fileData) return JSON.parse(fileData)
} }
export async function loadLayoutConfig() { export async function loadLayoutConfig() {
const config = await readDesignFile() const config = await readDesignFile()
if (Array.isArray(config)) return config if (Array.isArray(config)) return config