develop-updateui #1

Merged
lethdat merged 78 commits from develop-updateui into master 2026-04-02 05:59:23 +00:00
8 changed files with 463 additions and 30 deletions
Showing only changes of commit 770c09b9b2 - Show all commits

View File

@@ -682,12 +682,5 @@ export class Api<
export const client = new Api({ export const client = new Api({
baseUrl: 'r', baseUrl: 'r',
// baseUrl: 'https://api.pipic.fun', // baseUrl: 'https://api.pipic.fun',
customFetch: (url, options) => { customFetch
options.headers = {
...options.headers,
"X-Forwarded-For": "[IP_ADDRESS]"
}
options.credentials = "include"
return fetch(url, options)
}
}); });

View File

@@ -1,25 +1,40 @@
import { tryGetContext } from "hono/context-storage"; import { tryGetContext } from "hono/context-storage";
export const customFetch = async (url: string, options: RequestInit) => { export const customFetch = (url: string, options: RequestInit) => {
options.credentials = "include"; options.credentials = "include";
if (import.meta.env.SSR) { if (import.meta.env.SSR) {
const c = tryGetContext<any>(); const c = tryGetContext<any>();
if (!c) { if (!c) {
throw new Error("Hono context not found in SSR"); throw new Error("Hono context not found in SSR");
} }
Object.assign(options, { // Merge headers properly - keep original options.headers and add request headers
headers: c.req.header() const reqHeaders = new Headers(c.req.header());
}); // Remove headers that shouldn't be forwarded
console.log("url", url) reqHeaders.delete("host");
const res = await fetch(["https://api.pipic.fun", url.replace(/r\//, '')].join('/'), options); reqHeaders.delete("connection");
if (url.includes("r/plans")) {
console.log("res", await res.json())
} const mergedHeaders: Record<string, string> = {};
res.headers.forEach((value, key) => { reqHeaders.forEach((value, key) => {
c.header(key, value); mergedHeaders[key] = value;
});
options.headers = {
...mergedHeaders,
...(options.headers as Record<string, string>)
};
const apiUrl = ["https://api.pipic.fun", url.replace(/^r/, '')].join('');
// const res = await fetch(apiUrl, options);
// Forward response headers to client (especially Set-Cookie)
// res.headers.forEach((value, key) => {
// c.header(key, value);
// });
return fetch(apiUrl, options).then(res => {
// Forward response headers to client (especially Set-Cookie)
res.headers.forEach((value, key) => {
c.header(key, value);
});
return res;
}); });
return res;
} }
return fetch(url, options);
} }

View File

@@ -1,8 +1,157 @@
<script setup lang="ts">
import PageHeader from '@/components/dashboard/PageHeader.vue';
import { computed, ref } from 'vue';
import NotificationActions from './components/NotificationActions.vue';
import NotificationList from './components/NotificationList.vue';
import NotificationTabs from './components/NotificationTabs.vue';
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
interface Notification {
id: string;
type: NotificationType;
title: string;
message: string;
time: string;
read: boolean;
actionUrl?: string;
actionLabel?: string;
}
const loading = ref(false);
const activeTab = ref('all');
// Mock notifications data
const notifications = ref<Notification[]>([
{
id: '1',
type: 'video',
title: 'Video processing complete',
message: 'Your video "Summer Vacation 2024" has been successfully processed and is now ready to stream.',
time: '2 minutes ago',
read: false,
actionUrl: '/video',
actionLabel: 'View video'
},
{
id: '2',
type: 'payment',
title: 'Payment successful',
message: 'Your subscription to Pro Plan has been renewed successfully. Next billing date: Feb 25, 2026.',
time: '1 hour ago',
read: false,
actionUrl: '/payments-and-plans',
actionLabel: 'View receipt'
},
{
id: '3',
type: 'warning',
title: 'Storage almost full',
message: 'You have used 85% of your storage quota. Consider upgrading your plan for more space.',
time: '3 hours ago',
read: false,
actionUrl: '/payments-and-plans',
actionLabel: 'Upgrade plan'
},
{
id: '4',
type: 'success',
title: 'Upload successful',
message: 'Your video "Product Demo v2" has been uploaded successfully.',
time: '1 day ago',
read: true
},
{
id: '5',
type: 'system',
title: 'Scheduled maintenance',
message: 'We will perform scheduled maintenance on Jan 30, 2026 from 2:00 AM to 4:00 AM UTC.',
time: '2 days ago',
read: true
},
{
id: '6',
type: 'info',
title: 'New feature available',
message: 'We just launched video analytics! Track your video performance with detailed insights.',
time: '3 days ago',
read: true,
actionUrl: '/video',
actionLabel: 'Try it now'
}
]);
const tabs = computed(() => [
{ key: 'all', label: 'All', icon: 'i-lucide-inbox', count: notifications.value.length },
{ key: 'unread', label: 'Unread', icon: 'i-lucide-bell-dot', count: unreadCount.value },
{ key: 'video', label: 'Videos', icon: 'i-lucide-video', count: notifications.value.filter(n => n.type === 'video').length },
{ key: 'payment', label: 'Payments', icon: 'i-lucide-credit-card', count: notifications.value.filter(n => n.type === 'payment').length }
]);
const filteredNotifications = computed(() => {
if (activeTab.value === 'all') return notifications.value;
if (activeTab.value === 'unread') return notifications.value.filter(n => !n.read);
return notifications.value.filter(n => n.type === activeTab.value);
});
const unreadCount = computed(() => notifications.value.filter(n => !n.read).length);
const handleMarkRead = (id: string) => {
const notification = notifications.value.find(n => n.id === id);
if (notification) notification.read = true;
};
const handleDelete = (id: string) => {
notifications.value = notifications.value.filter(n => n.id !== id);
};
const handleMarkAllRead = () => {
notifications.value.forEach(n => n.read = true);
};
const handleClearAll = () => {
notifications.value = [];
};
</script>
<template> <template>
<div> <div class="notification-page">
notification <PageHeader
title="Notifications"
description="Stay updated with your latest activities and alerts."
:breadcrumbs="[
{ label: 'Dashboard', to: '/' },
{ label: 'Notifications' }
]"
/>
<div class="notification-container bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
<NotificationActions
:loading="loading"
:total-count="notifications.length"
:unread-count="unreadCount"
@mark-all-read="handleMarkAllRead"
@clear-all="handleClearAll"
/>
<NotificationTabs
:tabs="tabs"
:active-tab="activeTab"
@update:active-tab="activeTab = $event"
/>
<NotificationList
:notifications="filteredNotifications"
:loading="loading"
@mark-read="handleMarkRead"
@delete="handleDelete"
/>
</div>
</div> </div>
</template> </template>
<script setup lang="ts">
</script> <style scoped>
.notification-page {
max-width: 800px;
}
</style>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
interface Props {
loading?: boolean;
totalCount: number;
unreadCount: number;
}
defineProps<Props>();
const emit = defineEmits<{
markAllRead: [];
clearAll: [];
}>();
</script>
<template>
<div class="notification-header flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="stats flex items-center gap-4">
<div class="flex items-center gap-2 text-sm">
<span class="i-lucide-bell w-4 h-4 text-gray-400"></span>
<span class="text-gray-600">{{ totalCount }} notifications</span>
</div>
<div v-if="unreadCount > 0" class="flex items-center gap-2 text-sm">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
<span class="text-primary font-medium">{{ unreadCount }} unread</span>
</div>
</div>
</div>
<div class="actions flex items-center gap-2">
<button
v-if="unreadCount > 0"
@click="emit('markAllRead')"
:disabled="loading"
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-primary
hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
>
<span class="i-lucide-check-check w-4 h-4"></span>
Mark all read
</button>
<button
v-if="totalCount > 0"
@click="emit('clearAll')"
:disabled="loading"
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-red-600
hover:bg-red-50 rounded-lg transition-colors flex items-center gap-2"
>
<span class="i-lucide-trash w-4 h-4"></span>
Clear all
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
notification: {
id: string;
type: 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
title: string;
message: string;
time: string;
read: boolean;
actionUrl?: string;
actionLabel?: string;
};
}
const props = defineProps<Props>();
const emit = defineEmits<{
markRead: [id: string];
delete: [id: string];
}>();
const iconClass = computed(() => {
const icons: Record<string, string> = {
info: 'i-lucide-info text-blue-500',
success: 'i-lucide-check-circle text-green-500',
warning: 'i-lucide-alert-triangle text-amber-500',
error: 'i-lucide-x-circle text-red-500',
video: 'i-lucide-video text-purple-500',
payment: 'i-lucide-credit-card text-emerald-500',
system: 'i-lucide-settings text-gray-500'
};
return icons[props.notification.type] || icons.info;
});
const bgClass = computed(() => {
return props.notification.read
? 'bg-white hover:bg-gray-50'
: 'bg-blue-50/50 hover:bg-blue-50';
});
</script>
<template>
<div
:class="[
'notification-item p-4 rounded-xl border border-gray-200/80 transition-all duration-200',
'flex items-start gap-4 group cursor-pointer',
bgClass
]"
@click="emit('markRead', notification.id)"
>
<!-- Icon -->
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
<span :class="[iconClass, 'w-5 h-5']"></span>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<h4 :class="['font-semibold text-gray-900', !notification.read && 'text-primary-700']">
{{ notification.title }}
</h4>
<span class="text-xs text-gray-400 whitespace-nowrap">{{ notification.time }}</span>
</div>
<p class="text-sm text-gray-600 mt-1 line-clamp-2">{{ notification.message }}</p>
<!-- Action Button -->
<router-link
v-if="notification.actionUrl"
:to="notification.actionUrl"
class="inline-flex items-center gap-1 text-sm text-primary font-medium mt-2 hover:underline"
>
{{ notification.actionLabel || 'View Details' }}
<span class="i-lucide-arrow-right w-4 h-4"></span>
</router-link>
</div>
<!-- Actions -->
<div class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1">
<button
v-if="!notification.read"
@click.stop="emit('markRead', notification.id)"
class="p-2 rounded-lg hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="Mark as read"
>
<span class="i-lucide-check w-4 h-4"></span>
</button>
<button
@click.stop="emit('delete', notification.id)"
class="p-2 rounded-lg hover:bg-red-100 text-gray-500 hover:text-red-600 transition-colors"
title="Delete"
>
<span class="i-lucide-trash-2 w-4 h-4"></span>
</button>
</div>
<!-- Unread indicator -->
<div
v-if="!notification.read"
class="absolute left-2 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-primary"
></div>
</div>
</template>
<style scoped>
.notification-item {
position: relative;
}
</style>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import NotificationItem from './NotificationItem.vue';
interface Notification {
id: string;
type: 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
title: string;
message: string;
time: string;
read: boolean;
actionUrl?: string;
actionLabel?: string;
}
interface Props {
notifications: Notification[];
loading?: boolean;
}
defineProps<Props>();
const emit = defineEmits<{
markRead: [id: string];
delete: [id: string];
}>();
</script>
<template>
<div class="notification-list space-y-3">
<!-- Loading skeleton -->
<template v-if="loading">
<div
v-for="i in 5"
:key="i"
class="p-4 rounded-xl border border-gray-200 animate-pulse"
>
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full bg-gray-200"></div>
<div class="flex-1 space-y-2">
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
<div class="h-3 bg-gray-200 rounded w-2/3"></div>
</div>
</div>
</div>
</template>
<!-- Notification items -->
<template v-else-if="notifications.length > 0">
<NotificationItem
v-for="notification in notifications"
:key="notification.id"
:notification="notification"
@mark-read="emit('markRead', $event)"
@delete="emit('delete', $event)"
/>
</template>
<!-- Empty state -->
<div
v-else
class="py-16 text-center"
>
<div class="w-20 h-20 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
<span class="i-lucide-bell-off w-10 h-10 text-gray-400"></span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-1">No notifications</h3>
<p class="text-gray-500">You're all caught up! Check back later.</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
interface Tab {
key: string;
label: string;
icon: string;
count?: number;
}
interface Props {
tabs: Tab[];
activeTab: string;
}
defineProps<Props>();
const emit = defineEmits<{
'update:activeTab': [key: string];
}>();
</script>
<template>
<div class="notification-tabs flex items-center gap-1 p-1 bg-gray-100 rounded-xl mb-6">
<button
v-for="tab in tabs"
:key="tab.key"
@click="emit('update:activeTab', tab.key)"
:class="[
'flex-1 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200',
'flex items-center justify-center gap-2',
activeTab === tab.key
? 'bg-white text-primary shadow-sm'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
]"
>
<span :class="[tab.icon, 'w-4 h-4']"></span>
{{ tab.label }}
<span
v-if="tab.count && tab.count > 0"
:class="[
'px-1.5 py-0.5 text-xs rounded-full min-w-[20px]',
activeTab === tab.key
? 'bg-primary text-white'
: 'bg-gray-200 text-gray-600'
]"
>
{{ tab.count > 99 ? '99+' : tab.count }}
</span>
</button>
</div>
</template>

View File

@@ -43,11 +43,7 @@ const currentPlan = computed(() => {
return plans.value.find(p => p.id === currentPlanId.value); return plans.value.find(p => p.id === currentPlanId.value);
}); });
const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", async () => { const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", () => client.plans.plansList())
console.log("plansList")
const res = await client.plans.plansList()
return res.data
})
watch(data, (newValue) => { watch(data, (newValue) => {
if (newValue) { if (newValue) {