Compare commits
No commits in common. "main" and "namds/implement-highlight-and-subcription-section" have entirely different histories.
main
...
namds/impl
|
|
@ -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"
|
|
||||||
|
|
@ -563,20 +563,15 @@
|
||||||
"cardClassName": "rounded-2xl overflow-hidden bg-[#CFECD9] w-[800px] p-10",
|
"cardClassName": "rounded-2xl overflow-hidden bg-[#CFECD9] w-[800px] p-10",
|
||||||
"title": "10% für dich!",
|
"title": "10% für dich!",
|
||||||
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
|
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
|
||||||
"highlightClassName": "text-[#003F31] font-bold",
|
|
||||||
"description": true,
|
"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",
|
"descriptionPrefix": "Melde dich jetzt zum 3Bears Newsletter an und sichere dir",
|
||||||
"descriptionHighlight": "10% Rabatt auf deinen nächsten Einkauf!",
|
"descriptionHighlight": "10% Rabatt auf deinen nächsten Einkauf!",
|
||||||
"subtextClassName": "text-[#003F31] text-[16px] text-center",
|
|
||||||
"descriptionSuffix": "",
|
"descriptionSuffix": "",
|
||||||
"subtext": "Deinen Rabattcode bekommst du von uns per Mail.",
|
"subtext": "Deinen Rabattcode bekommst du von uns per Mail.",
|
||||||
"firstName": { "placeholder": "Vorname" },
|
"firstName": { "placeholder": "Vorname" },
|
||||||
"email": { "placeholder": "E-Mail-Adresse" },
|
"email": { "placeholder": "E-Mail-Adresse" },
|
||||||
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
|
"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" }
|
"cta": { "label": "Anmelden" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
340
docs/PRD.md
340
docs/PRD.md
|
|
@ -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:279–318` | `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:24–27` | `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:34–38`
|
|
||||||
- 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:279–318`
|
|
||||||
- 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.
|
|
||||||
|
|
@ -188,8 +188,8 @@ export default function VtSubcription({
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
{classes?.subtextSubcribe && (
|
{classes?.subtextSubcribe && (
|
||||||
<div className={props.subtextSubcribe?.className}>
|
<div className={props.subtextSubcribe.className}>
|
||||||
{props.subtextSubcribe?.label}
|
{props.subtextSubcribe.label}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{submitted && (
|
{submitted && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue