feat: wire up paidreviews (#3656)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from lnbits.helpers import (
|
||||
download_url,
|
||||
@@ -777,6 +777,32 @@ class ExtensionDetailsRequest(BaseModel):
|
||||
version: str
|
||||
|
||||
|
||||
class ExtensionReviewsStatus(BaseModel):
|
||||
tag: str
|
||||
avg_rating: float
|
||||
review_count: int
|
||||
|
||||
|
||||
class CreateExtensionReview(BaseModel):
|
||||
tag: str
|
||||
name: str | None = Field(None)
|
||||
rating: int = Field(..., ge=0, le=1000)
|
||||
comment: str | None = Field(None)
|
||||
|
||||
|
||||
class ExtensionReviewPaymentRequest(BaseModel):
|
||||
payment_hash: str
|
||||
payment_request: str
|
||||
|
||||
|
||||
class ExtensionReview(BaseModel):
|
||||
id: str
|
||||
name: str | None = Field(default=None)
|
||||
tag: str | None = Field(default=None)
|
||||
rating: int = Field(default=0, ge=0, le=1000)
|
||||
comment: str | None = Field(default=None)
|
||||
|
||||
|
||||
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
if settings.lnbits_ext_github_token:
|
||||
|
||||
@@ -2,8 +2,10 @@ import sys
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.requests import Request
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_extensions
|
||||
@@ -14,10 +16,14 @@ from lnbits.core.models import (
|
||||
)
|
||||
from lnbits.core.models.extensions import (
|
||||
CreateExtension,
|
||||
CreateExtensionReview,
|
||||
Extension,
|
||||
ExtensionConfig,
|
||||
ExtensionMeta,
|
||||
ExtensionRelease,
|
||||
ExtensionReview,
|
||||
ExtensionReviewPaymentRequest,
|
||||
ExtensionReviewsStatus,
|
||||
InstallableExtension,
|
||||
PayToEnableInfo,
|
||||
ReleasePaymentInfo,
|
||||
@@ -34,6 +40,7 @@ from lnbits.core.services.extensions import (
|
||||
install_extension,
|
||||
uninstall_extension,
|
||||
)
|
||||
from lnbits.db import Page
|
||||
from lnbits.decorators import (
|
||||
check_account_exists,
|
||||
check_account_id_exists,
|
||||
@@ -589,3 +596,49 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
for ext in installable_exts
|
||||
]
|
||||
return extension_data
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/reviews/tags",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
)
|
||||
async def get_extension_reviews_tags() -> list[ExtensionReviewsStatus]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(settings.lnbits_extensions_reviews_url + "/tags")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return [ExtensionReviewsStatus(**item) for item in data]
|
||||
|
||||
|
||||
@extension_router.get(
|
||||
"/reviews/{ext_id}",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
)
|
||||
async def get_extension_reviews(ext_id: str, request: Request) -> Page[ExtensionReview]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
query_string = str(request.query_params)
|
||||
resp = await client.get(
|
||||
settings.lnbits_extensions_reviews_url + f"/reviews/{ext_id}?{query_string}"
|
||||
)
|
||||
resp.raise_for_status()
|
||||
reviews = resp.json()
|
||||
return Page(
|
||||
data=[ExtensionReview(**item) for item in reviews["data"]],
|
||||
total=reviews["total"],
|
||||
)
|
||||
|
||||
|
||||
@extension_router.put(
|
||||
"/reviews",
|
||||
dependencies=[Depends(check_account_exists)],
|
||||
)
|
||||
async def create_extension_review(
|
||||
data: CreateExtensionReview,
|
||||
) -> ExtensionReviewPaymentRequest:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
settings.lnbits_extensions_reviews_url + "/reviews", json=data.dict()
|
||||
)
|
||||
resp.raise_for_status()
|
||||
payment_request = resp.json()
|
||||
return ExtensionReviewPaymentRequest(**payment_request)
|
||||
|
||||
@@ -107,6 +107,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||
"has_holdinvoice": settings.has_holdinvoice,
|
||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||
|
||||
@@ -52,6 +52,13 @@ class ExtensionsSettings(LNbitsSettings):
|
||||
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||
lnbits_extensions_builder_activate_non_admins: bool = Field(default=False)
|
||||
lnbits_extensions_reviews_url: str = Field(
|
||||
default="https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r",
|
||||
description="""
|
||||
URL for the paid reviews.
|
||||
Regular users can view this URL (not secret).
|
||||
""",
|
||||
)
|
||||
lnbits_extensions_manifests: list[str] = Field(
|
||||
default=[
|
||||
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -187,6 +187,25 @@ window.localisation.en = {
|
||||
'Only admin accounts can create extensions',
|
||||
admin_only: 'Admin Only',
|
||||
new_version: 'New Version',
|
||||
reviews_url: 'Reviews URL',
|
||||
reviews_url_label: 'Reviews server URL',
|
||||
reviews_url_hint:
|
||||
'Full PaidReviews URL including the settings id (e.g. https://example.com/paidreviews/SETTINGS_ID)',
|
||||
reviews_open: 'View reviews',
|
||||
reviews_leave: 'Leave a review',
|
||||
reviews_name: 'Your name',
|
||||
reviews_comment: 'Your review',
|
||||
reviews_rating: 'Rating',
|
||||
reviews_submit: 'Submit review',
|
||||
reviews_loading: 'Loading reviews...',
|
||||
reviews_refresh: 'Refresh reviews',
|
||||
reviews_error_load: 'Could not load reviews',
|
||||
reviews_url_not_configured: 'Reviews URL not configured',
|
||||
reviews_pay_invoice: 'Pay invoice',
|
||||
reviews_invoice_paid: 'Invoice paid',
|
||||
reviews_invoice_title: 'Pay this invoice to submit your review',
|
||||
reviews_count: 'Reviews',
|
||||
no_reviews: 'No reviews yet',
|
||||
extension_has_free_release: 'Has free releases',
|
||||
extension_has_paid_release: 'Has paid releases',
|
||||
extension_depends_on: 'Depends on:',
|
||||
|
||||
@@ -3,7 +3,34 @@ window.app.component(QrcodeVue)
|
||||
window.app.component('lnbits-extension-rating', {
|
||||
template: '#lnbits-extension-rating',
|
||||
name: 'lnbits-extension-rating',
|
||||
props: ['rating']
|
||||
props: {
|
||||
rating: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayRating() {
|
||||
return Math.round((this.rating || 0) * 2) / 2
|
||||
},
|
||||
hasData() {
|
||||
return this.count !== null && this.count !== undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
if (!this.clickable) return
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.app.component('lnbits-fsat', {
|
||||
|
||||
@@ -27,7 +27,62 @@ window.PageExtensions = {
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null,
|
||||
searchToggle: false
|
||||
searchToggle: false,
|
||||
reviewsUrl: null,
|
||||
reviewsDialog: {
|
||||
show: false,
|
||||
extension: null,
|
||||
loading: false,
|
||||
submitting: false,
|
||||
form: {
|
||||
name: '',
|
||||
rating: 0,
|
||||
comment: ''
|
||||
},
|
||||
error: null
|
||||
},
|
||||
reviews: [],
|
||||
reviewsTable: {
|
||||
loading: false,
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: this.$t('Name'),
|
||||
field: 'name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'comment',
|
||||
align: 'left',
|
||||
label: this.$t('Comment'),
|
||||
field: 'comment'
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
align: 'left',
|
||||
label: this.$t('Date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
name: 'rating',
|
||||
align: 'right',
|
||||
label: 'Rating',
|
||||
field: 'rating'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 5,
|
||||
sortBy: 'created_at',
|
||||
descending: true,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
paymentDialog: {
|
||||
show: false,
|
||||
invoice: '',
|
||||
hash: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -72,7 +127,6 @@ window.PageExtensions = {
|
||||
// the install logic has been triggered one way or another
|
||||
this.unsubscribeFromPaylinkWs()
|
||||
|
||||
const extension = this.selectedExtension
|
||||
this.selectedExtension.inProgress = true
|
||||
this.showManageExtensionDialog = false
|
||||
release.payment_hash =
|
||||
@@ -286,8 +340,7 @@ window.PageExtensions = {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/${extension.id}/releases`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
`/api/v1/extension/${extension.id}/releases`
|
||||
)
|
||||
|
||||
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
||||
@@ -325,6 +378,9 @@ window.PageExtensions = {
|
||||
if (!detailsLink) {
|
||||
return
|
||||
}
|
||||
// keep a reference to the extension so we can show rating stats
|
||||
this.selectedExtension =
|
||||
this.extensions.find(ext => ext.id === extId) || this.selectedExtension
|
||||
this.selectedExtensionDetails = null
|
||||
this.showExtensionDetailsDialog = true
|
||||
this.slide = 0
|
||||
@@ -333,8 +389,7 @@ window.PageExtensions = {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`,
|
||||
this.g.user.wallets[0].inkey
|
||||
`/api/v1/extension/${extId}/details?details_link=${detailsLink}`
|
||||
)
|
||||
|
||||
this.selectedExtensionDetails = data
|
||||
@@ -455,7 +510,7 @@ window.PageExtensions = {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${extId}/invoice/install`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
null,
|
||||
{
|
||||
ext_id: extId,
|
||||
archive: release.archive,
|
||||
@@ -471,7 +526,7 @@ window.PageExtensions = {
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/${extId}/invoice/enable`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
null,
|
||||
{
|
||||
amount
|
||||
}
|
||||
@@ -562,8 +617,7 @@ window.PageExtensions = {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/release/${org}/${repo}/${release.version}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
`/api/v1/extension/release/${org}/${repo}/${release.version}`
|
||||
)
|
||||
release.loaded = true
|
||||
release.is_version_compatible = data.is_version_compatible
|
||||
@@ -588,18 +642,13 @@ window.PageExtensions = {
|
||||
continue
|
||||
}
|
||||
ext.inProgress = true
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`/api/v1/extension`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
ext_id: ext.id,
|
||||
archive: ext.latestRelease.archive,
|
||||
source_repo: ext.latestRelease.source_repo,
|
||||
payment_hash: ext.latestRelease.payment_hash,
|
||||
version: ext.latestRelease.version
|
||||
}
|
||||
)
|
||||
await LNbits.api.request('POST', `/api/v1/extension`, null, {
|
||||
ext_id: ext.id,
|
||||
archive: ext.latestRelease.archive,
|
||||
source_repo: ext.latestRelease.source_repo,
|
||||
payment_hash: ext.latestRelease.payment_hash,
|
||||
version: ext.latestRelease.version
|
||||
})
|
||||
count++
|
||||
ext.isAvailable = true
|
||||
ext.isInstalled = true
|
||||
@@ -624,6 +673,163 @@ window.PageExtensions = {
|
||||
})
|
||||
this.showUpdateAllDialog = false
|
||||
},
|
||||
formatAvg(raw) {
|
||||
const val = Number(raw || 0)
|
||||
return Math.round((val / 2 / 100) * 2) / 2
|
||||
},
|
||||
|
||||
async loadReviewStats() {
|
||||
if (!this.reviewsUrl) {
|
||||
console.info('Extension reviews are not configured')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/reviews/tags`
|
||||
)
|
||||
const map = {}
|
||||
data.forEach(stat => {
|
||||
map[stat.tag] = stat
|
||||
})
|
||||
this.extensions.forEach(ext => {
|
||||
ext.reviewStats = map[ext.id] || null
|
||||
})
|
||||
this.filterExtensions(this.searchTerm, this.tab)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
async openReviews(extension) {
|
||||
const targetExtension =
|
||||
extension ||
|
||||
(this.selectedExtensionDetails
|
||||
? this.extensions.find(e => e.id === this.selectedExtensionDetails.id)
|
||||
: null)
|
||||
if (!targetExtension) {
|
||||
return
|
||||
}
|
||||
if (!this.reviewsUrl) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: this.$t('reviews_url_not_configured')
|
||||
})
|
||||
return
|
||||
}
|
||||
this.reviewsDialog.extension = targetExtension
|
||||
this.selectedExtension = extension
|
||||
this.reviewsDialog.show = true
|
||||
await this.getTagReviews()
|
||||
},
|
||||
async getTagReviews(props) {
|
||||
if (!this.reviewsUrl) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: this.$t('reviews_url_not_configured')
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.reviewsTable.loading = true
|
||||
const params = LNbits.utils.prepareFilterQuery(this.reviewsTable, props)
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/api/v1/extension/reviews/${this.selectedExtension.id}?${params}`
|
||||
)
|
||||
this.reviews = data.data
|
||||
this.reviewsTable.pagination.rowsNumber = data.total
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
} finally {
|
||||
this.reviewsTable.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
formatReviewDate(val) {
|
||||
if (!val) return ''
|
||||
const numeric = Number(val)
|
||||
if (!Number.isNaN(numeric)) {
|
||||
return this.utils.formatTimestamp(numeric)
|
||||
}
|
||||
return this.utils.formatDate(val)
|
||||
},
|
||||
async submitReview() {
|
||||
if (!this.reviewsDialog.extension || !this.reviewsUrl) {
|
||||
return
|
||||
}
|
||||
this.reviewsDialog.submitting = true
|
||||
try {
|
||||
const payload = {
|
||||
tag: this.reviewsDialog.extension.id,
|
||||
name: this.reviewsDialog.form.name,
|
||||
rating: this.reviewsDialog.form.rating * 100,
|
||||
comment: this.reviewsDialog.form.comment
|
||||
}
|
||||
const {data} = await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/extension/reviews`,
|
||||
null,
|
||||
payload
|
||||
)
|
||||
if (data.payment_request) {
|
||||
this.openInvoiceDialog(data.payment_request, data.payment_hash)
|
||||
} else {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Review submitted'
|
||||
})
|
||||
this.resetReviewForm()
|
||||
await this.getTagReviews()
|
||||
await this.loadReviewStats()
|
||||
}
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.reviewsDialog.submitting = false
|
||||
}
|
||||
},
|
||||
openInvoiceDialog(invoice, hash) {
|
||||
this.paymentDialog.invoice = invoice
|
||||
this.paymentDialog.hash = hash
|
||||
this.paymentDialog.show = true
|
||||
this.listenForPayment(hash)
|
||||
},
|
||||
resetReviewForm() {
|
||||
this.reviewsDialog.form = {
|
||||
name: '',
|
||||
rating: 0,
|
||||
comment: ''
|
||||
}
|
||||
this.paymentDialog = {
|
||||
show: false,
|
||||
invoice: '',
|
||||
hash: ''
|
||||
}
|
||||
},
|
||||
listenForPayment(hash) {
|
||||
try {
|
||||
const base = new URL(this.reviewsUrl)
|
||||
base.protocol = base.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
base.pathname = `/api/v1/ws/${hash}`
|
||||
const ws = new WebSocket(base)
|
||||
ws.addEventListener('message', async () => {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('reviews_invoice_paid')
|
||||
})
|
||||
this.paymentDialog.show = false
|
||||
this.resetReviewForm()
|
||||
setTimeout(async () => {
|
||||
await this.getTagReviews()
|
||||
}, 1000)
|
||||
|
||||
await this.loadReviewStats()
|
||||
ws.close()
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
async fetchAllExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||
@@ -638,6 +844,7 @@ window.PageExtensions = {
|
||||
async created() {
|
||||
this.extensions = await this.fetchAllExtensions()
|
||||
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
|
||||
|
||||
if (this.g.user.extensions.length === 0) {
|
||||
this.tab = 'all'
|
||||
@@ -654,6 +861,8 @@ window.PageExtensions = {
|
||||
this.hasNewVersion(ext)
|
||||
)
|
||||
|
||||
await this.loadReviewStats()
|
||||
|
||||
this.filterExtensions(this.searchTerm, this.tab)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,11 +561,32 @@ include('components/lnbits-error.vue') %}
|
||||
</template>
|
||||
|
||||
<template id="lnbits-extension-rating">
|
||||
<div style="margin-bottom: 3px">
|
||||
<q-rating v-model="rating" size="1.5em" :max="5" color="primary"
|
||||
><q-tooltip>
|
||||
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
|
||||
></q-rating>
|
||||
<div class="row items-center q-gutter-xs" style="margin-bottom: 3px">
|
||||
<q-rating
|
||||
:model-value="displayRating"
|
||||
size="1.4em"
|
||||
:max="5"
|
||||
color="primary"
|
||||
icon="star_border"
|
||||
icon-selected="star"
|
||||
icon-half="star_half"
|
||||
readonly
|
||||
:class="clickable ? 'cursor-pointer' : ''"
|
||||
@click="handleClick"
|
||||
>
|
||||
<q-tooltip v-if="!hasData">
|
||||
<span v-text="$t('extension_rating_soon')"></span>
|
||||
</q-tooltip>
|
||||
<q-tooltip v-else-if="clickable">
|
||||
<span v-text="$t('reviews_open')"></span>
|
||||
</q-tooltip>
|
||||
</q-rating>
|
||||
<div
|
||||
class="text-caption text-grey"
|
||||
v-if="count !== null && count !== undefined"
|
||||
>
|
||||
<span v-text="`(${count})`"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -137,6 +137,19 @@
|
||||
:hint="$t('extension_builder_manifest_url_hint')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
<span v-text="$t('reviews_url')"></span>
|
||||
</p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_extensions_reviews_url"
|
||||
:label="$t('reviews_url_label')"
|
||||
:hint="$t('reviews_url_hint')"
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -163,7 +163,22 @@
|
||||
v-text="extension.name"
|
||||
></div>
|
||||
<div style="justify-content: space-between; display: flex">
|
||||
<lnbits-extension-rating :rating="0" />
|
||||
<lnbits-extension-rating
|
||||
:rating="
|
||||
formatAvg(
|
||||
extension.reviewStats
|
||||
? extension.reviewStats.avg_rating
|
||||
: 0
|
||||
)
|
||||
"
|
||||
:count="
|
||||
extension.reviewStats
|
||||
? extension.reviewStats.review_count
|
||||
: null
|
||||
"
|
||||
:clickable="!!reviewsUrl"
|
||||
@click="openReviews(extension)"
|
||||
/>
|
||||
<q-btn-group size="xs" style="margin: 5px 0">
|
||||
<q-btn
|
||||
v-if="extension.hasFreeRelease"
|
||||
@@ -998,8 +1013,20 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 q-pl-sm">
|
||||
<lnbits-extension-rating
|
||||
:rating="0"
|
||||
size="2.5em"
|
||||
:rating="
|
||||
formatAvg(
|
||||
selectedExtension && selectedExtension.reviewStats
|
||||
? selectedExtension.reviewStats.avg_rating
|
||||
: 0
|
||||
)
|
||||
"
|
||||
:count="
|
||||
selectedExtension && selectedExtension.reviewStats
|
||||
? selectedExtension.reviewStats.review_count
|
||||
: null
|
||||
"
|
||||
:clickable="!!reviewsUrl"
|
||||
@click="openReviews(selectedExtension)"
|
||||
></lnbits-extension-rating>
|
||||
<div class="q-mt-md">
|
||||
<b>
|
||||
@@ -1057,6 +1084,182 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="reviewsDialog.show" position="top">
|
||||
<q-card
|
||||
class="q-pa-md lnbits__dialog-card"
|
||||
style="width: 900px; max-width: 95vw"
|
||||
>
|
||||
<q-card-section class="q-gutter-y-sm">
|
||||
<div class="row items-center">
|
||||
<div class="col">
|
||||
<div
|
||||
class="text-h6"
|
||||
v-text="
|
||||
(reviewsDialog.extension && reviewsDialog.extension.name) ||
|
||||
(reviewsDialog.extension && reviewsDialog.extension.id)
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="text-caption text-grey"
|
||||
v-text="reviewsDialog.extension && reviewsDialog.extension.id"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<lnbits-extension-rating
|
||||
:rating="
|
||||
formatAvg(
|
||||
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
||||
? reviewsDialog.extension.reviewStats.avg_rating
|
||||
: 0
|
||||
)
|
||||
"
|
||||
:count="
|
||||
reviewsDialog.extension && reviewsDialog.extension.reviewStats
|
||||
? reviewsDialog.extension.reviewStats.review_count
|
||||
: null
|
||||
"
|
||||
></lnbits-extension-rating>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section v-if="!reviewsUrl">
|
||||
<div
|
||||
class="text-negative"
|
||||
v-text="$t('reviews_url_not_configured')"
|
||||
></div>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
<div v-if="reviewsDialog.loading" class="row justify-center q-pa-lg">
|
||||
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="reviewsDialog.error" class="text-negative q-mb-md">
|
||||
<span v-text="reviewsDialog.error"></span>
|
||||
</div>
|
||||
|
||||
<q-form @submit="submitReview" class="q-gutter-md">
|
||||
<div class="row q-col-gutter-lg">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="reviewsDialog.form.name"
|
||||
:label="$t('reviews_name')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-rating
|
||||
v-model="reviewsDialog.form.rating"
|
||||
max="10"
|
||||
icon="star_border"
|
||||
icon-selected="star"
|
||||
icon-half="star_half"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
></q-rating>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
type="textarea"
|
||||
autogrow
|
||||
v-model="reviewsDialog.form.comment"
|
||||
:label="$t('reviews_comment')"
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
unelevated
|
||||
:loading="reviewsDialog.submitting"
|
||||
:disable="!reviewsDialog.form.rating"
|
||||
:label="$t('reviews_submit')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-form>
|
||||
<q-separator class="q-my-md"></q-separator>
|
||||
<div class="row q-col-gutter-lg">
|
||||
<div class="col-12">
|
||||
<div v-if="reviews.length === 0" class="text-grey q-mb-md">
|
||||
<span v-text="$t('no_reviews')"></span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<q-table
|
||||
:rows="reviews"
|
||||
:columns="reviewsTable.columns"
|
||||
v-model:pagination="reviewsTable.pagination"
|
||||
@request="getTagReviews"
|
||||
:loading="reviewsTable.loading"
|
||||
row-key="name"
|
||||
:filter="reviewsTable.search"
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="name" :props="props">
|
||||
<span v-text="props.row.name"></span>
|
||||
</q-td>
|
||||
<q-td key="comment" :props="props">
|
||||
<span v-text="props.row.comment"></span>
|
||||
</q-td>
|
||||
<q-td key="created_at" :props="props">
|
||||
<span
|
||||
v-text="formatReviewDate(props.row.created_at)"
|
||||
></span>
|
||||
</q-td>
|
||||
<q-td key="rating" :props="props">
|
||||
<q-rating
|
||||
class="float-right q-pt-xs"
|
||||
readonly
|
||||
icon="star_border"
|
||||
icon-selected="star"
|
||||
icon-half="star_half"
|
||||
:model-value="formatAvg(props.row.rating)"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
></q-rating>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator v-if="reviews.length"></q-separator>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
@click="reviewsDialog.show = false"
|
||||
color="grey"
|
||||
outline
|
||||
class="float-right"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="paymentDialog.show" position="top">
|
||||
<q-card class="q-pa-md lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-xl">
|
||||
<lnbits-qrcode
|
||||
:value="paymentDialog.invoice"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</q-card-section>
|
||||
<q-card-actions align="between">
|
||||
<q-btn v-close-popup flat color="grey" :label="$t('close')"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showUpdateAllDialog" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<div class="row">
|
||||
|
||||
Reference in New Issue
Block a user