feat(modules): redesign admin marketplace cards and detail view

ModuleCard moves badges to the top-right, shows a cover placeholder when art is missing, and drops the rating/pricing chrome that was never populated by the marketplace. ModuleDetailView splits into a hero row (cover + module info, two-thirds width) plus a sticky action card on the right (one-third) so install/update/purchase buttons stay visible when scrolling long descriptions.

ModuleIndexView promotes the marketplace API token form to a persistent card at the top of the page and adds an authenticated/premium status pill so super-admins can see whether the current token unlocks premium listings. The tabs and empty state were reorganized so 'installed' and 'marketplace' feel like peers.

The admin modules store tracks marketplace auth status, adds checkApiToken() and setApiToken() methods, and unifies the install-request shape into ModuleInstallPayload so both the free and paid install buttons route through the same code path.
This commit is contained in:
Darko Gjorgjijoski
2026-04-10 19:00:00 +02:00
parent 23d1476870
commit 3d79fe1abc
4 changed files with 525 additions and 410 deletions

View File

@@ -2,25 +2,17 @@ import { defineStore } from 'pinia'
import { moduleService } from '../../../api/services/module.service'
import type {
Module,
ModuleReview,
ModuleFaq,
ModuleLink,
ModuleScreenshot,
} from '../../../types/domain/module'
import type {
ModuleCheckResponse,
ModuleDetailResponse,
ModuleInstallPayload,
} from '../../../api/services/module.service'
// ----------------------------------------------------------------
// Types
// ----------------------------------------------------------------
export interface ModuleDetailMeta {
modules: Module[]
}
export interface ModuleDetailResponse {
data: Module
meta: ModuleDetailMeta
}
export interface InstallationStep {
translationKey: string
stepUrl: string
@@ -40,6 +32,11 @@ export interface ModuleState {
currentUser: {
api_token: string | null
}
marketplaceStatus: {
authenticated: boolean
premium: boolean
invalidToken: boolean
}
enableModules: string[]
}
@@ -51,6 +48,11 @@ export const useModuleStore = defineStore('modules', {
currentUser: {
api_token: null,
},
marketplaceStatus: {
authenticated: false,
premium: false,
invalidToken: false,
},
enableModules: [],
}),
@@ -70,25 +72,30 @@ export const useModuleStore = defineStore('modules', {
async fetchModule(slug: string): Promise<ModuleDetailResponse> {
const response = await moduleService.get(slug)
const data = response as unknown as ModuleDetailResponse
if ((data as Record<string, unknown>).error === 'invalid_token') {
this.currentModule = null
this.modules = []
this.apiToken = null
this.currentUser.api_token = null
return data
}
this.currentModule = data
return data
this.currentModule = response
return response
},
async checkApiToken(token: string): Promise<{ success: boolean; error?: string }> {
async checkApiToken(token: string): Promise<ModuleCheckResponse> {
const response = await moduleService.checkToken(token)
return {
success: response.success ?? false,
error: response.error,
this.marketplaceStatus = {
authenticated: response.authenticated ?? false,
premium: response.premium ?? false,
invalidToken: response.error === 'invalid_token',
}
return response
},
setApiToken(token: string | null): void {
this.apiToken = token
this.currentUser.api_token = token
},
clearMarketplaceStatus(): void {
this.marketplaceStatus = {
authenticated: false,
premium: false,
invalidToken: false,
}
},
@@ -101,8 +108,7 @@ export const useModuleStore = defineStore('modules', {
},
async installModule(
moduleName: string,
version: string,
payload: ModuleInstallPayload,
onStepUpdate?: (step: InstallationStep) => void,
): Promise<boolean> {
const steps: InstallationStep[] = [
@@ -145,13 +151,25 @@ export const useModuleStore = defineStore('modules', {
try {
const stepFns: Record<string, () => Promise<Record<string, unknown>>> = {
'/api/v1/modules/download': () =>
moduleService.download({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
moduleService.download({
...payload,
path: path ?? undefined,
}) as Promise<Record<string, unknown>>,
'/api/v1/modules/unzip': () =>
moduleService.unzip({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
moduleService.unzip({
...payload,
path: path ?? undefined,
}) as Promise<Record<string, unknown>>,
'/api/v1/modules/copy': () =>
moduleService.copy({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
moduleService.copy({
...payload,
path: path ?? undefined,
}) as Promise<Record<string, unknown>>,
'/api/v1/modules/complete': () =>
moduleService.complete({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
moduleService.complete({
...payload,
path: path ?? undefined,
}) as Promise<Record<string, unknown>>,
}
const result = await stepFns[step.stepUrl]()