feat: add PopupAd and AdminPopupAd interfaces with CRUD operations
- Introduced PopupAd and AdminPopupAd interfaces in common.ts. - Implemented encoding, decoding, and JSON conversion methods for both PopupAd and AdminPopupAd. - Added new RPC methods for managing PopupAds in admin.ts and me.ts, including list, create, update, and delete functionalities. - Integrated PopupAdsClient in grpcClient.ts for gRPC communication. - Updated auth store to handle real-time notifications for user-specific topics. - Modified tsconfig.json to include auto-imports and components type definitions.
This commit is contained in:
6
components.d.ts
vendored
6
components.d.ts
vendored
@@ -30,7 +30,6 @@ declare module 'vue' {
|
||||
AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||
BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
@@ -72,6 +71,7 @@ declare module 'vue' {
|
||||
PlayIcon: typeof import('./src/components/icons/PlayIcon.vue')['default']
|
||||
PlusIcon: typeof import('./src/components/icons/PlusIcon.vue')['default']
|
||||
PlusSquareIcon: typeof import('./src/components/icons/PlusSquareIcon.vue')['default']
|
||||
PopupAdsRuntime: typeof import('./src/components/PopupAdsRuntime.vue')['default']
|
||||
RepeatIcon: typeof import('./src/components/icons/RepeatIcon.vue')['default']
|
||||
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
@@ -95,6 +95,7 @@ declare module 'vue' {
|
||||
VolumeOffIcon: typeof import('./src/components/icons/VolumeOffIcon.vue')['default']
|
||||
VueHead: typeof import('./src/components/VueHead.tsx')['default']
|
||||
WifiIcon: typeof import('./src/components/icons/WifiIcon.vue')['default']
|
||||
Windows: typeof import('./src/components/icons/windows.vue')['default']
|
||||
XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
|
||||
XIcon: typeof import('./src/components/icons/XIcon.vue')['default']
|
||||
}
|
||||
@@ -120,7 +121,6 @@ declare global {
|
||||
const AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||
const BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
const BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
@@ -162,6 +162,7 @@ declare global {
|
||||
const PlayIcon: typeof import('./src/components/icons/PlayIcon.vue')['default']
|
||||
const PlusIcon: typeof import('./src/components/icons/PlusIcon.vue')['default']
|
||||
const PlusSquareIcon: typeof import('./src/components/icons/PlusSquareIcon.vue')['default']
|
||||
const PopupAdsRuntime: typeof import('./src/components/PopupAdsRuntime.vue')['default']
|
||||
const RepeatIcon: typeof import('./src/components/icons/RepeatIcon.vue')['default']
|
||||
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
@@ -185,6 +186,7 @@ declare global {
|
||||
const VolumeOffIcon: typeof import('./src/components/icons/VolumeOffIcon.vue')['default']
|
||||
const VueHead: typeof import('./src/components/VueHead.tsx')['default']
|
||||
const WifiIcon: typeof import('./src/components/icons/WifiIcon.vue')['default']
|
||||
const Windows: typeof import('./src/components/icons/windows.vue')['default']
|
||||
const XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
|
||||
const XIcon: typeof import('./src/components/icons/XIcon.vue')['default']
|
||||
}
|
||||
@@ -121,7 +121,8 @@
|
||||
"playerConfigs": "Player Configs",
|
||||
"domains": "Allowed Domains",
|
||||
"ads": "Ads & VAST",
|
||||
"danger": "Danger Zone"
|
||||
"danger": "Danger Zone",
|
||||
"popupAds": "Popup Ads"
|
||||
},
|
||||
"content": {
|
||||
"fallbackTitle": "Settings",
|
||||
@@ -157,6 +158,10 @@
|
||||
"danger": {
|
||||
"title": "Danger Zone",
|
||||
"subtitle": "Irreversible and destructive actions. Be careful!"
|
||||
},
|
||||
"popupAds": {
|
||||
"title": "Popup Ads",
|
||||
"subtitle": "Manage your popup ad settings and preferences."
|
||||
}
|
||||
},
|
||||
"notificationSettings": {
|
||||
@@ -501,6 +506,68 @@
|
||||
"failedDetail": "Failed to load or update VAST templates."
|
||||
}
|
||||
},
|
||||
"popupAds": {
|
||||
"createItem": "Add Popup Ad",
|
||||
"maxTriggersLabel": "Highest URL trigger limit per session ({{count}})",
|
||||
"emptyTitle": "No popup ads yet",
|
||||
"emptySubtitle": "Create a popup ad to start opening URLs or injecting scripts.",
|
||||
"types": {
|
||||
"url": "URL",
|
||||
"script": "Script"
|
||||
},
|
||||
"table": {
|
||||
"label": "Label",
|
||||
"type": "Type",
|
||||
"target": "Target",
|
||||
"maxTriggersPerSession": "Max triggers/session"
|
||||
},
|
||||
"dialog": {
|
||||
"createTitle": "Create Popup Ad",
|
||||
"editTitle": "Edit Popup Ad",
|
||||
"type": "Type",
|
||||
"label": "Label",
|
||||
"labelPlaceholder": "e.g., Ad Network 1",
|
||||
"url": "Destination URL",
|
||||
"urlPlaceholder": "https://example.com/landing-page",
|
||||
"script": "Script snippet",
|
||||
"scriptPlaceholder": "<script async src=\"//example.com/ad.js\"></script>",
|
||||
"maxTriggersPerSession": "Max popup triggers per session",
|
||||
"activeTitle": "Item status",
|
||||
"activeDescription": "Disable an item to keep it in the table without serving it.",
|
||||
"update": "Update",
|
||||
"create": "Create"
|
||||
},
|
||||
"info": {
|
||||
"urlTitle": "URL:",
|
||||
"urlDescription": "Opens in a new tab when viewer clicks.",
|
||||
"scriptTitle": "Script:",
|
||||
"scriptDescription": "Injects the script tag into the page (popunder networks, etc)."
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Are you sure you want to delete \"{{name}}\"?",
|
||||
"deleteHeader": "Delete Popup Ad",
|
||||
"deleteAccept": "Delete",
|
||||
"deleteReject": "Cancel"
|
||||
},
|
||||
"toast": {
|
||||
"labelRequiredSummary": "Label required",
|
||||
"labelRequiredDetail": "Please enter a label for this popup ad.",
|
||||
"valueRequiredSummary": "Value required",
|
||||
"valueRequiredDetail": "Please enter a URL or script snippet.",
|
||||
"maxTriggersRequiredSummary": "Trigger limit required",
|
||||
"maxTriggersRequiredDetail": "Please enter a max trigger count greater than 0 for URL popup ads.",
|
||||
"invalidUrlSummary": "Invalid URL",
|
||||
"invalidUrlDetail": "Please enter a valid URL.",
|
||||
"createdSummary": "Popup ad created",
|
||||
"createdDetail": "The popup ad has been added.",
|
||||
"updatedSummary": "Popup ad updated",
|
||||
"updatedDetail": "The popup ad has been updated.",
|
||||
"deletedSummary": "Popup ad deleted",
|
||||
"deletedDetail": "The popup ad has been removed.",
|
||||
"failedSummary": "Action failed",
|
||||
"failedDetail": "Failed to load or update popup ads."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile Information",
|
||||
"subtitle": "Manage your personal information and account details.",
|
||||
@@ -768,7 +835,7 @@
|
||||
},
|
||||
"overview": {
|
||||
"welcome": {
|
||||
"title": "Hello, {{{name}}}",
|
||||
"title": "Hello, {{name}}",
|
||||
"subtitle": "Here's what's happening with your content today."
|
||||
},
|
||||
"stats": {
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
"playerConfigs": "Cấu hình trình phát",
|
||||
"domains": "Tên miền được phép",
|
||||
"ads": "Quảng cáo & VAST",
|
||||
"danger": "Vùng nguy hiểm"
|
||||
"danger": "Vùng nguy hiểm",
|
||||
"popupAds": "Popup Ads"
|
||||
},
|
||||
"content": {
|
||||
"fallbackTitle": "Cài đặt",
|
||||
@@ -157,6 +158,10 @@
|
||||
"danger": {
|
||||
"title": "Vùng nguy hiểm",
|
||||
"subtitle": "Hành động không thể hoàn tác và có tính phá hủy. Hãy cẩn thận!"
|
||||
},
|
||||
"popupAds": {
|
||||
"title": "Popup Ads",
|
||||
"subtitle": "Quản lý cài đặt và tùy chọn quảng cáo popup của bạn."
|
||||
}
|
||||
},
|
||||
"notificationSettings": {
|
||||
@@ -501,6 +506,68 @@
|
||||
"failedDetail": "Không thể tải hoặc cập nhật mẫu VAST."
|
||||
}
|
||||
},
|
||||
"popupAds": {
|
||||
"createItem": "Thêm popup ad",
|
||||
"maxTriggersLabel": "Giới hạn trigger URL cao nhất mỗi phiên ({{count}})",
|
||||
"emptyTitle": "Chưa có popup ad",
|
||||
"emptySubtitle": "Tạo popup ad để bắt đầu mở URL hoặc inject script.",
|
||||
"types": {
|
||||
"url": "URL",
|
||||
"script": "Script"
|
||||
},
|
||||
"table": {
|
||||
"label": "Nhãn",
|
||||
"type": "Loại",
|
||||
"target": "Đích",
|
||||
"maxTriggersPerSession": "Số trigger tối đa/phiên"
|
||||
},
|
||||
"dialog": {
|
||||
"createTitle": "Tạo popup ad",
|
||||
"editTitle": "Sửa popup ad",
|
||||
"type": "Loại",
|
||||
"label": "Nhãn",
|
||||
"labelPlaceholder": "ví dụ: Ad Network 1",
|
||||
"url": "URL đích",
|
||||
"urlPlaceholder": "https://example.com/landing-page",
|
||||
"script": "Đoạn script",
|
||||
"scriptPlaceholder": "<script async src=\"//example.com/ad.js\"></script>",
|
||||
"maxTriggersPerSession": "Số lần popup tối đa mỗi phiên",
|
||||
"activeTitle": "Trạng thái mục",
|
||||
"activeDescription": "Tắt một mục để giữ nó trong bảng mà không phân phối nó.",
|
||||
"update": "Cập nhật",
|
||||
"create": "Tạo"
|
||||
},
|
||||
"info": {
|
||||
"urlTitle": "URL:",
|
||||
"urlDescription": "Mở tab mới khi người xem nhấp.",
|
||||
"scriptTitle": "Script:",
|
||||
"scriptDescription": "Inject script tag vào trang cho các mạng popup/popunder."
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{{name}}\"?",
|
||||
"deleteHeader": "Xóa popup ad",
|
||||
"deleteAccept": "Xóa",
|
||||
"deleteReject": "Hủy"
|
||||
},
|
||||
"toast": {
|
||||
"labelRequiredSummary": "Thiếu nhãn",
|
||||
"labelRequiredDetail": "Vui lòng nhập nhãn cho popup ad này.",
|
||||
"valueRequiredSummary": "Thiếu giá trị",
|
||||
"valueRequiredDetail": "Vui lòng nhập URL hoặc đoạn script.",
|
||||
"maxTriggersRequiredSummary": "Thiếu giới hạn trigger",
|
||||
"maxTriggersRequiredDetail": "Vui lòng nhập số trigger lớn hơn 0 cho popup ad loại URL.",
|
||||
"invalidUrlSummary": "URL không hợp lệ",
|
||||
"invalidUrlDetail": "Vui lòng nhập URL hợp lệ.",
|
||||
"createdSummary": "Đã tạo popup ad",
|
||||
"createdDetail": "Popup ad đã được thêm.",
|
||||
"updatedSummary": "Đã cập nhật popup ad",
|
||||
"updatedDetail": "Popup ad đã được cập nhật.",
|
||||
"deletedSummary": "Đã xóa popup ad",
|
||||
"deletedDetail": "Popup ad đã được gỡ bỏ.",
|
||||
"failedSummary": "Thao tác thất bại",
|
||||
"failedDetail": "Không thể tải hoặc cập nhật popup ads."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Thông tin hồ sơ",
|
||||
"subtitle": "Quản lý thông tin cá nhân và chi tiết tài khoản của bạn.",
|
||||
@@ -767,7 +834,7 @@
|
||||
},
|
||||
"overview": {
|
||||
"welcome": {
|
||||
"title": "Xin chào, {{{name}}}",
|
||||
"title": "Xin chào, {{name}}",
|
||||
"subtitle": "Đây là tình hình nội dung của bạn hôm nay."
|
||||
},
|
||||
"stats": {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Upload from "@/routes/upload/Upload.vue";
|
||||
import DashboardNav from "./DashboardNav.vue";
|
||||
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
||||
import PopupAdsRuntime from "./PopupAdsRuntime.vue";
|
||||
|
||||
</script>
|
||||
|
||||
@@ -22,5 +23,6 @@ import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
||||
</div>
|
||||
<GlobalUploadIndicator />
|
||||
<Upload />
|
||||
<PopupAdsRuntime />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
75
src/components/PopupAdsRuntime.vue
Normal file
75
src/components/PopupAdsRuntime.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import ClientOnly from '@/components/ClientOnly';
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
let activeItem: any | null = null;
|
||||
let clickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
let scriptNode: HTMLScriptElement | null = null;
|
||||
let triggerCount = 0;
|
||||
|
||||
const triggerKey = (id: string) => `popup_ad_triggers:${id}`;
|
||||
|
||||
const cleanupScript = () => {
|
||||
if (scriptNode?.parentNode) {
|
||||
scriptNode.parentNode.removeChild(scriptNode);
|
||||
}
|
||||
scriptNode = null;
|
||||
};
|
||||
|
||||
const attachUrlHandler = () => {
|
||||
if (!activeItem?.id || typeof window === 'undefined') return;
|
||||
const maxTriggers = Number(activeItem.maxTriggersPerSession || 1);
|
||||
triggerCount = Number(sessionStorage.getItem(triggerKey(activeItem.id)) || '0');
|
||||
|
||||
clickHandler = () => {
|
||||
if (!activeItem?.value || triggerCount >= maxTriggers) return;
|
||||
triggerCount += 1;
|
||||
sessionStorage.setItem(triggerKey(activeItem.id), String(triggerCount));
|
||||
window.open(activeItem.value, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
window.addEventListener('click', clickHandler, { capture: true });
|
||||
};
|
||||
|
||||
const attachScript = () => {
|
||||
if (!activeItem?.value || typeof document === 'undefined') return;
|
||||
cleanupScript();
|
||||
scriptNode = document.createElement('script');
|
||||
scriptNode.async = true;
|
||||
scriptNode.text = activeItem.value;
|
||||
document.body.appendChild(scriptNode);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await rpcClient.getActivePopupAd();
|
||||
activeItem = response.item || null;
|
||||
if (!activeItem?.isActive) return;
|
||||
|
||||
if (activeItem.type === 'script') {
|
||||
attachScript();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeItem.type === 'url') {
|
||||
attachUrlHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (clickHandler && typeof window !== 'undefined') {
|
||||
window.removeEventListener('click', clickHandler, { capture: true } as EventListenerOptions);
|
||||
}
|
||||
cleanupScript();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<span class="hidden" />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
7
src/components/icons/windows.vue
Normal file
7
src/components/icons/windows.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 468"><path d="M170 74v64h64V74h288v192h-96v64h96c35 0 64-29 64-64V74c0-35-29-64-64-64H234c-35 0-64 29-64 64z" fill="var(--fill1)"/><path d="M74 138c-35 0-64 29-64 64v192c0 35 29 64 64 64h288c35 0 64-29 64-64V202c0-35-29-64-64-64H74zm24 80h240c13 0 24 11 24 24s-11 24-24 24H98c-13 0-24-11-24-24s11-24 24-24z" fill="var(--fill4)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="596" height="468" viewBox="-10 -226 596 468"><path d="M512-184H224c-18 0-32 14-32 32v16h-32v-16c0-35 29-64 64-64h288c35 0 64 29 64 64V40c0 35-29 64-64 64h-48V72h48c18 0 32-14 32-32v-192c0-18-14-32-32-32zM352-56H64c-18 0-32 14-32 32V8h352v-32c0-18-14-32-32-32zm32 96H32v128c0 18 14 32 32 32h288c18 0 32-14 32-32V40zM64-88h288c35 0 64 29 64 64v192c0 35-29 64-64 64H64c-35 0-64-29-64-64V-24c0-35 29-64 64-64z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { computed, useSlots } from 'vue';
|
||||
// Vue macro is available at compile time; provide a safe fallback for typecheck.
|
||||
declare const defineModelModifiers: undefined | (<T>() => T);
|
||||
|
||||
|
||||
type Props = {
|
||||
as?: 'input' | 'textarea' | 'select';
|
||||
modelValue?: string | number | null;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
@@ -21,14 +20,17 @@ type Props = {
|
||||
max?: number | string;
|
||||
step?: number | string;
|
||||
maxlength?: number;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'input',
|
||||
modelValue: '',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
rows: 3,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,10 +42,13 @@ const modelModifiers = (typeof defineModelModifiers === 'function'
|
||||
? defineModelModifiers<{ number?: boolean }>()
|
||||
: ({} as { number?: boolean }));
|
||||
|
||||
const isNumberLike = computed(() => props.type === 'number' || !!modelModifiers.number);
|
||||
const isNumberLike = computed(() => props.as === 'input' && (props.type === 'number' || !!modelModifiers.number));
|
||||
const hasLeadingSlot = computed(() => props.as === 'input' && !!useSlots().prefix);
|
||||
const isTextarea = computed(() => props.as === 'textarea');
|
||||
const isSelect = computed(() => props.as === 'select');
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
const el = e.target as HTMLInputElement;
|
||||
const el = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
const raw = el.value;
|
||||
if (isNumberLike.value) {
|
||||
if (raw === '') {
|
||||
@@ -66,11 +71,40 @@ const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-head
|
||||
|
||||
<template>
|
||||
<div :class="cn('relative', wrapperClass)">
|
||||
<div v-if="$slots.prefix" class="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/50">
|
||||
<div v-if="hasLeadingSlot" class="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/50">
|
||||
<slot name="prefix" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-if="isTextarea"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:value="modelValue ?? ''"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:maxlength="maxlength"
|
||||
:class="cn(baseInputClass, inputClass)"
|
||||
@input="onInput"
|
||||
@keyup="onKeyup"
|
||||
/>
|
||||
|
||||
<select
|
||||
v-else-if="isSelect"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:value="modelValue ?? ''"
|
||||
:disabled="disabled"
|
||||
:class="cn(baseInputClass, inputClass)"
|
||||
@change="onInput"
|
||||
@keyup="onKeyup"
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-else
|
||||
:id="id"
|
||||
:name="name"
|
||||
:type="type"
|
||||
@@ -83,7 +117,7 @@ const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-head
|
||||
:max="max"
|
||||
:step="step"
|
||||
:maxlength="maxlength"
|
||||
:class="cn(baseInputClass, $slots.prefix ? 'pl-10' : '', inputClass)"
|
||||
:class="cn(baseInputClass, hasLeadingSlot ? 'pl-10' : '', inputClass)"
|
||||
@input="onInput"
|
||||
@keyup="onKeyup"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts" generic="TData extends Record<string, any>">
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
FlexRender,
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
type SortingState,
|
||||
type Updater,
|
||||
} from '@tanstack/vue-table';
|
||||
import { ref } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
type TableColumnMeta = ColumnMeta<TData, any> & {
|
||||
headerClass?: string;
|
||||
@@ -28,11 +30,34 @@ const props = withDefaults(defineProps<{
|
||||
headerRowClass?: string;
|
||||
bodyRowClass?: string | ((row: Row<TData>) => string | undefined);
|
||||
getRowId?: (originalRow: TData, index: number) => string;
|
||||
pagination?: boolean;
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
totalRecords?: number;
|
||||
rowsPerPage?: number;
|
||||
pageSizeOptions?: number[];
|
||||
canPreviousPage?: boolean;
|
||||
canNextPage?: boolean;
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: 'No data available.',
|
||||
pagination: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalRecords: 0,
|
||||
rowsPerPage: 10,
|
||||
pageSizeOptions: () => [],
|
||||
canPreviousPage: false,
|
||||
canNextPage: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'previous-page'): void;
|
||||
(e: 'next-page'): void;
|
||||
(e: 'page-size-change', value: number): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const sorting = ref<SortingState>([]);
|
||||
|
||||
function updateSorting(updaterOrValue: Updater<SortingState>) {
|
||||
@@ -64,6 +89,27 @@ function resolveBodyRowClass(row: Row<TData>) {
|
||||
? props.bodyRowClass(row)
|
||||
: props.bodyRowClass;
|
||||
}
|
||||
|
||||
const shouldRenderPagination = computed(() => (
|
||||
props.pagination
|
||||
&& !props.loading
|
||||
&& table.getRowModel().rows.length > 0
|
||||
));
|
||||
|
||||
function previousPage() {
|
||||
if (!props.canPreviousPage) return;
|
||||
emit('previous-page');
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (!props.canNextPage) return;
|
||||
emit('next-page');
|
||||
}
|
||||
|
||||
function changePageSize(event: Event) {
|
||||
const nextValue = Number((event.target as HTMLSelectElement).value) || props.rowsPerPage;
|
||||
emit('page-size-change', nextValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -150,5 +196,21 @@ function resolveBodyRowClass(row: Row<TData>) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="shouldRenderPagination" class="flex flex-col gap-3 border-t border-gray-200 bg-muted/20 px-6 py-4 text-xs text-foreground/55 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>{{ t('common.page', { current: currentPage, total: totalPages }) }} · {{ totalRecords }} {{ t('common.records') }}</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label v-if="pageSizeOptions.length" class="flex items-center gap-2">
|
||||
<span>{{ t('common.rowsPerPage') }}</span>
|
||||
<select class="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground" :value="String(rowsPerPage)" @change="changePageSize">
|
||||
<option v-for="option in pageSizeOptions" :key="option" :value="String(option)">{{ option }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 xl:justify-end">
|
||||
<AppButton size="sm" variant="secondary" :disabled="!canPreviousPage" @click="previousPage">{{ t('common.previous') }}</AppButton>
|
||||
<AppButton size="sm" variant="secondary" :disabled="!canNextPage" @click="nextPage">{{ t('common.next') }}</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +27,11 @@ type NotificationApiItem = {
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
type IncomingNotificationEnvelope = {
|
||||
type?: string;
|
||||
payload?: NotificationApiItem;
|
||||
};
|
||||
|
||||
const notifications = ref<AppNotification[]>([]);
|
||||
const loading = ref(false);
|
||||
const loaded = ref(false);
|
||||
@@ -45,6 +50,31 @@ const normalizeType = (value?: string): NotificationType => {
|
||||
}
|
||||
};
|
||||
|
||||
const mapNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
id: item.id || '',
|
||||
type: normalizeType(item.type),
|
||||
title: item.title || '',
|
||||
message: item.message || '',
|
||||
time: '',
|
||||
read: Boolean(item.read),
|
||||
actionUrl: item.actionUrl || undefined,
|
||||
actionLabel: item.actionLabel || undefined,
|
||||
createdAt: item.createdAt,
|
||||
});
|
||||
|
||||
const upsertNotification = (item: NotificationApiItem) => {
|
||||
const mapped = mapNotification({ ...item, read: item.read ?? false });
|
||||
if (!mapped.id) return;
|
||||
|
||||
const index = notifications.value.findIndex(notification => notification.id === mapped.id);
|
||||
if (index >= 0) {
|
||||
notifications.value[index] = { ...notifications.value[index], ...mapped };
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.value = [mapped, ...notifications.value];
|
||||
};
|
||||
|
||||
export function useNotifications() {
|
||||
const { t, i18next } = useTranslation();
|
||||
|
||||
@@ -62,23 +92,16 @@ export function useNotifications() {
|
||||
return t('notification.time.daysAgo', { count: Math.max(1, days) });
|
||||
};
|
||||
|
||||
const mapNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
id: item.id || '',
|
||||
type: normalizeType(item.type),
|
||||
title: item.title || '',
|
||||
message: item.message || '',
|
||||
const hydrateNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
...mapNotification(item),
|
||||
time: formatRelativeTime(item.createdAt),
|
||||
read: Boolean(item.read),
|
||||
actionUrl: item.actionUrl || undefined,
|
||||
actionLabel: item.actionLabel || undefined,
|
||||
createdAt: item.createdAt,
|
||||
});
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await rpcClient.listNotifications();
|
||||
notifications.value = (response.notifications || []).map(mapNotification);
|
||||
notifications.value = (response.notifications || []).map(hydrateNotification);
|
||||
loaded.value = true;
|
||||
return notifications.value;
|
||||
} finally {
|
||||
@@ -86,6 +109,22 @@ export function useNotifications() {
|
||||
}
|
||||
};
|
||||
|
||||
const ingestRealtimeNotification = (raw: string | IncomingNotificationEnvelope) => {
|
||||
try {
|
||||
|
||||
const envelope = typeof raw === 'string' ? JSON.parse(raw) as IncomingNotificationEnvelope : raw;
|
||||
if (envelope?.type !== 'notification.created' || !envelope.payload) return false;
|
||||
upsertNotification(envelope.payload);
|
||||
notifications.value = notifications.value.map(item => ({
|
||||
...item,
|
||||
time: formatRelativeTime(item.createdAt),
|
||||
}));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const markRead = async (id: string) => {
|
||||
if (!id) return;
|
||||
await rpcClient.markNotificationRead({ id });
|
||||
@@ -118,6 +157,7 @@ export function useNotifications() {
|
||||
unreadCount,
|
||||
locale: computed(() => i18next.resolvedLanguage),
|
||||
fetchNotifications,
|
||||
ingestRealtimeNotification,
|
||||
markRead,
|
||||
deleteNotification,
|
||||
markAllRead,
|
||||
|
||||
@@ -85,7 +85,9 @@ export class TinyMqttClient implements ITinyMqttClient {
|
||||
break;
|
||||
case 0xD0: // PINGRESP
|
||||
break;
|
||||
case 0x30: // PUBLISH
|
||||
case 0x30: // PUBLISH QoS 0
|
||||
case 0x32: // PUBLISH QoS 1
|
||||
case 0x34: // PUBLISH QoS 2
|
||||
this.parsePublish(data);
|
||||
break;
|
||||
}
|
||||
@@ -102,9 +104,32 @@ export class TinyMqttClient implements ITinyMqttClient {
|
||||
}
|
||||
|
||||
private parsePublish(data: Uint8Array): void {
|
||||
const tLen = (data[2] << 8) | data[3];
|
||||
const topic = this.decoder.decode(data.slice(4, 4 + tLen));
|
||||
const payload = this.decoder.decode(data.slice(4 + tLen));
|
||||
let multiplier = 1;
|
||||
let remainingLength = 0;
|
||||
let offset = 1;
|
||||
let encodedByte = 0;
|
||||
|
||||
do {
|
||||
encodedByte = data[offset++];
|
||||
remainingLength += (encodedByte & 127) * multiplier;
|
||||
multiplier *= 128;
|
||||
} while ((encodedByte & 128) !== 0 && offset < data.length);
|
||||
|
||||
const variableHeaderStart = offset;
|
||||
const topicLength = (data[offset] << 8) | data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
const topic = this.decoder.decode(data.slice(offset, offset + topicLength));
|
||||
offset += topicLength;
|
||||
|
||||
const qos = (data[0] >> 1) & 0x03;
|
||||
if (qos > 0) {
|
||||
offset += 2; // packet identifier
|
||||
}
|
||||
|
||||
const consumedFromVariableHeader = offset - variableHeaderStart;
|
||||
const payloadLength = Math.max(0, remainingLength - consumedFromVariableHeader);
|
||||
const payload = this.decoder.decode(data.slice(offset, offset + payloadLength));
|
||||
this.onMessage(topic, payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,16 @@ const routes: RouteData[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "popup-ads",
|
||||
name: "settings-popup-ads",
|
||||
component: () => import("./settings/PopupAds/PopupAds.vue"),
|
||||
meta: {
|
||||
head: {
|
||||
title: "Popup Ads - Holistream",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "player-configs",
|
||||
name: "settings-player-configs",
|
||||
@@ -234,6 +244,7 @@ const routes: RouteData[] = [
|
||||
{ path: "payments", name: "admin-payments", component: () => import("./settings/admin/Payments.vue") },
|
||||
{ path: "plans", name: "admin-plans", component: () => import("./settings/admin/Plans.vue") },
|
||||
{ path: "ad-templates", name: "admin-ad-templates", component: () => import("./settings/admin/AdTemplates.vue") },
|
||||
{ path: "popup-ads", name: "admin-popup-ads", component: () => import("./settings/admin/PopupAds.vue") },
|
||||
{ path: "player-configs", name: "admin-player-configs", component: () => import("./settings/admin/PlayerConfigs.vue") },
|
||||
{ path: "jobs", name: "admin-jobs", component: () => import("./settings/admin/Jobs.vue") },
|
||||
{ path: "agents", name: "admin-agents", component: () => import("./settings/admin/Agents.vue") },
|
||||
|
||||
@@ -13,7 +13,7 @@ import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
import { formatDate } from '../../DomainsDns/helpers';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdTemplateTable',
|
||||
|
||||
@@ -11,8 +11,8 @@ import DomainsDnsEmbedCode from './components/DomainsDnsEmbedCode.vue';
|
||||
import DomainsDnsNotices from './components/DomainsDnsNotices.vue';
|
||||
import DomainsDnsTable from './components/DomainsDnsTable.vue';
|
||||
import DomainsDnsToolbar from './components/DomainsDnsToolbar.vue';
|
||||
import { mapDomainItem, normalizeDomainInput } from './helpers';
|
||||
import type { DomainItem } from './types';
|
||||
import { normalizeDomainInput } from './helpers';
|
||||
import type { Domain } from '@/server/api/proto/app/v1/common';
|
||||
|
||||
const toast = useAppToast();
|
||||
const confirm = useAppConfirm();
|
||||
@@ -27,7 +27,7 @@ const { data: domainsSnapshot, error, isPending, refetch } = useQuery({
|
||||
key: () => ['settings', 'domains'],
|
||||
query: async () => {
|
||||
const response = await rpcClient.listDomains();
|
||||
return (response.domains || []).map(mapDomainItem);
|
||||
return (response.domains || []);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,16 +126,16 @@ const handleAddDomain = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveDomain = (domain: DomainItem) => {
|
||||
const handleRemoveDomain = (domain: Domain) => {
|
||||
confirm.require({
|
||||
message: t('settings.domainsDns.confirm.removeMessage', { domain: domain.name }),
|
||||
header: t('settings.domainsDns.confirm.removeHeader'),
|
||||
acceptLabel: t('settings.domainsDns.confirm.removeAccept'),
|
||||
rejectLabel: t('settings.domainsDns.confirm.removeReject'),
|
||||
accept: async () => {
|
||||
removingId.value = domain.id;
|
||||
removingId.value = domain.id!;
|
||||
try {
|
||||
await rpcClient.deleteDomain({ id: domain.id });
|
||||
await rpcClient.deleteDomain({ id: domain.id! });
|
||||
await refetch();
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
|
||||
@@ -3,31 +3,31 @@ import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
import type { Domain } from '@/server/api/proto/app/v1/common';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, h } from 'vue';
|
||||
import type { DomainItem } from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
domains: DomainItem[];
|
||||
domains: Domain[];
|
||||
isInitialLoading: boolean;
|
||||
adding: boolean;
|
||||
removingId: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', domain: DomainItem): void;
|
||||
(e: 'remove', domain: Domain): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
const columns = computed<ColumnDef<Domain>[]>(() => [
|
||||
{
|
||||
id: 'domain',
|
||||
header: t('settings.domainsDns.table.domain'),
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => h('div', { class: 'flex items-center gap-2' }, [
|
||||
cell: ({ row, getValue }) => h('div', { class: 'flex items-center gap-2' }, [
|
||||
h(LinkIcon, { class: 'h-4 w-4 text-foreground/40' }),
|
||||
h('span', { class: 'text-sm font-medium text-foreground' }, row.original.name),
|
||||
]),
|
||||
@@ -39,8 +39,8 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
{
|
||||
id: 'addedAt',
|
||||
header: t('settings.domainsDns.table.addedDate'),
|
||||
accessorFn: row => row.addedAt,
|
||||
cell: ({ row }) => h('span', { class: 'text-sm text-foreground/60' }, row.original.addedAt),
|
||||
accessorFn: row => formatDate(row.createdAt),
|
||||
cell: ({ getValue }) => h('span', { class: 'text-sm text-foreground/60' }, getValue<string>()),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
@@ -58,10 +58,7 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
}, {
|
||||
icon: () => h(TrashIcon, { class: 'h-4 w-4 text-danger' }),
|
||||
}),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-right',
|
||||
},
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
@@ -73,7 +70,7 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
v-else
|
||||
:data="domains"
|
||||
:columns="columns"
|
||||
:get-row-id="(row) => row.id"
|
||||
:get-row-id="(row) => row.id!"
|
||||
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||
tableClass="w-full"
|
||||
headerRowClass="bg-muted/30"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { DomainApiItem, DomainItem } from './types';
|
||||
|
||||
export const normalizeDomainInput = (value: string) => value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -7,19 +5,19 @@ export const normalizeDomainInput = (value: string) => value
|
||||
.replace(/^www\./, '')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
export const formatDate = (value?: string) => {
|
||||
if (!value) return '-';
|
||||
// export const formatDate = (value?: string) => {
|
||||
// if (!value) return '-';
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value.split('T')[0] || value;
|
||||
}
|
||||
// const date = new Date(value);
|
||||
// if (Number.isNaN(date.getTime())) {
|
||||
// return value.split('T')[0] || value;
|
||||
// }
|
||||
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
// return date.toISOString().split('T')[0];
|
||||
// };
|
||||
|
||||
export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
|
||||
id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
|
||||
name: item.name || '',
|
||||
addedAt: formatDate(item.created_at),
|
||||
});
|
||||
// export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
|
||||
// id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
|
||||
// name: item.name || '',
|
||||
// addedAt: formatDate(item.created_at),
|
||||
// });
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export type DomainApiItem = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export type DomainItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
addedAt: string;
|
||||
};
|
||||
242
src/routes/settings/PopupAds/PopupAds.vue
Normal file
242
src/routes/settings/PopupAds/PopupAds.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import SettingsSectionCard from '../components/SettingsSectionCard.vue';
|
||||
import PopupAdsDialog from './components/PopupAdsDialog.vue';
|
||||
import PopupAdsTable from './components/PopupAdsTable';
|
||||
import PopupAdsToolbar from './components/PopupAdsToolbar.vue';
|
||||
import type { PopupAdFormData, PopupAdItem } from './types';
|
||||
import { useQuery } from '@pinia/colada';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
const toast = useAppToast();
|
||||
const confirm = useAppConfirm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showDialog = ref(false);
|
||||
const saving = ref(false);
|
||||
const deletingId = ref<string | null>(null);
|
||||
const togglingId = ref<string | null>(null);
|
||||
const editingItem = ref<PopupAdItem | null>(null);
|
||||
|
||||
const createInitialFormData = (): PopupAdFormData => ({
|
||||
type: 'url',
|
||||
label: '',
|
||||
value: '',
|
||||
isActive: true,
|
||||
maxTriggersPerSession: 3,
|
||||
});
|
||||
|
||||
const formData = ref<PopupAdFormData>(createInitialFormData());
|
||||
|
||||
const pageSizeOptions = [5, 10, 20, 50] as const;
|
||||
const page = ref(1);
|
||||
const limit = ref(10);
|
||||
|
||||
const { data: popupSnapshot, error, isPending, refetch } = useQuery({
|
||||
key: () => ['settings', 'popup-ads', page.value, limit.value],
|
||||
query: async () => {
|
||||
return await rpcClient.listPopupAds({ page: page.value, limit: limit.value });
|
||||
},
|
||||
});
|
||||
|
||||
const items = computed<PopupAdItem[]>(() => popupSnapshot.value?.items || []);
|
||||
const total = computed(() => popupSnapshot.value?.total || 0);
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / limit.value)));
|
||||
const hasPrev = computed(() => page.value > 1);
|
||||
const hasNext = computed(() => page.value < totalPages.value);
|
||||
const isMutating = computed(() => saving.value || deletingId.value !== null || togglingId.value !== null);
|
||||
const isInitialLoading = computed(() => isPending.value && !popupSnapshot.value);
|
||||
|
||||
const getErrorMessage = (value: any, fallback: string) => value?.error?.message || value?.message || value?.data?.message || fallback;
|
||||
const showActionErrorToast = (value: any) => {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.failedSummary'), detail: getErrorMessage(value, t('settings.popupAds.toast.failedDetail')), life: 5000 });
|
||||
};
|
||||
|
||||
watch(error, (value, previous) => {
|
||||
if (!value || value === previous || isMutating.value) return;
|
||||
showActionErrorToast(value);
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = createInitialFormData();
|
||||
editingItem.value = null;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
showDialog.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
resetForm();
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
const openEditDialog = (item: PopupAdItem) => {
|
||||
editingItem.value = item;
|
||||
formData.value = {
|
||||
type: (item.type as 'url' | 'script') || 'url',
|
||||
label: item.label || '',
|
||||
value: item.value || '',
|
||||
isActive: Boolean(item.isActive),
|
||||
maxTriggersPerSession: Number(item.maxTriggersPerSession || 3),
|
||||
};
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saving.value) return;
|
||||
|
||||
const label = formData.value.label.trim();
|
||||
const value = formData.value.value.trim();
|
||||
|
||||
if (!label) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.labelRequiredSummary'), detail: t('settings.popupAds.toast.labelRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.valueRequiredSummary'), detail: t('settings.popupAds.toast.valueRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (formData.value.type === 'url') {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.invalidUrlSummary'), detail: t('settings.popupAds.toast.invalidUrlDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (formData.value.type === 'url' && formData.value.maxTriggersPerSession < 1) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.maxTriggersRequiredSummary'), detail: t('settings.popupAds.toast.maxTriggersRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
type: formData.value.type,
|
||||
label,
|
||||
value,
|
||||
isActive: formData.value.isActive,
|
||||
maxTriggersPerSession: formData.value.type === 'url' ? formData.value.maxTriggersPerSession : undefined,
|
||||
};
|
||||
|
||||
if (editingItem.value?.id) {
|
||||
await rpcClient.updatePopupAd({ id: editingItem.value.id, ...payload });
|
||||
toast.add({ severity: 'success', summary: t('settings.popupAds.toast.updatedSummary'), detail: t('settings.popupAds.toast.updatedDetail'), life: 2500 });
|
||||
} else {
|
||||
await rpcClient.createPopupAd(payload);
|
||||
toast.add({ severity: 'success', summary: t('settings.popupAds.toast.createdSummary'), detail: t('settings.popupAds.toast.createdDetail'), life: 2500 });
|
||||
}
|
||||
|
||||
await refetch();
|
||||
closeDialog();
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (item: PopupAdItem) => {
|
||||
confirm.require({
|
||||
message: t('settings.popupAds.confirm.deleteMessage', { name: item.label || '' }),
|
||||
header: t('settings.popupAds.confirm.deleteHeader'),
|
||||
acceptLabel: t('settings.popupAds.confirm.deleteAccept'),
|
||||
rejectLabel: t('settings.popupAds.confirm.deleteReject'),
|
||||
accept: async () => {
|
||||
deletingId.value = item.id || null;
|
||||
try {
|
||||
await rpcClient.deletePopupAd({ id: item.id || '' });
|
||||
await refetch();
|
||||
toast.add({ severity: 'info', summary: t('settings.popupAds.toast.deletedSummary'), detail: t('settings.popupAds.toast.deletedDetail'), life: 2500 });
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
deletingId.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleActive = async ({ item, value }: { item: PopupAdItem; value: boolean }) => {
|
||||
togglingId.value = item.id || null;
|
||||
try {
|
||||
await rpcClient.updatePopupAd({
|
||||
id: item.id || '',
|
||||
type: item.type || 'url',
|
||||
label: item.label || '',
|
||||
value: item.value || '',
|
||||
isActive: value,
|
||||
maxTriggersPerSession: item.type === 'url' ? item.maxTriggersPerSession : undefined,
|
||||
});
|
||||
await refetch();
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
togglingId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const previousPage = () => {
|
||||
if (!hasPrev.value || isInitialLoading.value) return;
|
||||
page.value -= 1;
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (!hasNext.value || isInitialLoading.value) return;
|
||||
page.value += 1;
|
||||
};
|
||||
|
||||
const changePageSize = (value: number) => {
|
||||
const nextLimit = Number(value) || 10;
|
||||
if (nextLimit === limit.value) return;
|
||||
limit.value = nextLimit;
|
||||
page.value = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsSectionCard :title="t('settings.content.popupAds.title')" :description="t('settings.content.popupAds.subtitle')" bodyClass="">
|
||||
<template #header-actions>
|
||||
<PopupAdsToolbar :disabled="isInitialLoading || isMutating" @create="openCreateDialog" />
|
||||
</template>
|
||||
|
||||
<PopupAdsTable
|
||||
:items="items"
|
||||
:disabled="isMutating"
|
||||
:is-loading="isInitialLoading"
|
||||
:current-page="page"
|
||||
:total-pages="totalPages"
|
||||
:total-records="total"
|
||||
:rows-per-page="limit"
|
||||
:page-size-options="pageSizeOptions as unknown as number[]"
|
||||
:can-previous-page="hasPrev"
|
||||
:can-next-page="hasNext"
|
||||
@edit="openEditDialog"
|
||||
@delete="handleDelete"
|
||||
@toggle-active="handleToggleActive"
|
||||
@previous-page="previousPage"
|
||||
@next-page="nextPage"
|
||||
@page-size-change="changePageSize"
|
||||
/>
|
||||
|
||||
<div class="px-4 py-3 bg-header">
|
||||
<p class="text-xs leading-5 text-foreground/60">
|
||||
<strong class="text-foreground/80">{{ t('settings.popupAds.info.urlTitle') }}</strong>
|
||||
{{ t('settings.popupAds.info.urlDescription') }}
|
||||
<br>
|
||||
<strong class="text-foreground/80">{{ t('settings.popupAds.info.scriptTitle') }}</strong>
|
||||
{{ t('settings.popupAds.info.scriptDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PopupAdsDialog :visible="showDialog" :editing-item="editingItem" :form-data="formData" :saving="saving" @update:visible="showDialog = $event" @update:form-data="formData = $event" @save="handleSave" @close="closeDialog" />
|
||||
</SettingsSectionCard>
|
||||
</template>
|
||||
137
src/routes/settings/PopupAds/components/PopupAdsDialog.vue
Normal file
137
src/routes/settings/PopupAds/components/PopupAdsDialog.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed } from 'vue';
|
||||
import type { PopupAdFormData, PopupAdItem } from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
editingItem: PopupAdItem | null;
|
||||
formData: PopupAdFormData;
|
||||
saving: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'update:formData', value: PopupAdFormData): void;
|
||||
(e: 'save'): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = computed(() => props.editingItem
|
||||
? t('settings.popupAds.dialog.editTitle')
|
||||
: t('settings.popupAds.dialog.createTitle'));
|
||||
|
||||
const updateForm = (patch: Partial<PopupAdFormData>) => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
...patch,
|
||||
});
|
||||
};
|
||||
|
||||
const updateTextField = (key: 'label' | 'value', value: string | number | null) => {
|
||||
updateForm({
|
||||
[key]: typeof value === 'string' ? value : value == null ? '' : String(value),
|
||||
} as Partial<PopupAdFormData>);
|
||||
};
|
||||
|
||||
const updateNumberField = (value: string | number | null) => {
|
||||
const parsed = typeof value === 'number' ? value : Number(value ?? 0);
|
||||
updateForm({ maxTriggersPerSession: Number.isFinite(parsed) && parsed > 0 ? parsed : 1 });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
:visible="visible"
|
||||
:title="title"
|
||||
maxWidthClass="max-w-lg"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-type" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.type') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-type"
|
||||
as="select"
|
||||
:model-value="formData.type"
|
||||
@update:model-value="updateForm({ type: ($event as 'url' | 'script') || 'url' })"
|
||||
>
|
||||
<option value="url">{{ t('settings.popupAds.types.url') }}</option>
|
||||
<option value="script">{{ t('settings.popupAds.types.script') }}</option>
|
||||
</AppInput>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-label" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.label') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-label"
|
||||
:model-value="formData.label"
|
||||
:placeholder="t('settings.popupAds.dialog.labelPlaceholder')"
|
||||
@update:model-value="updateTextField('label', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-value" class="text-sm font-medium text-foreground">{{ t(formData.type === 'url' ? 'settings.popupAds.dialog.url' : 'settings.popupAds.dialog.script') }}</label>
|
||||
<AppInput
|
||||
v-if="formData.type === 'url'"
|
||||
id="popup-ad-value"
|
||||
:model-value="formData.value"
|
||||
:placeholder="t('settings.popupAds.dialog.urlPlaceholder')"
|
||||
@update:model-value="updateTextField('value', $event)"
|
||||
/>
|
||||
<AppInput
|
||||
v-else
|
||||
id="popup-ad-value"
|
||||
as="textarea"
|
||||
:rows="5"
|
||||
:model-value="formData.value"
|
||||
:placeholder="t('settings.popupAds.dialog.scriptPlaceholder')"
|
||||
inputClass="resize-y font-mono text-sm"
|
||||
@update:model-value="updateTextField('value', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.type === 'url'" class="grid gap-2">
|
||||
<label for="popup-ad-max-triggers" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.maxTriggersPerSession') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-max-triggers"
|
||||
type="number"
|
||||
min="1"
|
||||
:model-value="formData.maxTriggersPerSession"
|
||||
@update:model-value="updateNumberField($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-md border border-border bg-header/40 px-3 py-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.activeTitle') }}</p>
|
||||
<p class="mt-0.5 text-xs text-foreground/60">{{ t('settings.popupAds.dialog.activeDescription') }}</p>
|
||||
</div>
|
||||
<AppSwitch :model-value="formData.isActive" @update:model-value="updateForm({ isActive: $event })" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="secondary" size="sm" :disabled="saving" @click="emit('close')">
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton size="sm" :loading="saving" @click="emit('save')">
|
||||
<template #icon>
|
||||
<CheckIcon class="h-4 w-4" />
|
||||
</template>
|
||||
{{ editingItem ? t('settings.popupAds.dialog.update') : t('settings.popupAds.dialog.create') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
150
src/routes/settings/PopupAds/components/PopupAdsTable.tsx
Normal file
150
src/routes/settings/PopupAds/components/PopupAdsTable.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, defineComponent, type PropType } from 'vue';
|
||||
import type { PopupAdItem } from '../types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PopupAdsTable',
|
||||
props: {
|
||||
items: { type: Array as PropType<PopupAdItem[]>, required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
currentPage: { type: Number, default: 1 },
|
||||
totalPages: { type: Number, default: 1 },
|
||||
totalRecords: { type: Number, default: 0 },
|
||||
rowsPerPage: { type: Number, default: 10 },
|
||||
pageSizeOptions: { type: Array as PropType<number[]>, default: () => [] },
|
||||
canPreviousPage: { type: Boolean, default: false },
|
||||
canNextPage: { type: Boolean, default: false },
|
||||
},
|
||||
emits: {
|
||||
edit: (item: PopupAdItem) => true,
|
||||
delete: (item: PopupAdItem) => true,
|
||||
'toggle-active': (payload: { item: PopupAdItem; value: boolean }) => true,
|
||||
'previous-page': () => true,
|
||||
'next-page': () => true,
|
||||
'page-size-change': (value: number) => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = computed<ColumnDef<PopupAdItem>[]>(() => [
|
||||
{
|
||||
id: 'label',
|
||||
header: t('settings.popupAds.table.label'),
|
||||
accessorFn: (row) => row.label || '',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{row.original.label}</p>
|
||||
<p class="mt-0.5 text-xs text-foreground/50">#{row.original.id}</p>
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: t('settings.popupAds.table.type'),
|
||||
accessorFn: (row) => row.type || '',
|
||||
cell: ({ row }) => (
|
||||
<span class={[
|
||||
'inline-flex rounded-full px-2 py-1 text-xs font-medium uppercase',
|
||||
row.original.type === 'url' ? 'bg-emerald-500/10 text-emerald-600' : 'bg-indigo-500/10 text-indigo-600',
|
||||
]}>
|
||||
{t(`settings.popupAds.types.${row.original.type}`)}
|
||||
</span>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
header: t('settings.popupAds.table.target'),
|
||||
accessorFn: (row) => row.value || '',
|
||||
cell: ({ row }) => (
|
||||
<div class="max-w-[320px]">
|
||||
<code class={[
|
||||
'block truncate text-xs',
|
||||
row.original.type === 'script' ? 'font-mono text-foreground/60' : 'text-foreground/60',
|
||||
]}>
|
||||
{row.original.value}
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'maxTriggersPerSession',
|
||||
header: t('settings.popupAds.table.maxTriggersPerSession'),
|
||||
accessorFn: (row) => row.maxTriggersPerSession || 0,
|
||||
cell: ({ row }) => <span class="text-foreground/70">{row.original.type === 'url' ? row.original.maxTriggersPerSession || 0 : '—'}</span>,
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: t('common.status'),
|
||||
accessorFn: (row) => Number(Boolean(row.isActive)),
|
||||
cell: ({ row }) => (
|
||||
<div class="text-center">
|
||||
<AppSwitch
|
||||
modelValue={Boolean(row.original.isActive)}
|
||||
disabled={props.disabled}
|
||||
onUpdate:modelValue={(value: boolean) => emit('toggle-active', { item: row.original, value })}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: t('common.actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<AppButton variant="ghost" size="icon" disabled={props.disabled} onClick={() => emit('edit', row.original)} v-slots={{ icon: () => <PencilIcon filled class="h-4 w-4" /> }} />
|
||||
<AppButton variant="ghost" size="icon" disabled={props.disabled} onClick={() => emit('delete', row.original)} v-slots={{ icon: () => <TrashIcon filled class="h-4 w-4" /> }} />
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
]);
|
||||
|
||||
return () => (
|
||||
<BaseTable
|
||||
data={props.items}
|
||||
columns={columns.value}
|
||||
loading={props.isLoading}
|
||||
getRowId={(row: PopupAdItem) => String(row.id)}
|
||||
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||
tableClass="w-full"
|
||||
headerRowClass="bg-muted/30"
|
||||
bodyRowClass="border-b border-border hover:bg-muted/30"
|
||||
pagination
|
||||
currentPage={props.currentPage}
|
||||
totalPages={props.totalPages}
|
||||
totalRecords={props.totalRecords}
|
||||
rowsPerPage={props.rowsPerPage}
|
||||
pageSizeOptions={props.pageSizeOptions}
|
||||
canPreviousPage={props.canPreviousPage}
|
||||
canNextPage={props.canNextPage}
|
||||
onPrevious-page={() => emit('previous-page')}
|
||||
onNext-page={() => emit('next-page')}
|
||||
onPage-size-change={(value: number) => emit('page-size-change', value)}
|
||||
v-slots={{
|
||||
empty: () => (
|
||||
<div class="px-6 py-12 text-center">
|
||||
<LinkIcon class="mx-auto mb-3 block h-10 w-10 text-foreground/30" />
|
||||
<p class="mb-1 text-sm text-foreground/60">{t('settings.popupAds.emptyTitle')}</p>
|
||||
<p class="text-xs text-foreground/40">{t('settings.popupAds.emptySubtitle')}</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
24
src/routes/settings/PopupAds/components/PopupAdsToolbar.vue
Normal file
24
src/routes/settings/PopupAds/components/PopupAdsToolbar.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppButton size="sm" :disabled="disabled" @click="emit('create')">
|
||||
<template #icon>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
</template>
|
||||
{{ t('settings.popupAds.createItem') }}
|
||||
</AppButton>
|
||||
</template>
|
||||
17
src/routes/settings/PopupAds/types.ts
Normal file
17
src/routes/settings/PopupAds/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type { PopupAd } from '@/server/api/proto/app/v1/common';
|
||||
export type {
|
||||
CreatePopupAdRequest,
|
||||
DeletePopupAdRequest,
|
||||
UpdatePopupAdRequest,
|
||||
} from '@/server/api/proto/app/v1/catalog';
|
||||
|
||||
export type PopupAdType = 'url' | 'script';
|
||||
export type PopupAdItem = PopupAd;
|
||||
|
||||
export interface PopupAdFormData {
|
||||
type: PopupAdType;
|
||||
label: string;
|
||||
value: string;
|
||||
isActive: boolean;
|
||||
maxTriggersPerSession: number;
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import GlobeIcon from '@/components/icons/Globe.vue';
|
||||
import ShieldUser from '@/components/icons/shield-user.vue';
|
||||
import UserIcon from '@/components/icons/UserIcon.vue';
|
||||
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
|
||||
import Windows from '@/components/icons/windows.vue';
|
||||
import AppConfirmHost from '@/components/ui/AppConfirmHost.vue';
|
||||
import AppToastHost from '@/components/ui/AppToastHost.vue';
|
||||
import { isAdmin } from '@/lib/utils';
|
||||
@@ -119,6 +120,7 @@ const menuSections = computed<{ title: string; items: MenuItem[] }[]>(() => [
|
||||
items: [
|
||||
{ to: '/settings/domains', value: 'domains', label: t('settings.menu.domains'), icon: GlobeIcon },
|
||||
{ to: '/settings/ads', value: 'ads', label: t('settings.menu.ads'), icon: AdvertisementIcon },
|
||||
{ to: '/settings/popup-ads', value: 'popup-ads', label: t('settings.menu.popupAds'), icon: Windows },
|
||||
],
|
||||
},
|
||||
...(isAdmin(auth.user?.role) ? [{
|
||||
@@ -134,6 +136,7 @@ const menuSections = computed<{ title: string; items: MenuItem[] }[]>(() => [
|
||||
title: 'Admin Operations',
|
||||
items: [
|
||||
{ to: '/settings/admin/ad-templates', value: 'admin-ad-templates', label: 'Ad Templates', description: 'VAST templates and defaults' },
|
||||
{ to: '/settings/admin/popup-ads', value: 'admin-popup-ads', label: 'Popup Ads', description: 'Popup campaigns, timing and cooldowns' },
|
||||
{ to: '/settings/admin/player-configs', value: 'admin-player-configs', label: 'Player Configs', description: 'Cross-user player presets and defaults' },
|
||||
{ to: '/settings/admin/jobs', value: 'admin-jobs', label: 'Jobs', description: 'Queue, retries and live logs' },
|
||||
{ to: '/settings/admin/agents', value: 'admin-agents', label: 'Agents', description: 'Workers, health and maintenance' },
|
||||
@@ -193,6 +196,10 @@ const content = computed(() => ({
|
||||
title: t('settings.content.ads.title'),
|
||||
subtitle: t('settings.content.ads.subtitle')
|
||||
},
|
||||
'settings-popup-ads': {
|
||||
title: t('settings.content.popupAds.title'),
|
||||
subtitle: t('settings.content.popupAds.subtitle')
|
||||
},
|
||||
'settings-player-configs': {
|
||||
title: t('settings.content.playerConfigs.title'),
|
||||
subtitle: t('settings.content.playerConfigs.subtitle')
|
||||
@@ -225,6 +232,10 @@ const content = computed(() => ({
|
||||
title: 'Ad Templates',
|
||||
subtitle: 'VAST templates, ownership metadata and default assignments.',
|
||||
},
|
||||
'admin-popup-ads': {
|
||||
title: 'Popup Ads',
|
||||
subtitle: 'Popup campaigns, timing windows and cooldown controls across users.',
|
||||
},
|
||||
'admin-player-configs': {
|
||||
title: 'Player Configs',
|
||||
subtitle: 'Cross-user player presets, flags and default assignments.',
|
||||
|
||||
306
src/routes/settings/admin/PopupAds.vue
Normal file
306
src/routes/settings/admin/PopupAds.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AdminInput from './components/AdminInput.vue';
|
||||
import AdminSelect from './components/AdminSelect.vue';
|
||||
import AdminTable from './components/AdminTable.vue';
|
||||
import AdminSectionCard from './components/AdminSectionCard.vue';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import AdminMetricCard from './components/AdminMetricCard.vue';
|
||||
import AdminPlaceholderTable from './components/AdminPlaceholderTable.vue';
|
||||
import AdminSectionShell from './components/AdminSectionShell.vue';
|
||||
import { useAdminPageHeader } from './components/useAdminPageHeader';
|
||||
|
||||
type ListPopupAdsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPopupAds>>;
|
||||
type AdminPopupAdRow = NonNullable<ListPopupAdsResponse['items']>[number];
|
||||
|
||||
const popupTypes = ['url', 'script'] as const;
|
||||
const loading = ref(true);
|
||||
const submitting = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const actionError = ref<string | null>(null);
|
||||
const rows = ref<AdminPopupAdRow[]>([]);
|
||||
const total = ref(0);
|
||||
const limit = ref(12);
|
||||
const page = ref(1);
|
||||
const search = ref('');
|
||||
const appliedSearch = ref('');
|
||||
const ownerFilter = ref('');
|
||||
const appliedOwnerFilter = ref('');
|
||||
const selectedRow = ref<AdminPopupAdRow | null>(null);
|
||||
const createOpen = ref(false);
|
||||
const editOpen = ref(false);
|
||||
const deleteOpen = ref(false);
|
||||
|
||||
const createForm = reactive({ userId: '', type: 'url', label: '', value: '', isActive: true, maxTriggersPerSession: 3 });
|
||||
const editForm = reactive({ id: '', userId: '', type: 'url', label: '', value: '', isActive: true, maxTriggersPerSession: 3 });
|
||||
|
||||
const canCreate = computed(() => createForm.userId.trim() && createForm.label.trim() && createForm.value.trim());
|
||||
const canUpdate = computed(() => editForm.id.trim() && editForm.userId.trim() && editForm.label.trim() && editForm.value.trim());
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / limit.value)));
|
||||
const summary = computed(() => [
|
||||
{ label: 'Visible records', value: rows.value.length },
|
||||
{ label: 'Active', value: rows.value.filter((row) => row.isActive).length },
|
||||
{ label: 'URL type', value: rows.value.filter((row) => row.type === 'url').length },
|
||||
{ label: 'Script type', value: rows.value.filter((row) => row.type === 'script').length },
|
||||
]);
|
||||
|
||||
const loadPopupAds = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await rpcClient.listAdminPopupAds({ page: page.value, limit: limit.value, userId: appliedOwnerFilter.value.trim() || undefined, search: appliedSearch.value.trim() || undefined });
|
||||
rows.value = response.items ?? [];
|
||||
total.value = response.total ?? rows.value.length;
|
||||
limit.value = response.limit ?? limit.value;
|
||||
page.value = response.page ?? page.value;
|
||||
} catch (err: any) {
|
||||
error.value = err?.message || 'Failed to load admin popup ads';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = async () => {
|
||||
page.value = 1;
|
||||
appliedSearch.value = search.value;
|
||||
appliedOwnerFilter.value = ownerFilter.value;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const resetCreateForm = () => {
|
||||
createForm.userId = '';
|
||||
createForm.type = 'url';
|
||||
createForm.label = '';
|
||||
createForm.value = '';
|
||||
createForm.isActive = true;
|
||||
createForm.maxTriggersPerSession = 3;
|
||||
};
|
||||
|
||||
const openEditDialog = (row: AdminPopupAdRow) => {
|
||||
selectedRow.value = row;
|
||||
actionError.value = null;
|
||||
editForm.id = row.id || '';
|
||||
editForm.userId = row.userId || '';
|
||||
editForm.type = (row.type as 'url' | 'script') || 'url';
|
||||
editForm.label = row.label || '';
|
||||
editForm.value = row.value || '';
|
||||
editForm.isActive = !!row.isActive;
|
||||
editForm.maxTriggersPerSession = Number(row.maxTriggersPerSession || 3);
|
||||
editOpen.value = true;
|
||||
};
|
||||
|
||||
const openDeleteDialog = (row: AdminPopupAdRow) => {
|
||||
selectedRow.value = row;
|
||||
actionError.value = null;
|
||||
deleteOpen.value = true;
|
||||
};
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!canCreate.value) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.createAdminPopupAd({
|
||||
userId: createForm.userId.trim(),
|
||||
type: createForm.type,
|
||||
label: createForm.label.trim(),
|
||||
value: createForm.value.trim(),
|
||||
isActive: createForm.isActive,
|
||||
maxTriggersPerSession: createForm.type === 'url' ? createForm.maxTriggersPerSession : undefined,
|
||||
});
|
||||
resetCreateForm();
|
||||
createOpen.value = false;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to create popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitEdit = async () => {
|
||||
if (!canUpdate.value) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.updateAdminPopupAd({
|
||||
id: editForm.id,
|
||||
userId: editForm.userId.trim(),
|
||||
type: editForm.type,
|
||||
label: editForm.label.trim(),
|
||||
value: editForm.value.trim(),
|
||||
isActive: editForm.isActive,
|
||||
maxTriggersPerSession: editForm.type === 'url' ? editForm.maxTriggersPerSession : undefined,
|
||||
});
|
||||
editOpen.value = false;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to update popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitDelete = async () => {
|
||||
if (!selectedRow.value?.id) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.deleteAdminPopupAd({ id: selectedRow.value.id });
|
||||
deleteOpen.value = false;
|
||||
selectedRow.value = null;
|
||||
if (page.value > 1 && rows.value.length === 1) page.value -= 1;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to delete popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const previousPage = async () => {
|
||||
if (page.value <= 1) return;
|
||||
page.value -= 1;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const nextPage = async () => {
|
||||
if (page.value >= totalPages.value) return;
|
||||
page.value += 1;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const columns = computed<ColumnDef<AdminPopupAdRow>[]>(() => [
|
||||
{
|
||||
id: 'label',
|
||||
header: 'Popup',
|
||||
accessorFn: row => row.label || '',
|
||||
cell: ({ row }) => h('div', { class: 'text-left' }, [
|
||||
h('div', { class: 'font-medium text-foreground' }, row.original.label),
|
||||
h('div', { class: 'mt-1 text-xs text-foreground/60' }, row.original.ownerEmail || row.original.userId || 'No owner'),
|
||||
]),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
accessorFn: row => row.type || '',
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
header: 'Value',
|
||||
accessorFn: row => row.value || '',
|
||||
cell: ({ row }) => h('code', { class: 'block max-w-[360px] truncate text-xs text-foreground/60' }, row.original.value || '—'),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'maxTriggersPerSession',
|
||||
header: 'Max triggers/session',
|
||||
accessorFn: row => row.maxTriggersPerSession || 0,
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorFn: row => row.isActive ? 'ACTIVE' : 'INACTIVE',
|
||||
cell: ({ row }) => h('span', { class: 'text-foreground/70' }, row.original.isActive ? 'ACTIVE' : 'INACTIVE'),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => h('div', { class: 'flex justify-end gap-2' }, [
|
||||
h(AppButton, { size: 'sm', variant: 'secondary', onClick: () => openEditDialog(row.original) }, { default: () => 'Edit' }),
|
||||
h(AppButton, { size: 'sm', variant: 'danger', onClick: () => openDeleteDialog(row.original) }, { default: () => 'Delete' }),
|
||||
]),
|
||||
meta: { headerClass: 'px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-right' },
|
||||
},
|
||||
]);
|
||||
|
||||
useAdminPageHeader(() => ({
|
||||
eyebrow: 'Advertising',
|
||||
badge: loading.value ? 'Syncing popup inventory' : `${total.value} total popups`,
|
||||
actions: [
|
||||
{ label: 'Refresh', variant: 'secondary', loading: loading.value, onClick: loadPopupAds },
|
||||
{ label: 'Create popup', onClick: () => { actionError.value = null; createOpen.value = true; } },
|
||||
],
|
||||
}));
|
||||
|
||||
onMounted(loadPopupAds);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminSectionShell>
|
||||
<template #stats>
|
||||
<AdminMetricCard v-for="item in summary" :key="item.label" :label="item.label" :value="item.value" />
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<AdminSectionCard title="Filters" description="Search popup ads by label and narrow by owner reference if needed." bodyClass="p-5">
|
||||
<div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_220px_auto] xl:items-end">
|
||||
<div class="space-y-2"><label class="text-xs font-medium text-foreground/60">Search</label><AdminInput v-model="search" placeholder="Search popup label" @enter="applyFilters" /></div>
|
||||
<div class="space-y-2"><label class="text-xs font-medium text-foreground/60">Owner reference</label><AdminInput v-model="ownerFilter" placeholder="Optional owner reference" @enter="applyFilters" /></div>
|
||||
<div class="flex items-center gap-2 xl:justify-end"><AppButton size="sm" variant="ghost" @click="search = ''; ownerFilter = ''; appliedSearch = ''; appliedOwnerFilter = ''; loadPopupAds()">Reset</AppButton><AppButton size="sm" variant="secondary" @click="applyFilters">Apply</AppButton></div>
|
||||
</div>
|
||||
</AdminSectionCard>
|
||||
|
||||
<div v-if="error" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ error }}</div>
|
||||
|
||||
<AdminSectionCard v-else title="Popup Ads" description="Popup ad inventory across users." bodyClass="">
|
||||
<AdminPlaceholderTable v-if="loading" :columns="6" :rows="4" />
|
||||
<AdminTable v-else :data="rows" :columns="columns" :get-row-id="(row) => row.id || row.label || ''" wrapperClass="border-x-0 border-t-0 rounded-none bg-transparent" tableClass="w-full" headerRowClass="bg-muted/30" bodyRowClass="border-b border-border hover:bg-muted/30">
|
||||
<template #empty>
|
||||
<div class="px-6 py-12 text-center"><p class="mb-1 text-sm text-foreground/60">No popup ads matched the current filters.</p><p class="text-xs text-foreground/40">Try a broader label or clear the owner filter.</p></div>
|
||||
</template>
|
||||
</AdminTable>
|
||||
<div class="flex flex-col gap-3 border-t border-border bg-muted/20 px-6 py-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="text-xs text-foreground/55">Page {{ page }} of {{ totalPages }} · {{ total }} records</div>
|
||||
<div class="flex items-center gap-2"><AppButton size="sm" variant="secondary" :disabled="page <= 1 || loading" @click="previousPage">Previous</AppButton><AppButton size="sm" variant="secondary" :disabled="page >= totalPages || loading" @click="nextPage">Next</AppButton></div>
|
||||
</div>
|
||||
</AdminSectionCard>
|
||||
</div>
|
||||
</AdminSectionShell>
|
||||
|
||||
<AppDialog v-model:visible="createOpen" title="Create popup ad" maxWidthClass="max-w-2xl" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Owner user ID</label><AdminInput v-model="createForm.userId" placeholder="user-id" /></div>
|
||||
<div class="space-y-2"><label class="text-sm font-medium text-foreground/70">Type</label><AdminSelect v-model="createForm.type"><option v-for="type in popupTypes" :key="type" :value="type">{{ type }}</option></AdminSelect></div>
|
||||
<div v-if="createForm.type === 'url'" class="space-y-2"><label class="text-sm font-medium text-foreground/70">Max triggers / session</label><AdminInput v-model="createForm.maxTriggersPerSession" type="number" min="1" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Label</label><AdminInput v-model="createForm.label" placeholder="Homepage campaign" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Value</label><AdminInput v-if="createForm.type === 'url'" v-model="createForm.value" placeholder="https://..." /><textarea v-else v-model="createForm.value" rows="4" class="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm" placeholder="<script async src='//example.com/ad.js'></script>" /></div>
|
||||
<label class="flex items-center gap-2 text-sm text-foreground/70"><input v-model="createForm.isActive" type="checkbox" class="h-4 w-4" />Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="createOpen = false">Cancel</AppButton><AppButton size="sm" :loading="submitting" :disabled="!canCreate" @click="submitCreate">Create</AppButton></div></template>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model:visible="editOpen" title="Edit popup ad" maxWidthClass="max-w-2xl" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Owner user ID</label><AdminInput v-model="editForm.userId" /></div>
|
||||
<div class="space-y-2"><label class="text-sm font-medium text-foreground/70">Type</label><AdminSelect v-model="editForm.type"><option v-for="type in popupTypes" :key="type" :value="type">{{ type }}</option></AdminSelect></div>
|
||||
<div v-if="editForm.type === 'url'" class="space-y-2"><label class="text-sm font-medium text-foreground/70">Max triggers / session</label><AdminInput v-model="editForm.maxTriggersPerSession" type="number" min="1" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Label</label><AdminInput v-model="editForm.label" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Value</label><AdminInput v-if="editForm.type === 'url'" v-model="editForm.value" /><textarea v-else v-model="editForm.value" rows="4" class="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm" /></div>
|
||||
<label class="flex items-center gap-2 text-sm text-foreground/70"><input v-model="editForm.isActive" type="checkbox" class="h-4 w-4" />Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="editOpen = false">Cancel</AppButton><AppButton size="sm" :loading="submitting" :disabled="!canUpdate" @click="submitEdit">Save</AppButton></div></template>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model:visible="deleteOpen" title="Delete popup ad" maxWidthClass="max-w-md" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<p class="text-sm text-foreground/70">Delete popup ad <span class="font-medium">{{ selectedRow?.label || 'this popup' }}</span>.</p>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="deleteOpen = false">Cancel</AppButton><AppButton variant="danger" size="sm" :loading="submitting" @click="submitDelete">Delete</AppButton></div></template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ import {
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import { AdTemplate, Domain, MessageResponse, Plan, PlayerConfig } from "./common";
|
||||
import { AdTemplate, Domain, MessageResponse, Plan, PlayerConfig, PopupAd } from "./common";
|
||||
|
||||
export const protobufPackage = "stream.app.v1";
|
||||
|
||||
@@ -81,6 +81,54 @@ export interface DeleteAdTemplateRequest {
|
||||
id?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ListPopupAdsRequest {
|
||||
page?: number | undefined;
|
||||
limit?: number | undefined;
|
||||
}
|
||||
|
||||
export interface ListPopupAdsResponse {
|
||||
items?: PopupAd[] | undefined;
|
||||
total?: number | undefined;
|
||||
page?: number | undefined;
|
||||
limit?: number | undefined;
|
||||
}
|
||||
|
||||
export interface CreatePopupAdRequest {
|
||||
type?: string | undefined;
|
||||
label?: string | undefined;
|
||||
value?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
maxTriggersPerSession?: number | undefined;
|
||||
}
|
||||
|
||||
export interface CreatePopupAdResponse {
|
||||
item?: PopupAd | undefined;
|
||||
}
|
||||
|
||||
export interface UpdatePopupAdRequest {
|
||||
id?: string | undefined;
|
||||
type?: string | undefined;
|
||||
label?: string | undefined;
|
||||
value?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
maxTriggersPerSession?: number | undefined;
|
||||
}
|
||||
|
||||
export interface UpdatePopupAdResponse {
|
||||
item?: PopupAd | undefined;
|
||||
}
|
||||
|
||||
export interface DeletePopupAdRequest {
|
||||
id?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GetActivePopupAdRequest {
|
||||
}
|
||||
|
||||
export interface GetActivePopupAdResponse {
|
||||
item?: PopupAd | undefined;
|
||||
}
|
||||
|
||||
export interface ListPlayerConfigsRequest {
|
||||
}
|
||||
|
||||
@@ -1089,6 +1137,750 @@ export const DeleteAdTemplateRequest: MessageFns<DeleteAdTemplateRequest> = {
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseListPopupAdsRequest(): ListPopupAdsRequest {
|
||||
return { page: 0, limit: 0 };
|
||||
}
|
||||
|
||||
export const ListPopupAdsRequest: MessageFns<ListPopupAdsRequest> = {
|
||||
encode(message: ListPopupAdsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.page !== undefined && message.page !== 0) {
|
||||
writer.uint32(8).int32(message.page);
|
||||
}
|
||||
if (message.limit !== undefined && message.limit !== 0) {
|
||||
writer.uint32(16).int32(message.limit);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ListPopupAdsRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseListPopupAdsRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.page = reader.int32();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.limit = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ListPopupAdsRequest {
|
||||
return {
|
||||
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
|
||||
limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ListPopupAdsRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.page !== undefined && message.page !== 0) {
|
||||
obj.page = Math.round(message.page);
|
||||
}
|
||||
if (message.limit !== undefined && message.limit !== 0) {
|
||||
obj.limit = Math.round(message.limit);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ListPopupAdsRequest>, I>>(base?: I): ListPopupAdsRequest {
|
||||
return ListPopupAdsRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ListPopupAdsRequest>, I>>(object: I): ListPopupAdsRequest {
|
||||
const message = createBaseListPopupAdsRequest();
|
||||
message.page = object.page ?? 0;
|
||||
message.limit = object.limit ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseListPopupAdsResponse(): ListPopupAdsResponse {
|
||||
return { items: [], total: 0, page: 0, limit: 0 };
|
||||
}
|
||||
|
||||
export const ListPopupAdsResponse: MessageFns<ListPopupAdsResponse> = {
|
||||
encode(message: ListPopupAdsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.items !== undefined && message.items.length !== 0) {
|
||||
for (const v of message.items) {
|
||||
PopupAd.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
}
|
||||
if (message.total !== undefined && message.total !== 0) {
|
||||
writer.uint32(16).int64(message.total);
|
||||
}
|
||||
if (message.page !== undefined && message.page !== 0) {
|
||||
writer.uint32(24).int32(message.page);
|
||||
}
|
||||
if (message.limit !== undefined && message.limit !== 0) {
|
||||
writer.uint32(32).int32(message.limit);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ListPopupAdsResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseListPopupAdsResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
const el = PopupAd.decode(reader, reader.uint32());
|
||||
if (el !== undefined) {
|
||||
message.items!.push(el);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.total = longToNumber(reader.int64());
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 24) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.page = reader.int32();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.limit = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ListPopupAdsResponse {
|
||||
return {
|
||||
items: globalThis.Array.isArray(object?.items) ? object.items.map((e: any) => PopupAd.fromJSON(e)) : [],
|
||||
total: isSet(object.total) ? globalThis.Number(object.total) : 0,
|
||||
page: isSet(object.page) ? globalThis.Number(object.page) : 0,
|
||||
limit: isSet(object.limit) ? globalThis.Number(object.limit) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ListPopupAdsResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.items?.length) {
|
||||
obj.items = message.items.map((e) => PopupAd.toJSON(e));
|
||||
}
|
||||
if (message.total !== undefined && message.total !== 0) {
|
||||
obj.total = Math.round(message.total);
|
||||
}
|
||||
if (message.page !== undefined && message.page !== 0) {
|
||||
obj.page = Math.round(message.page);
|
||||
}
|
||||
if (message.limit !== undefined && message.limit !== 0) {
|
||||
obj.limit = Math.round(message.limit);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ListPopupAdsResponse>, I>>(base?: I): ListPopupAdsResponse {
|
||||
return ListPopupAdsResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<ListPopupAdsResponse>, I>>(object: I): ListPopupAdsResponse {
|
||||
const message = createBaseListPopupAdsResponse();
|
||||
message.items = object.items?.map((e) => PopupAd.fromPartial(e)) || [];
|
||||
message.total = object.total ?? 0;
|
||||
message.page = object.page ?? 0;
|
||||
message.limit = object.limit ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseCreatePopupAdRequest(): CreatePopupAdRequest {
|
||||
return { type: "", label: "", value: "", isActive: undefined, maxTriggersPerSession: undefined };
|
||||
}
|
||||
|
||||
export const CreatePopupAdRequest: MessageFns<CreatePopupAdRequest> = {
|
||||
encode(message: CreatePopupAdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
writer.uint32(10).string(message.type);
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
writer.uint32(18).string(message.label);
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
writer.uint32(26).string(message.value);
|
||||
}
|
||||
if (message.isActive !== undefined) {
|
||||
writer.uint32(32).bool(message.isActive);
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined) {
|
||||
writer.uint32(40).int32(message.maxTriggersPerSession);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): CreatePopupAdRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseCreatePopupAdRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.type = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.label = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.isActive = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.maxTriggersPerSession = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): CreatePopupAdRequest {
|
||||
return {
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
label: isSet(object.label) ? globalThis.String(object.label) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
isActive: isSet(object.isActive)
|
||||
? globalThis.Boolean(object.isActive)
|
||||
: isSet(object.is_active)
|
||||
? globalThis.Boolean(object.is_active)
|
||||
: undefined,
|
||||
maxTriggersPerSession: isSet(object.maxTriggersPerSession)
|
||||
? globalThis.Number(object.maxTriggersPerSession)
|
||||
: isSet(object.max_triggers_per_session)
|
||||
? globalThis.Number(object.max_triggers_per_session)
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: CreatePopupAdRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
obj.type = message.type;
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
obj.label = message.label;
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
if (message.isActive !== undefined) {
|
||||
obj.isActive = message.isActive;
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined) {
|
||||
obj.maxTriggersPerSession = Math.round(message.maxTriggersPerSession);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<CreatePopupAdRequest>, I>>(base?: I): CreatePopupAdRequest {
|
||||
return CreatePopupAdRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<CreatePopupAdRequest>, I>>(object: I): CreatePopupAdRequest {
|
||||
const message = createBaseCreatePopupAdRequest();
|
||||
message.type = object.type ?? "";
|
||||
message.label = object.label ?? "";
|
||||
message.value = object.value ?? "";
|
||||
message.isActive = object.isActive ?? undefined;
|
||||
message.maxTriggersPerSession = object.maxTriggersPerSession ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseCreatePopupAdResponse(): CreatePopupAdResponse {
|
||||
return { item: undefined };
|
||||
}
|
||||
|
||||
export const CreatePopupAdResponse: MessageFns<CreatePopupAdResponse> = {
|
||||
encode(message: CreatePopupAdResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.item !== undefined) {
|
||||
PopupAd.encode(message.item, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): CreatePopupAdResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseCreatePopupAdResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.item = PopupAd.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): CreatePopupAdResponse {
|
||||
return { item: isSet(object.item) ? PopupAd.fromJSON(object.item) : undefined };
|
||||
},
|
||||
|
||||
toJSON(message: CreatePopupAdResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.item !== undefined) {
|
||||
obj.item = PopupAd.toJSON(message.item);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<CreatePopupAdResponse>, I>>(base?: I): CreatePopupAdResponse {
|
||||
return CreatePopupAdResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<CreatePopupAdResponse>, I>>(object: I): CreatePopupAdResponse {
|
||||
const message = createBaseCreatePopupAdResponse();
|
||||
message.item = (object.item !== undefined && object.item !== null) ? PopupAd.fromPartial(object.item) : undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseUpdatePopupAdRequest(): UpdatePopupAdRequest {
|
||||
return { id: "", type: "", label: "", value: "", isActive: undefined, maxTriggersPerSession: undefined };
|
||||
}
|
||||
|
||||
export const UpdatePopupAdRequest: MessageFns<UpdatePopupAdRequest> = {
|
||||
encode(message: UpdatePopupAdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
writer.uint32(18).string(message.type);
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
writer.uint32(26).string(message.label);
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
writer.uint32(34).string(message.value);
|
||||
}
|
||||
if (message.isActive !== undefined) {
|
||||
writer.uint32(40).bool(message.isActive);
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined) {
|
||||
writer.uint32(48).int32(message.maxTriggersPerSession);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): UpdatePopupAdRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseUpdatePopupAdRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.type = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.label = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.isActive = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.maxTriggersPerSession = reader.int32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): UpdatePopupAdRequest {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
label: isSet(object.label) ? globalThis.String(object.label) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
isActive: isSet(object.isActive)
|
||||
? globalThis.Boolean(object.isActive)
|
||||
: isSet(object.is_active)
|
||||
? globalThis.Boolean(object.is_active)
|
||||
: undefined,
|
||||
maxTriggersPerSession: isSet(object.maxTriggersPerSession)
|
||||
? globalThis.Number(object.maxTriggersPerSession)
|
||||
: isSet(object.max_triggers_per_session)
|
||||
? globalThis.Number(object.max_triggers_per_session)
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: UpdatePopupAdRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
obj.type = message.type;
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
obj.label = message.label;
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
if (message.isActive !== undefined) {
|
||||
obj.isActive = message.isActive;
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined) {
|
||||
obj.maxTriggersPerSession = Math.round(message.maxTriggersPerSession);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<UpdatePopupAdRequest>, I>>(base?: I): UpdatePopupAdRequest {
|
||||
return UpdatePopupAdRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<UpdatePopupAdRequest>, I>>(object: I): UpdatePopupAdRequest {
|
||||
const message = createBaseUpdatePopupAdRequest();
|
||||
message.id = object.id ?? "";
|
||||
message.type = object.type ?? "";
|
||||
message.label = object.label ?? "";
|
||||
message.value = object.value ?? "";
|
||||
message.isActive = object.isActive ?? undefined;
|
||||
message.maxTriggersPerSession = object.maxTriggersPerSession ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseUpdatePopupAdResponse(): UpdatePopupAdResponse {
|
||||
return { item: undefined };
|
||||
}
|
||||
|
||||
export const UpdatePopupAdResponse: MessageFns<UpdatePopupAdResponse> = {
|
||||
encode(message: UpdatePopupAdResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.item !== undefined) {
|
||||
PopupAd.encode(message.item, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): UpdatePopupAdResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseUpdatePopupAdResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.item = PopupAd.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): UpdatePopupAdResponse {
|
||||
return { item: isSet(object.item) ? PopupAd.fromJSON(object.item) : undefined };
|
||||
},
|
||||
|
||||
toJSON(message: UpdatePopupAdResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.item !== undefined) {
|
||||
obj.item = PopupAd.toJSON(message.item);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<UpdatePopupAdResponse>, I>>(base?: I): UpdatePopupAdResponse {
|
||||
return UpdatePopupAdResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<UpdatePopupAdResponse>, I>>(object: I): UpdatePopupAdResponse {
|
||||
const message = createBaseUpdatePopupAdResponse();
|
||||
message.item = (object.item !== undefined && object.item !== null) ? PopupAd.fromPartial(object.item) : undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseDeletePopupAdRequest(): DeletePopupAdRequest {
|
||||
return { id: "" };
|
||||
}
|
||||
|
||||
export const DeletePopupAdRequest: MessageFns<DeletePopupAdRequest> = {
|
||||
encode(message: DeletePopupAdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeletePopupAdRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeletePopupAdRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeletePopupAdRequest {
|
||||
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: DeletePopupAdRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeletePopupAdRequest>, I>>(base?: I): DeletePopupAdRequest {
|
||||
return DeletePopupAdRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<DeletePopupAdRequest>, I>>(object: I): DeletePopupAdRequest {
|
||||
const message = createBaseDeletePopupAdRequest();
|
||||
message.id = object.id ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseGetActivePopupAdRequest(): GetActivePopupAdRequest {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const GetActivePopupAdRequest: MessageFns<GetActivePopupAdRequest> = {
|
||||
encode(_: GetActivePopupAdRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): GetActivePopupAdRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseGetActivePopupAdRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(_: any): GetActivePopupAdRequest {
|
||||
return {};
|
||||
},
|
||||
|
||||
toJSON(_: GetActivePopupAdRequest): unknown {
|
||||
const obj: any = {};
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<GetActivePopupAdRequest>, I>>(base?: I): GetActivePopupAdRequest {
|
||||
return GetActivePopupAdRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<GetActivePopupAdRequest>, I>>(_: I): GetActivePopupAdRequest {
|
||||
const message = createBaseGetActivePopupAdRequest();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseGetActivePopupAdResponse(): GetActivePopupAdResponse {
|
||||
return { item: undefined };
|
||||
}
|
||||
|
||||
export const GetActivePopupAdResponse: MessageFns<GetActivePopupAdResponse> = {
|
||||
encode(message: GetActivePopupAdResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.item !== undefined) {
|
||||
PopupAd.encode(message.item, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): GetActivePopupAdResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseGetActivePopupAdResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.item = PopupAd.decode(reader, reader.uint32());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): GetActivePopupAdResponse {
|
||||
return { item: isSet(object.item) ? PopupAd.fromJSON(object.item) : undefined };
|
||||
},
|
||||
|
||||
toJSON(message: GetActivePopupAdResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.item !== undefined) {
|
||||
obj.item = PopupAd.toJSON(message.item);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<GetActivePopupAdResponse>, I>>(base?: I): GetActivePopupAdResponse {
|
||||
return GetActivePopupAdResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<GetActivePopupAdResponse>, I>>(object: I): GetActivePopupAdResponse {
|
||||
const message = createBaseGetActivePopupAdResponse();
|
||||
message.item = (object.item !== undefined && object.item !== null) ? PopupAd.fromPartial(object.item) : undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseListPlayerConfigsRequest(): ListPlayerConfigsRequest {
|
||||
return {};
|
||||
}
|
||||
@@ -2290,6 +3082,152 @@ export const AdTemplatesClient = makeGenericClientConstructor(
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
export type PopupAdsService = typeof PopupAdsService;
|
||||
export const PopupAdsService = {
|
||||
listPopupAds: {
|
||||
path: "/stream.app.v1.PopupAds/ListPopupAds",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: ListPopupAdsRequest): Buffer => Buffer.from(ListPopupAdsRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): ListPopupAdsRequest => ListPopupAdsRequest.decode(value),
|
||||
responseSerialize: (value: ListPopupAdsResponse): Buffer =>
|
||||
Buffer.from(ListPopupAdsResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): ListPopupAdsResponse => ListPopupAdsResponse.decode(value),
|
||||
},
|
||||
createPopupAd: {
|
||||
path: "/stream.app.v1.PopupAds/CreatePopupAd",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: CreatePopupAdRequest): Buffer => Buffer.from(CreatePopupAdRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): CreatePopupAdRequest => CreatePopupAdRequest.decode(value),
|
||||
responseSerialize: (value: CreatePopupAdResponse): Buffer =>
|
||||
Buffer.from(CreatePopupAdResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): CreatePopupAdResponse => CreatePopupAdResponse.decode(value),
|
||||
},
|
||||
updatePopupAd: {
|
||||
path: "/stream.app.v1.PopupAds/UpdatePopupAd",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: UpdatePopupAdRequest): Buffer => Buffer.from(UpdatePopupAdRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): UpdatePopupAdRequest => UpdatePopupAdRequest.decode(value),
|
||||
responseSerialize: (value: UpdatePopupAdResponse): Buffer =>
|
||||
Buffer.from(UpdatePopupAdResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): UpdatePopupAdResponse => UpdatePopupAdResponse.decode(value),
|
||||
},
|
||||
deletePopupAd: {
|
||||
path: "/stream.app.v1.PopupAds/DeletePopupAd",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: DeletePopupAdRequest): Buffer => Buffer.from(DeletePopupAdRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): DeletePopupAdRequest => DeletePopupAdRequest.decode(value),
|
||||
responseSerialize: (value: MessageResponse): Buffer => Buffer.from(MessageResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): MessageResponse => MessageResponse.decode(value),
|
||||
},
|
||||
getActivePopupAd: {
|
||||
path: "/stream.app.v1.PopupAds/GetActivePopupAd",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (value: GetActivePopupAdRequest): Buffer =>
|
||||
Buffer.from(GetActivePopupAdRequest.encode(value).finish()),
|
||||
requestDeserialize: (value: Buffer): GetActivePopupAdRequest => GetActivePopupAdRequest.decode(value),
|
||||
responseSerialize: (value: GetActivePopupAdResponse): Buffer =>
|
||||
Buffer.from(GetActivePopupAdResponse.encode(value).finish()),
|
||||
responseDeserialize: (value: Buffer): GetActivePopupAdResponse => GetActivePopupAdResponse.decode(value),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface PopupAdsServer extends UntypedServiceImplementation {
|
||||
listPopupAds: handleUnaryCall<ListPopupAdsRequest, ListPopupAdsResponse>;
|
||||
createPopupAd: handleUnaryCall<CreatePopupAdRequest, CreatePopupAdResponse>;
|
||||
updatePopupAd: handleUnaryCall<UpdatePopupAdRequest, UpdatePopupAdResponse>;
|
||||
deletePopupAd: handleUnaryCall<DeletePopupAdRequest, MessageResponse>;
|
||||
getActivePopupAd: handleUnaryCall<GetActivePopupAdRequest, GetActivePopupAdResponse>;
|
||||
}
|
||||
|
||||
export interface PopupAdsClient extends Client {
|
||||
listPopupAds(
|
||||
request: ListPopupAdsRequest,
|
||||
callback: (error: ServiceError | null, response: ListPopupAdsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
listPopupAds(
|
||||
request: ListPopupAdsRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: ListPopupAdsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
listPopupAds(
|
||||
request: ListPopupAdsRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: ListPopupAdsResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
createPopupAd(
|
||||
request: CreatePopupAdRequest,
|
||||
callback: (error: ServiceError | null, response: CreatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
createPopupAd(
|
||||
request: CreatePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: CreatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
createPopupAd(
|
||||
request: CreatePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: CreatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
updatePopupAd(
|
||||
request: UpdatePopupAdRequest,
|
||||
callback: (error: ServiceError | null, response: UpdatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
updatePopupAd(
|
||||
request: UpdatePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: UpdatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
updatePopupAd(
|
||||
request: UpdatePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: UpdatePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
deletePopupAd(
|
||||
request: DeletePopupAdRequest,
|
||||
callback: (error: ServiceError | null, response: MessageResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
deletePopupAd(
|
||||
request: DeletePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: MessageResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
deletePopupAd(
|
||||
request: DeletePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: MessageResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
getActivePopupAd(
|
||||
request: GetActivePopupAdRequest,
|
||||
callback: (error: ServiceError | null, response: GetActivePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
getActivePopupAd(
|
||||
request: GetActivePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
callback: (error: ServiceError | null, response: GetActivePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
getActivePopupAd(
|
||||
request: GetActivePopupAdRequest,
|
||||
metadata: Metadata,
|
||||
options: Partial<CallOptions>,
|
||||
callback: (error: ServiceError | null, response: GetActivePopupAdResponse) => void,
|
||||
): ClientUnaryCall;
|
||||
}
|
||||
|
||||
export const PopupAdsClient = makeGenericClientConstructor(PopupAdsService, "stream.app.v1.PopupAds") as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): PopupAdsClient;
|
||||
service: typeof PopupAdsService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
export type PlayerConfigsService = typeof PlayerConfigsService;
|
||||
export const PlayerConfigsService = {
|
||||
listPlayerConfigs: {
|
||||
@@ -2469,6 +3407,17 @@ type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ export interface AdTemplate {
|
||||
updatedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface PopupAd {
|
||||
id?: string | undefined;
|
||||
type?: string | undefined;
|
||||
label?: string | undefined;
|
||||
value?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
maxTriggersPerSession?: number | undefined;
|
||||
createdAt?: string | undefined;
|
||||
updatedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface PlayerConfig {
|
||||
id?: string | undefined;
|
||||
name?: string | undefined;
|
||||
@@ -335,6 +346,19 @@ export interface AdminAdTemplate {
|
||||
updatedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AdminPopupAd {
|
||||
id?: string | undefined;
|
||||
userId?: string | undefined;
|
||||
type?: string | undefined;
|
||||
label?: string | undefined;
|
||||
value?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
maxTriggersPerSession?: number | undefined;
|
||||
ownerEmail?: string | undefined;
|
||||
createdAt?: string | undefined;
|
||||
updatedAt?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AdminJob {
|
||||
id?: string | undefined;
|
||||
status?: string | undefined;
|
||||
@@ -1681,6 +1705,203 @@ export const AdTemplate: MessageFns<AdTemplate> = {
|
||||
},
|
||||
};
|
||||
|
||||
function createBasePopupAd(): PopupAd {
|
||||
return {
|
||||
id: "",
|
||||
type: "",
|
||||
label: "",
|
||||
value: "",
|
||||
isActive: false,
|
||||
maxTriggersPerSession: 0,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const PopupAd: MessageFns<PopupAd> = {
|
||||
encode(message: PopupAd, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
writer.uint32(18).string(message.type);
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
writer.uint32(26).string(message.label);
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
writer.uint32(34).string(message.value);
|
||||
}
|
||||
if (message.isActive !== undefined && message.isActive !== false) {
|
||||
writer.uint32(40).bool(message.isActive);
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined && message.maxTriggersPerSession !== 0) {
|
||||
writer.uint32(48).int32(message.maxTriggersPerSession);
|
||||
}
|
||||
if (message.createdAt !== undefined) {
|
||||
Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(58).fork()).join();
|
||||
}
|
||||
if (message.updatedAt !== undefined) {
|
||||
Timestamp.encode(toTimestamp(message.updatedAt), writer.uint32(66).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): PopupAd {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePopupAd();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.type = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.label = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.isActive = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.maxTriggersPerSession = reader.int32();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 58) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 66) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.updatedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PopupAd {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
label: isSet(object.label) ? globalThis.String(object.label) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
isActive: isSet(object.isActive)
|
||||
? globalThis.Boolean(object.isActive)
|
||||
: isSet(object.is_active)
|
||||
? globalThis.Boolean(object.is_active)
|
||||
: false,
|
||||
maxTriggersPerSession: isSet(object.maxTriggersPerSession)
|
||||
? globalThis.Number(object.maxTriggersPerSession)
|
||||
: isSet(object.max_triggers_per_session)
|
||||
? globalThis.Number(object.max_triggers_per_session)
|
||||
: 0,
|
||||
createdAt: isSet(object.createdAt)
|
||||
? globalThis.String(object.createdAt)
|
||||
: isSet(object.created_at)
|
||||
? globalThis.String(object.created_at)
|
||||
: undefined,
|
||||
updatedAt: isSet(object.updatedAt)
|
||||
? globalThis.String(object.updatedAt)
|
||||
: isSet(object.updated_at)
|
||||
? globalThis.String(object.updated_at)
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: PopupAd): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
obj.type = message.type;
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
obj.label = message.label;
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
if (message.isActive !== undefined && message.isActive !== false) {
|
||||
obj.isActive = message.isActive;
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined && message.maxTriggersPerSession !== 0) {
|
||||
obj.maxTriggersPerSession = Math.round(message.maxTriggersPerSession);
|
||||
}
|
||||
if (message.createdAt !== undefined) {
|
||||
obj.createdAt = message.createdAt;
|
||||
}
|
||||
if (message.updatedAt !== undefined) {
|
||||
obj.updatedAt = message.updatedAt;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<PopupAd>, I>>(base?: I): PopupAd {
|
||||
return PopupAd.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<PopupAd>, I>>(object: I): PopupAd {
|
||||
const message = createBasePopupAd();
|
||||
message.id = object.id ?? "";
|
||||
message.type = object.type ?? "";
|
||||
message.label = object.label ?? "";
|
||||
message.value = object.value ?? "";
|
||||
message.isActive = object.isActive ?? false;
|
||||
message.maxTriggersPerSession = object.maxTriggersPerSession ?? 0;
|
||||
message.createdAt = object.createdAt ?? undefined;
|
||||
message.updatedAt = object.updatedAt ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBasePlayerConfig(): PlayerConfig {
|
||||
return {
|
||||
id: "",
|
||||
@@ -6341,6 +6562,245 @@ export const AdminAdTemplate: MessageFns<AdminAdTemplate> = {
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAdminPopupAd(): AdminPopupAd {
|
||||
return {
|
||||
id: "",
|
||||
userId: "",
|
||||
type: "",
|
||||
label: "",
|
||||
value: "",
|
||||
isActive: false,
|
||||
maxTriggersPerSession: 0,
|
||||
ownerEmail: undefined,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const AdminPopupAd: MessageFns<AdminPopupAd> = {
|
||||
encode(message: AdminPopupAd, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
if (message.userId !== undefined && message.userId !== "") {
|
||||
writer.uint32(18).string(message.userId);
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
writer.uint32(26).string(message.type);
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
writer.uint32(34).string(message.label);
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
writer.uint32(42).string(message.value);
|
||||
}
|
||||
if (message.isActive !== undefined && message.isActive !== false) {
|
||||
writer.uint32(48).bool(message.isActive);
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined && message.maxTriggersPerSession !== 0) {
|
||||
writer.uint32(56).int32(message.maxTriggersPerSession);
|
||||
}
|
||||
if (message.ownerEmail !== undefined) {
|
||||
writer.uint32(66).string(message.ownerEmail);
|
||||
}
|
||||
if (message.createdAt !== undefined) {
|
||||
Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(74).fork()).join();
|
||||
}
|
||||
if (message.updatedAt !== undefined) {
|
||||
Timestamp.encode(toTimestamp(message.updatedAt), writer.uint32(82).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AdminPopupAd {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
const end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAdminPopupAd();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.userId = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.type = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.label = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.value = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.isActive = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 56) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.maxTriggersPerSession = reader.int32();
|
||||
continue;
|
||||
}
|
||||
case 8: {
|
||||
if (tag !== 66) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ownerEmail = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 9: {
|
||||
if (tag !== 74) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
case 10: {
|
||||
if (tag !== 82) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.updatedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AdminPopupAd {
|
||||
return {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
userId: isSet(object.userId)
|
||||
? globalThis.String(object.userId)
|
||||
: isSet(object.user_id)
|
||||
? globalThis.String(object.user_id)
|
||||
: "",
|
||||
type: isSet(object.type) ? globalThis.String(object.type) : "",
|
||||
label: isSet(object.label) ? globalThis.String(object.label) : "",
|
||||
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||
isActive: isSet(object.isActive)
|
||||
? globalThis.Boolean(object.isActive)
|
||||
: isSet(object.is_active)
|
||||
? globalThis.Boolean(object.is_active)
|
||||
: false,
|
||||
maxTriggersPerSession: isSet(object.maxTriggersPerSession)
|
||||
? globalThis.Number(object.maxTriggersPerSession)
|
||||
: isSet(object.max_triggers_per_session)
|
||||
? globalThis.Number(object.max_triggers_per_session)
|
||||
: 0,
|
||||
ownerEmail: isSet(object.ownerEmail)
|
||||
? globalThis.String(object.ownerEmail)
|
||||
: isSet(object.owner_email)
|
||||
? globalThis.String(object.owner_email)
|
||||
: undefined,
|
||||
createdAt: isSet(object.createdAt)
|
||||
? globalThis.String(object.createdAt)
|
||||
: isSet(object.created_at)
|
||||
? globalThis.String(object.created_at)
|
||||
: undefined,
|
||||
updatedAt: isSet(object.updatedAt)
|
||||
? globalThis.String(object.updatedAt)
|
||||
: isSet(object.updated_at)
|
||||
? globalThis.String(object.updated_at)
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: AdminPopupAd): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== undefined && message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.userId !== undefined && message.userId !== "") {
|
||||
obj.userId = message.userId;
|
||||
}
|
||||
if (message.type !== undefined && message.type !== "") {
|
||||
obj.type = message.type;
|
||||
}
|
||||
if (message.label !== undefined && message.label !== "") {
|
||||
obj.label = message.label;
|
||||
}
|
||||
if (message.value !== undefined && message.value !== "") {
|
||||
obj.value = message.value;
|
||||
}
|
||||
if (message.isActive !== undefined && message.isActive !== false) {
|
||||
obj.isActive = message.isActive;
|
||||
}
|
||||
if (message.maxTriggersPerSession !== undefined && message.maxTriggersPerSession !== 0) {
|
||||
obj.maxTriggersPerSession = Math.round(message.maxTriggersPerSession);
|
||||
}
|
||||
if (message.ownerEmail !== undefined) {
|
||||
obj.ownerEmail = message.ownerEmail;
|
||||
}
|
||||
if (message.createdAt !== undefined) {
|
||||
obj.createdAt = message.createdAt;
|
||||
}
|
||||
if (message.updatedAt !== undefined) {
|
||||
obj.updatedAt = message.updatedAt;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AdminPopupAd>, I>>(base?: I): AdminPopupAd {
|
||||
return AdminPopupAd.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<AdminPopupAd>, I>>(object: I): AdminPopupAd {
|
||||
const message = createBaseAdminPopupAd();
|
||||
message.id = object.id ?? "";
|
||||
message.userId = object.userId ?? "";
|
||||
message.type = object.type ?? "";
|
||||
message.label = object.label ?? "";
|
||||
message.value = object.value ?? "";
|
||||
message.isActive = object.isActive ?? false;
|
||||
message.maxTriggersPerSession = object.maxTriggersPerSession ?? 0;
|
||||
message.ownerEmail = object.ownerEmail ?? undefined;
|
||||
message.createdAt = object.createdAt ?? undefined;
|
||||
message.updatedAt = object.updatedAt ?? undefined;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseAdminJob(): AdminJob {
|
||||
return {
|
||||
id: "",
|
||||
|
||||
@@ -307,6 +307,58 @@ export const adminMethods = {
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminAdTemplate(data, metadata);
|
||||
}),
|
||||
listAdminPopupAds: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
userId: optionalTrimmed(),
|
||||
search: optionalTrimmed(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.listAdminPopupAds(data, metadata);
|
||||
}),
|
||||
createAdminPopupAd: validateFn(
|
||||
z.object({
|
||||
userId: z.string().trim().min(1),
|
||||
type: z.enum(['url', 'script']),
|
||||
label: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1),
|
||||
isActive: z.boolean().optional(),
|
||||
maxTriggersPerSession: z.number().int().min(1).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.createAdminPopupAd(data, metadata);
|
||||
}),
|
||||
updateAdminPopupAd: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
type: z.enum(['url', 'script']),
|
||||
label: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1),
|
||||
isActive: z.boolean().optional(),
|
||||
maxTriggersPerSession: z.number().int().min(1).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.updateAdminPopupAd(data, metadata);
|
||||
}),
|
||||
deleteAdminPopupAd: validateFn(
|
||||
z.object({ id: z.string().trim().min(1) }),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const adminClient = context.get("adminClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adminClient.deleteAdminPopupAd(data, metadata);
|
||||
}),
|
||||
listAdminPlayerConfigs: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
|
||||
@@ -139,6 +139,60 @@ export const meMethods = {
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await adTemplatesClient.deleteAdTemplate(data, metadata);
|
||||
}),
|
||||
listPopupAds: validateFn(
|
||||
z.object({
|
||||
page: z.number().int().min(1).optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
}).optional().default({}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const popupAdsClient = context.get("popupAdsClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await popupAdsClient.listPopupAds(data, metadata);
|
||||
}),
|
||||
createPopupAd: validateFn(
|
||||
z.object({
|
||||
type: z.enum(['url', 'script']),
|
||||
label: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1),
|
||||
isActive: z.boolean().optional(),
|
||||
maxTriggersPerSession: z.number().int().min(1).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const popupAdsClient = context.get("popupAdsClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await popupAdsClient.createPopupAd(data, metadata);
|
||||
}),
|
||||
updatePopupAd: validateFn(
|
||||
z.object({
|
||||
id: z.string().trim().min(1),
|
||||
type: z.enum(['url', 'script']),
|
||||
label: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1),
|
||||
isActive: z.boolean().optional(),
|
||||
maxTriggersPerSession: z.number().int().min(1).optional(),
|
||||
}),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const popupAdsClient = context.get("popupAdsClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await popupAdsClient.updatePopupAd(data, metadata);
|
||||
}),
|
||||
deletePopupAd: validateFn(
|
||||
z.object({ id: z.string().trim().min(1) }),
|
||||
)(async (data) => {
|
||||
const context = getContext();
|
||||
const popupAdsClient = context.get("popupAdsClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await popupAdsClient.deletePopupAd(data, metadata);
|
||||
}),
|
||||
getActivePopupAd: async () => {
|
||||
const context = getContext();
|
||||
const popupAdsClient = context.get("popupAdsClient");
|
||||
const metadata = context.get("grpcMetadata");
|
||||
return await popupAdsClient.getActivePopupAd({}, metadata);
|
||||
},
|
||||
listPlayerConfigs: async () => {
|
||||
const context = getContext();
|
||||
const playerConfigsClient = context.get("playerConfigsClient");
|
||||
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
DomainsClient,
|
||||
PlayerConfigsClient,
|
||||
PlansClient,
|
||||
PopupAdsClient,
|
||||
type AdTemplatesClient as AdTemplatesClientType,
|
||||
type DomainsClient as DomainsClientType,
|
||||
type PlayerConfigsClient as PlayerConfigsClientType,
|
||||
type PlansClient as PlansClientType,
|
||||
type PopupAdsClient as PopupAdsClientType,
|
||||
} from "@/server/api/proto/app/v1/catalog";
|
||||
import {
|
||||
PaymentsClient,
|
||||
@@ -41,6 +43,7 @@ declare module "hono" {
|
||||
authClient: PromisifiedClient<AuthClientType>;
|
||||
adminClient: PromisifiedClient<AdminClientType>;
|
||||
adTemplatesClient: PromisifiedClient<AdTemplatesClientType>;
|
||||
popupAdsClient: PromisifiedClient<PopupAdsClientType>;
|
||||
videosClient: PromisifiedClient<VideosClientType>;
|
||||
domainsClient: PromisifiedClient<DomainsClientType>;
|
||||
playerConfigsClient: PromisifiedClient<PlayerConfigsClientType>;
|
||||
@@ -145,6 +148,14 @@ export const getAdTemplatesClient = () => {
|
||||
return context.get("adTemplatesClient");
|
||||
};
|
||||
|
||||
export const getPopupAdsClient = () => {
|
||||
const context = tryGetContext();
|
||||
if (!context) {
|
||||
throw new Error("No context available to get PopupAdsClient");
|
||||
}
|
||||
return context.get("popupAdsClient");
|
||||
};
|
||||
|
||||
export const getVideosClient = () => {
|
||||
const context = tryGetContext();
|
||||
if (!context) {
|
||||
@@ -205,6 +216,7 @@ export const setupServices = (app: Hono) => {
|
||||
const authClient = new AuthClient(grpcAddress(), creds);
|
||||
const adminClient = new AdminClient(grpcAddress(), creds);
|
||||
const adTemplatesClient = new AdTemplatesClient(grpcAddress(), creds);
|
||||
const popupAdsClient = new PopupAdsClient(grpcAddress(), creds);
|
||||
const videosClient = new VideosClient(grpcAddress(), creds);
|
||||
const domainsClient = new DomainsClient(grpcAddress(), creds);
|
||||
const playerConfigsClient = new PlayerConfigsClient(grpcAddress(), creds);
|
||||
@@ -216,6 +228,7 @@ export const setupServices = (app: Hono) => {
|
||||
c.set("authClient", promisifyClient(authClient));
|
||||
c.set("adminClient", promisifyClient(adminClient));
|
||||
c.set("adTemplatesClient", promisifyClient(adTemplatesClient));
|
||||
c.set("popupAdsClient", promisifyClient(popupAdsClient));
|
||||
c.set("videosClient", promisifyClient(videosClient));
|
||||
c.set("domainsClient", promisifyClient(domainsClient));
|
||||
c.set("playerConfigsClient", promisifyClient(playerConfigsClient));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "i18next-vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useNotifications } from "@/composables/useNotifications";
|
||||
|
||||
type ProfileUpdatePayload = {
|
||||
username?: string;
|
||||
@@ -21,6 +22,7 @@ type AuthUserPayload = User & {
|
||||
};
|
||||
|
||||
const mqttBrokerUrl = "wss://mqtt-dashboard.com:8884/mqtt";
|
||||
const userNotificationTopic = (userId: string) => ["picpic", "notifications", userId].join("/");
|
||||
|
||||
const normalizeUser = (user: User | null): AuthUserPayload | null => {
|
||||
if (!user) return null;
|
||||
@@ -38,6 +40,7 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
const user = ref<AuthUserPayload | null>(null);
|
||||
const router = useRouter();
|
||||
const { t, i18next } = useTranslation();
|
||||
const notificationStore = useNotifications();
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const initialized = ref(false);
|
||||
@@ -83,13 +86,14 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
|
||||
mqttClient = new TinyMqttClient(
|
||||
mqttBrokerUrl,
|
||||
[["ecos1231231", userId, "#"].join("/")],
|
||||
[userNotificationTopic(userId)],
|
||||
(topic, message) => {
|
||||
console.log(`Tín hiệu nhận được [${topic}]:`, message);
|
||||
notificationStore.ingestRealtimeNotification(message);
|
||||
},
|
||||
);
|
||||
mqttClient.connect();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(() => user.value?.language, (lng) => i18next.changeLanguage(lng));
|
||||
|
||||
@@ -21,5 +21,8 @@
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue" ]
|
||||
"src/**/*.vue",
|
||||
"auto-imports.d.ts",
|
||||
"components.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user