mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-16 01:34:08 +00:00
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:
@@ -1,83 +1,72 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative shadow-md border-2 border-line-default/60 rounded-lg cursor-pointer overflow-hidden h-100"
|
||||
class="group relative rounded-xl border border-line-default bg-surface cursor-pointer overflow-hidden transition-shadow hover:shadow-lg"
|
||||
@click="$router.push(`/admin/administration/modules/${data.slug}`)"
|
||||
>
|
||||
<div
|
||||
v-if="data.purchased"
|
||||
class="absolute mt-5 px-6 w-full flex justify-end"
|
||||
>
|
||||
<label
|
||||
v-if="data.purchased"
|
||||
class="bg-white/75 text-xs px-3 py-1 font-semibold tracking-wide rounded"
|
||||
<!-- Cover -->
|
||||
<div class="relative h-36 overflow-hidden">
|
||||
<img
|
||||
v-if="data.cover"
|
||||
class="w-full h-full object-cover object-center transition-transform group-hover:scale-105"
|
||||
:src="data.cover"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center"
|
||||
>
|
||||
{{ $t('modules.purchased') }}
|
||||
</label>
|
||||
<label
|
||||
v-if="data.installed"
|
||||
class="ml-2 bg-white/75 text-xs px-3 py-1 font-semibold tracking-wide rounded"
|
||||
>
|
||||
<span v-if="data.update_available">
|
||||
{{ $t('modules.update_available') }}
|
||||
<BaseIcon name="PuzzlePieceIcon" class="h-10 w-10 text-primary-400" />
|
||||
</div>
|
||||
|
||||
<!-- Badges -->
|
||||
<div class="absolute top-2.5 right-2.5 flex gap-1.5">
|
||||
<span class="bg-white/85 backdrop-blur-sm text-xs px-2 py-0.5 font-medium rounded-md text-heading">
|
||||
{{ data.access_tier === 'premium' ? 'Premium' : 'Public' }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('modules.installed') }}
|
||||
<span
|
||||
v-if="data.installed"
|
||||
class="text-xs px-2 py-0.5 font-medium rounded-md"
|
||||
:class="data.update_available
|
||||
? 'bg-amber-100/90 text-amber-700'
|
||||
: 'bg-green-100/90 text-green-700'"
|
||||
>
|
||||
{{ data.update_available ? $t('modules.update_available') : $t('modules.installed') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
class="lg:h-64 md:h-48 w-full object-cover object-center"
|
||||
:src="data.cover ?? ''"
|
||||
alt="cover"
|
||||
/>
|
||||
|
||||
<div class="px-6 py-5 flex flex-col bg-surface-secondary flex-1 justify-between">
|
||||
<span class="text-lg sm:text-2xl font-medium whitespace-nowrap truncate text-primary-500">
|
||||
<!-- Info -->
|
||||
<div class="p-4">
|
||||
<h3 class="text-base font-semibold text-heading truncate">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div v-if="data.author_avatar" class="flex items-center mt-2">
|
||||
<div class="flex items-center gap-1.5 mt-1 text-xs text-muted">
|
||||
<img
|
||||
class="hidden h-10 w-10 rounded-full sm:inline-block mr-2"
|
||||
v-if="data.author_avatar"
|
||||
class="h-4 w-4 rounded-full"
|
||||
:src="data.author_avatar"
|
||||
alt=""
|
||||
/>
|
||||
<span>by</span>
|
||||
<span class="ml-2 text-base font-semibold truncate">
|
||||
{{ data.author_name }}
|
||||
<span>{{ data.author_name }}</span>
|
||||
<span v-if="data.latest_module_version" class="ml-auto font-medium text-body">
|
||||
v{{ data.latest_module_version }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<base-text
|
||||
:text="data.short_description ?? ''"
|
||||
class="pt-4 text-muted h-16 line-clamp-2"
|
||||
/>
|
||||
|
||||
<div class="flex justify-between mt-4 flex-col space-y-2 sm:space-y-0 sm:flex-row">
|
||||
<div>
|
||||
<BaseRating :rating="averageRating" />
|
||||
</div>
|
||||
<div class="text-xl md:text-2xl font-semibold whitespace-nowrap text-primary-500">
|
||||
$
|
||||
{{ data.monthly_price ? data.monthly_price / 100 : (data.yearly_price ?? 0) / 100 }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2.5 text-xs text-muted leading-relaxed line-clamp-2">
|
||||
{{ data.short_description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Module } from '../../../../types/domain/module'
|
||||
|
||||
interface Props {
|
||||
data: Module
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const averageRating = computed<number>(() => {
|
||||
return parseInt(String(props.data.average_rating ?? 0), 10)
|
||||
})
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user