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:
2026-03-29 06:42:37 +00:00
parent 43702e8bf7
commit 8515498ade
31 changed files with 3905 additions and 78 deletions

6
components.d.ts vendored
View File

@@ -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']
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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>

View 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>

View 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>

View File

@@ -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"
/>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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") },

View File

@@ -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',

View File

@@ -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',

View File

@@ -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"

View File

@@ -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),
// });

View File

@@ -1,11 +0,0 @@
export type DomainApiItem = {
id?: string;
name?: string;
created_at?: string;
};
export type DomainItem = {
id: string;
name: string;
addedAt: string;
};

View 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>

View 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>

View 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>
),
}}
/>
);
},
});

View 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>

View 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;
}

View File

@@ -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.',

View 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

View File

@@ -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;
}

View File

@@ -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: "",

View File

@@ -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(),

View File

@@ -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");

View File

@@ -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));

View File

@@ -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));

View File

@@ -21,5 +21,8 @@
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue" ]
"src/**/*.vue",
"auto-imports.d.ts",
"components.d.ts"
]
}