Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ nuxt.d.ts
.output
.data
.playground/.env
coverage
framework
dist
.DS_Store
Expand All @@ -24,3 +25,8 @@ dist

# Claude Code local settings
.claude/settings.local.json

# Generated Store API types (fetched per-project, not versionized).
# Only storeApiTypes.overrides.ts is committed.
api-types/storeApiSchema.json
api-types/storeApiTypes.d.ts
8 changes: 5 additions & 3 deletions .playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ export default defineNuxtConfig({
// Required for consumer to set individual localeId
i18n: {
defaultLocale: 'en',
bundle: {
optimizeTranslationDirective: false,
},
locales: [
{
code: 'en',
Expand All @@ -76,6 +73,11 @@ export default defineNuxtConfig({
// Layer dev only
typescript: {
tsConfig: {
// Type-check the unit tests too: `nuxt typecheck` only covers `test/nuxt/**`
// by default, so our root-level `test/**` specs are otherwise invisible to
// the CLI gate (and get checked against the minimal Vitest stubs in the IDE
// instead of the real Nuxt types). Path is relative to the build dir.
include: ['../../test/**/*'],
compilerOptions: {
paths: {
'@shopware-pwa/helpers-next': ['../../node_modules/@shopware-pwa/helpers-next/dist'],
Expand Down
67 changes: 43 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ All components MUST be:
- **Single quotes** for strings
- **Foundation components** preferred over custom styling

### TypeScript Patterns
- Use explicit interfaces for component props
- Access reactive values with `.value` for refs/computed
- Handle union types properly (see StructureElementImageGallery for example)
- Use type guards for complex type narrowing

## Key Composables & Patterns

- **`useCarousel()`** - Advanced carousel with responsive, SSR-ready, scroll-based implementation
Expand Down Expand Up @@ -102,13 +96,45 @@ All components MUST be:
## Testing & Quality

- Run `npm lint` before committing
- **TypeScript Validation**: Always run `npx nuxt typecheck` after implementations to catch type errors
- Run `npx nuxt typecheck` after implementations (see the [TypeScript](#typescript) section)
- Follow accessibility guidelines (WCAG compliance)
- Ensure responsive design across breakpoints
- Test with screen readers and keyboard navigation
- Verify i18n translations work correctly

### TypeScript Error Resolution Process
## Documentation

Comprehensive documentation available in `/docs/` directory:
- Read `/docs/project-overview.md` for architecture understanding
- Follow `/docs/coding-standards.md` for detailed style guidelines
- Check `/docs/common-workflows.md` for development patterns
- Review `/docs/i18n-guide.md` for internationalization best practices

## TypeScript

The project uses TypeScript with strict typing and `<script setup lang="ts">` for all components.

### Patterns
- Use explicit interfaces for component props and emits
- Access reactive values with `.value` for refs/computed
- **Never use `any`.** Prefer type guards, `?? fallback`, optional chaining, `as const`, or an explicit interface. A narrow cast (`as SpecificType`) is acceptable only when the value is genuinely known to be that type (e.g. a value the surrounding logic has already validated).
- Handle union types with proper type guards (`'property' in object` patterns); apply `(item as SpecificType)` only when necessary. See `StructureElementImageGallery.vue` for handling complex union types.

### Type Declaration Locations

Where a type is declared depends on what it describes and how widely it is used:

- **Non-API types, used once** → declare inline in the file where they are used (e.g. a component's local prop/emit interface, a ref shape).
- **Non-API types, used in multiple files** → declare in `app/types/` (auto-imported via the `types/*` import dir).
- **API-related types** (anything derived from or overriding the Shopware Store API schema) → declare in `api-types/storeApiTypes.overrides.ts`. This is the only versioned file in `api-types/`.
- **Global/ambient types** (e.g. `window`, Nuxt `RuntimeConfig`/`PublicRuntimeConfig` augmentation) → declare in `/nuxt.d.ts`.

The generated schema files `api-types/storeApiSchema.json` and `api-types/storeApiTypes.d.ts` are **not versioned** — they are fetched per project (and may come from a customer instance with custom endpoints/entities), so they are `.gitignore`d. Only `storeApiTypes.overrides.ts` is committed; put schema/operation corrections there so they survive a re-fetch.

### Validation & Error Resolution

Always run `npx nuxt typecheck` after implementations to catch type errors.

1. Run `npx nuxt typecheck` to identify type errors
2. Filter errors for specific files using `npx nuxt typecheck 2>&1 | grep "filename.vue"`
3. Common fixes:
Expand All @@ -117,21 +143,18 @@ All components MUST be:
- Add conditional rendering `v-if` to ensure objects exist before use
- Handle union types with proper type guards

## Documentation
### Known Upstream Typecheck Errors

Comprehensive documentation available in `/docs/` directory:
- Read `/docs/project-overview.md` for architecture understanding
- Follow `/docs/coding-standards.md` for detailed style guidelines
- Check `/docs/common-workflows.md` for development patterns
- Review `/docs/i18n-guide.md` for internationalization best practices
`npx nuxt typecheck` currently reports **2 errors that live inside `node_modules`** and are **not fixable from this repo** (editing `node_modules` is not durable or versioned). They originate from the upgraded Shopware dependency stack, not from our code — treat a typecheck run as clean when only these remain:

- `@shopware/nuxt-module/plugin.ts` — `import ... from "./src"` while the package only ships `dist/` (upstream packaging bug).
- `@shopware/composables/.../useNewsletter.ts` — `result.data.status` where the api-client return type resolves to `never` inside the composables package's own type context.

## TypeScript Troubleshooting
Do not attempt to "fix" these by casting to `any`, editing `node_modules`, or broadly excluding those packages from the generated tsconfig.

When encountering TypeScript errors with union types (common in CMS components):
- Use type guards with `'property' in object` patterns
- Apply type assertions `(item as SpecificType)` when necessary
- Handle generic types from components like `CarouselItem` with proper casting
- See `StructureElementImageGallery.vue` for handling complex union types
### Linting Exceptions

- When the linter shows an error for an unused const "prop", it's okay to use `// eslint-disable-next-line @typescript-eslint/no-unused-vars`.

## Important Notes

Expand All @@ -150,10 +173,6 @@ When encountering TypeScript errors with union types (common in CMS components):
- **Client-Side Conditional Rendering**:
- Always use `<ClientOnly>` with a fallback when conditionally rendering based on user authentication state or any other client-side reactive data that's not available during SSR.

## TypeScript & Linting Exceptions

- When linter shows error for unused const "prop", it's okay to use `// eslint-disable-next-line @typescript-eslint/no-unused-vars`

## Agent skills

### Issue tracker
Expand Down
39 changes: 39 additions & 0 deletions api-types/storeApiTypes.overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
operations as mainOperations,
components as mainComponents,
Schemas as mainSchemas
} from "./storeApiTypes";

export type components = mainComponents & {
schemas: Schemas;
}

/**
* Schema overrides for api-gen tool.
* Only include schemas that need to be extended.
* Note: api-gen merges these but the generated file has a bug with mainSchemas reference.
*/
type SchemaOverrides = {
CmsSlot: mainSchemas['CmsSlot'] & {}
}

// For api-gen (not working correctly due to mainSchemas reference bug)
export type Schemas = SchemaOverrides;

/**
* Full merged schemas for use in shopware.d.ts.
* Combines base schemas with our extensions.
*/
export type MergedSchemas = Omit<mainSchemas, keyof SchemaOverrides> & SchemaOverrides;

type CustomOperations = {
"sendAskAi post /ai-assistant/prompt": {
contentType?: "application/json";
accept?: "application/json";
body: components["schemas"]["Product"];
response: components["schemas"]["Product"];
responseCode: 200;
};
}

export type operations = Omit<mainOperations, keyof CustomOperations> & CustomOperations;
14 changes: 8 additions & 6 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ useBreadcrumbs();
*/
const { apiClient } = useShopwareContext();
const sessionContextData = ref<Schemas["SalesChannelContext"]>();
sessionContextData.value = await apiClient.invoke("readContext get /context");
const { data: contextData } = await apiClient.invoke("readContext get /context");
sessionContextData.value = contextData;
const { languageIdChain, setCurrency, refreshSessionContext, sessionContext } = useSessionContext(sessionContextData.value);

/**
Expand Down Expand Up @@ -45,17 +46,18 @@ if (languages.value?.elements.length && router.currentRoute.value.name) {
defaultLocale,
);

provide("cmsTranslations", messages.value[prefix ?? defaultLocale] ?? {});
const messageKey = (prefix ?? defaultLocale) as keyof typeof messages.value;
provide("cmsTranslations", messages.value[messageKey] ?? {});

// Set session language
// If locale has set specific localeId via i18n config
if (localeProperties.value.localeId) {
if (languageIdChain.value !== localeProperties.value.localeId) {
languageToChangeId = localeProperties.value.localeId;
languageToChangeId = localeProperties.value.localeId as string;
}

// Set default header sw-language-id to selected language id
apiClient.defaultHeaders['sw-language-id'] = localeProperties.value.localeId;
apiClient.defaultHeaders['sw-language-id'] = localeProperties.value.localeId as string;
} else {
// Otherwise find and set language from prefix
// Important: Shopware language iso code and i18n language code have to match
Expand All @@ -74,7 +76,7 @@ if (languages.value?.elements.length && router.currentRoute.value.name) {
await refreshSessionContext();
}

locale.value = prefix ? prefix : defaultLocale;
locale.value = (prefix ? prefix : defaultLocale) as typeof locale.value;
// Set prefix from CMS components
provide("urlPrefix", prefix);
}
Expand All @@ -87,7 +89,7 @@ const { data: currencies } = await useAsyncData("currencies", async () => {
return await getAvailableCurrencies();
});

if (currencies.value.data != null) {
if (currencies?.value?.data != null) {
storeCurrencies.value = currencies.value.data
}

Expand Down
2 changes: 1 addition & 1 deletion app/components/account/AccountLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const handlePasswordRecovery = async () => {

try {
const runtimeConfig = useRuntimeConfig()
const storefrontUrl = runtimeConfig.public.shopwareDevStorefrontUrl || window.location.origin
const storefrontUrl = runtimeConfig.public.shopware.devStorefrontUrl || window.location.origin

await resetPassword({
email: formData.email,
Expand Down
19 changes: 13 additions & 6 deletions app/components/account/AccountPersonalData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,25 @@ const handleSubmit = async () => {
isSuccess.value = false

try {
const updateData: operations["changeProfile post /account/change-profile"]["body"] = {
const baseData = {
firstName: state.firstName,
lastName: state.lastName,
salutationId: state.salutationId,
title: state.title || undefined
}

if (state.accountType === 'business') {
updateData.company = state.company
updateData.vatIds = [state.vatIds]
updateData.accountType = state.accountType
}
// The change-profile body is a discriminated union on accountType, so the
// business-specific fields have to be part of the same object literal
// rather than assigned afterwards.
const updateData: operations["changeProfile post /account/change-profile"]["body"] =
state.accountType === 'business'
? {
...baseData,
accountType: 'business',
company: state.company,
vatIds: [state.vatIds]
}
: baseData

await updatePersonalInfo(updateData)
await refreshUser()
Expand Down
4 changes: 2 additions & 2 deletions app/components/cart/CartItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const removeItem = () => {
<!-- Product image -->
<div class="relative w-20 h-20 flex-shrink-0 rounded">
<FoundationImage
v-if="item.cover?.media?.url"
:src="item.cover.media.url"
v-if="item.cover?.url"
:src="item.cover.url"
:alt="item.label || ''"
class="w-full h-full object-cover rounded"
/>
Expand Down
2 changes: 1 addition & 1 deletion app/components/checkout/CheckoutShipping.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<!-- Price -->
<div class="ml-4 text-right">
<span v-if="method.prices?.[0]" class="font-medium text-primary">
{{ getFormattedPrice(method.prices[0].unitPrice) }}
{{ getFormattedPrice(method.prices[0].currencyPrice?.[0]?.gross) }}
</span>
<span v-else class="text-sm">
{{ $t('checkout.shipping.free') }}
Expand Down
2 changes: 1 addition & 1 deletion app/components/component/ComponentDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const contentEl = ref()
onClickOutside(contentEl, () => closeDropdown(), { ignore: [triggerEl] })
const { activate, deactivate } = useFocusTrap(contentEl, { allowOutsideClick: true })

function toggleDropdown (e: PointerEvent) {
function toggleDropdown (e: MouseEvent) {
if (!props.disabled && e.type === 'click') {
isOpen.value = !isOpen.value
if (isOpen.value) {
Expand Down
17 changes: 10 additions & 7 deletions app/components/component/ComponentToolTip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,26 @@ const props = withDefaults(defineProps<ComponentToolTipProps>(), {


const { width: windowWidth } = useWindowSize()
const positionBasedOnWindowSize = computed<'top' | 'right' | 'bottom' | 'left'>(() => {
const positionBasedOnWindowSize = computed<TooltipPosition>(() => {
const width = windowWidth.value
const breakpoints = props.breakpoints

let position: TooltipPosition | undefined
if (width < 640) {
return breakpoints.base
position = breakpoints.base
} else if (width < 768) {
return breakpoints['640']
position = breakpoints['640']
} else if (width < 1024) {
return breakpoints['768']
position = breakpoints['768']
} else if (width < 1280) {
return breakpoints['1024']
position = breakpoints['1024']
} else if (width < 1536) {
return breakpoints['1280']
position = breakpoints['1280']
} else {
return breakpoints['1536']
position = breakpoints['1536']
}

return position ?? 'top'
})

const positionClasses = {
Expand Down
4 changes: 2 additions & 2 deletions app/components/forms/FormsContact.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Schemas } from "#shopware"
import type { CmsElementForm } from "@shopware/composables"
import { ApiClientError } from "@shopware/api-client"

type CmsSlotWithContactConfig = Schemas["CmsSlot"] & {
type CmsSlotWithContactConfig = CmsElementForm & {
translated?: {
config?: {
title?: {
Expand Down
5 changes: 3 additions & 2 deletions app/components/forms/FormsNewsletter.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { Schemas, RequestParameters } from "#shopware";
import type { RequestParameters } from "#shopware";
import type { CmsElementForm } from "@shopware/composables";
import { ApiClientError } from "@shopware/api-client";

type CmsSlotWithTitle = Schemas["CmsSlot"] & {
type CmsSlotWithTitle = CmsElementForm & {
translated?: {
config?: {
title?: {
Expand Down
22 changes: 5 additions & 17 deletions app/components/frontend/FrontendDetailPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,15 @@ const { data: productResponse } = await useAsyncData(
async () => {
return await search(props.navigationId, {
withCmsAssociations: true,
associations: {
manufacturer: {
associations: {
media: {}
}
},
media: {
associations: {
media: {}
}
},
cover: {
associations: {
media: {}
}
}
}
associations: productDetailAssociations
})
},
)

if (!productResponse.value) {
throw createError({ statusCode: 404, statusMessage: 'Product not found' })
}

const { product } = useProduct(
productResponse.value.product,
productResponse.value.configurator,
Expand Down
2 changes: 1 addition & 1 deletion app/components/layout/LayoutHeader.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const { loadNavigationElements } = useNavigationCached()
const { loadNavigationElements } = useNavigation()
const { data } = useAsyncData("navigation", () => {
return loadNavigationElements({ depth: 3 })
})
Expand Down
Loading