@mundogamernetwork/shared-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -0
- package/components/PressKit/AssetGallery.vue +349 -0
- package/components/PressKit/Awards.vue +100 -0
- package/components/PressKit/Credits.vue +78 -0
- package/components/PressKit/FactSheet.vue +204 -0
- package/components/PressKit/Hero.vue +143 -0
- package/components/PressKit/Quotes.vue +80 -0
- package/components/PressKit/VideoPlayer.vue +134 -0
- package/components/checkout/MgCartItemList.vue +214 -0
- package/components/checkout/MgCartSummary.vue +204 -0
- package/components/checkout/MgCheckoutSidebar.vue +230 -0
- package/components/checkout/MgGuestEmailForm.vue +97 -0
- package/components/checkout/MgPaymentMethodSelector.vue +162 -0
- package/components/checkout/MgPixQRCode.vue +222 -0
- package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
- package/components/indie-wall/MuralCanvas.vue +481 -0
- package/components/indie-wall/StepBlock.vue +314 -0
- package/components/indie-wall/StepCustomize.vue +530 -0
- package/components/indie-wall/StepGoal.vue +169 -0
- package/components/indie-wall/StepPackage.vue +145 -0
- package/components/indie-wall/StepPay.vue +209 -0
- package/components/indie-wall/SupportStepper.vue +372 -0
- package/components/invoices/MgInvoiceDownload.vue +50 -0
- package/components/pricing/MgBillingToggle.vue +74 -0
- package/components/pricing/MgPricingCard.vue +245 -0
- package/components/ui/Header/MgMessageCard.vue +147 -0
- package/components/ui/Header/MgMessageModal.vue +414 -0
- package/components/ui/Header/MgNotificationCard.vue +200 -0
- package/components/ui/Header/MgNotificationsModal.vue +125 -0
- package/components/ui/MgAnnouncementBanner.vue +147 -0
- package/components/ui/MgBanners.vue +23 -0
- package/components/ui/MgHeaderComponent.vue +283 -0
- package/components/ui/MgHeaderUIConfig.vue +225 -0
- package/components/ui/MgHeaderUIUser.vue +301 -0
- package/components/ui/MgLoginModal.vue +156 -0
- package/components/ui/MgPromotionBanner.vue +185 -0
- package/composables/useLogout.ts +42 -0
- package/composables/useMgCheckout.ts +287 -0
- package/composables/useMgUserNotifications.ts +122 -0
- package/composables/usePaymentMethods.ts +75 -0
- package/composables/useSubscription.ts +163 -0
- package/middleware/auth.global.ts +40 -0
- package/nuxt.config.ts +31 -0
- package/package.json +40 -0
- package/pages/[slug]/index.vue +112 -0
- package/pages/about.vue +133 -0
- package/pages/blog.vue +430 -0
- package/pages/careers.vue +329 -0
- package/pages/contact.vue +339 -0
- package/pages/faq.vue +317 -0
- package/pages/health-check.vue +20 -0
- package/pages/icons.vue +58 -0
- package/pages/magazine/[slug].vue +209 -0
- package/pages/magazine/index.vue +267 -0
- package/pages/media-kit/[slug].vue +625 -0
- package/pages/mural/[slug].vue +1058 -0
- package/pages/partners.vue +290 -0
- package/pages/press.vue +237 -0
- package/pages/presskit/[slug].vue +191 -0
- package/pages/roadmap.vue +355 -0
- package/pages/status.vue +199 -0
- package/pages/team.vue +266 -0
- package/pages/wall/[slug].vue +11 -0
- package/plugins/auth.client.ts +17 -0
- package/plugins/echo.client.ts +132 -0
- package/services/authService.ts +95 -0
- package/services/chatService.ts +53 -0
- package/services/contactService.ts +35 -0
- package/services/documentService.ts +16 -0
- package/services/httpService.ts +95 -0
- package/services/indieWallService.ts +174 -0
- package/services/institutionalService.ts +248 -0
- package/services/mediaKitService.ts +51 -0
- package/services/notificationsService.ts +20 -0
- package/services/pressKitService.ts +55 -0
- package/stores/announcement.ts +129 -0
- package/stores/auth.ts +86 -0
- package/stores/chat.ts +150 -0
- package/stores/contact.ts +28 -0
- package/stores/document.ts +27 -0
- package/stores/index.ts +34 -0
- package/stores/institutional.ts +231 -0
- package/stores/login.ts +27 -0
- package/stores/notifications.ts +133 -0
- package/stores/promotion.ts +154 -0
- package/types/index.ts +135 -0
- package/utils/serialize.ts +29 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const promotionStore = usePromotionStore();
|
|
3
|
+
const { t } = useI18n();
|
|
4
|
+
|
|
5
|
+
onMounted(async () => {
|
|
6
|
+
await promotionStore.fetchBanner();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const { banner, isVisible } = storeToRefs(promotionStore);
|
|
10
|
+
|
|
11
|
+
const resolvedLinkText = computed(() => {
|
|
12
|
+
if (!banner.value?.link_url) return '';
|
|
13
|
+
if (banner.value.link_text) return banner.value.link_text;
|
|
14
|
+
return t('announcementBanner.learnMore', 'Learn more');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Inline colors override the type-based CSS vars when set
|
|
18
|
+
const bannerStyle = computed(() => {
|
|
19
|
+
if (!banner.value) return {};
|
|
20
|
+
const style: Record<string, string> = {};
|
|
21
|
+
if (banner.value.bg_color) style.backgroundColor = banner.value.bg_color;
|
|
22
|
+
if (banner.value.text_color) style.color = banner.value.text_color;
|
|
23
|
+
return style;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function onLinkClick() {
|
|
27
|
+
promotionStore.recordClick();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dismiss() {
|
|
31
|
+
promotionStore.dismiss();
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<Transition name="mg-promotion-slide">
|
|
37
|
+
<div
|
|
38
|
+
v-if="isVisible && banner && banner.message"
|
|
39
|
+
class="mg-promotion-banner"
|
|
40
|
+
:class="`mg-promotion-banner--${banner.type}`"
|
|
41
|
+
>
|
|
42
|
+
<img
|
|
43
|
+
v-if="banner.image_url"
|
|
44
|
+
:src="banner.image_url"
|
|
45
|
+
class="mg-promotion-banner__image"
|
|
46
|
+
:alt="banner.title || ''"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<div class="mg-promotion-banner__content">
|
|
50
|
+
<strong v-if="banner.title" class="mg-promotion-banner__title">
|
|
51
|
+
{{ banner.title }}
|
|
52
|
+
</strong>
|
|
53
|
+
<span class="mg-promotion-banner__message">{{ banner.message }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<a
|
|
57
|
+
v-if="banner.link_url"
|
|
58
|
+
:href="banner.link_url"
|
|
59
|
+
class="mg-promotion-banner__link"
|
|
60
|
+
target="_blank"
|
|
61
|
+
rel="noopener noreferrer"
|
|
62
|
+
@click="onLinkClick"
|
|
63
|
+
>{{ resolvedLinkText }}</a>
|
|
64
|
+
|
|
65
|
+
<button
|
|
66
|
+
class="mg-promotion-banner__close"
|
|
67
|
+
:aria-label="t('announcementBanner.close', 'Close')"
|
|
68
|
+
@click="dismiss"
|
|
69
|
+
>×</button>
|
|
70
|
+
</div>
|
|
71
|
+
</Transition>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<style lang="scss" scoped>
|
|
75
|
+
.mg-promotion-banner {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
gap: 0.75rem;
|
|
80
|
+
padding: 0.5rem 3rem 0.5rem 1rem;
|
|
81
|
+
width: 100%;
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
position: relative;
|
|
85
|
+
z-index: 99; // below announcement banner (z-index: 100)
|
|
86
|
+
|
|
87
|
+
&--info {
|
|
88
|
+
background: var(--promotion-info-bg, #1a6ea8);
|
|
89
|
+
color: var(--promotion-info-fg, #ffffff);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&--warning {
|
|
93
|
+
background: var(--promotion-warning-bg, #92400e);
|
|
94
|
+
color: var(--promotion-warning-fg, #ffffff);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&--success {
|
|
98
|
+
background: var(--promotion-success-bg, #166534);
|
|
99
|
+
color: var(--promotion-success-fg, #ffffff);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&--danger {
|
|
103
|
+
background: var(--promotion-danger-bg, #991b1b);
|
|
104
|
+
color: var(--promotion-danger-fg, #ffffff);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&--promo {
|
|
108
|
+
background: var(--promotion-promo-bg, #5b21b6);
|
|
109
|
+
color: var(--promotion-promo-fg, #ffffff);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__image {
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
height: 28px;
|
|
115
|
+
width: auto;
|
|
116
|
+
object-fit: contain;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&__content {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.375rem;
|
|
123
|
+
flex: 0 1 auto;
|
|
124
|
+
text-align: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&__title {
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&__message {
|
|
133
|
+
flex: 0 1 auto;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&__link {
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
text-decoration: underline;
|
|
140
|
+
color: inherit;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
|
|
143
|
+
&:hover {
|
|
144
|
+
opacity: 0.85;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&__close {
|
|
149
|
+
position: absolute;
|
|
150
|
+
right: 0.75rem;
|
|
151
|
+
top: 50%;
|
|
152
|
+
transform: translateY(-50%);
|
|
153
|
+
background: transparent;
|
|
154
|
+
border: none;
|
|
155
|
+
color: inherit;
|
|
156
|
+
font-size: 1.25rem;
|
|
157
|
+
line-height: 1;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
padding: 0 0.25rem;
|
|
160
|
+
opacity: 0.8;
|
|
161
|
+
|
|
162
|
+
&:hover {
|
|
163
|
+
opacity: 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.mg-promotion-slide-enter-active,
|
|
169
|
+
.mg-promotion-slide-leave-active {
|
|
170
|
+
transition: max-height 0.25s ease, opacity 0.25s ease;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.mg-promotion-slide-enter-from,
|
|
175
|
+
.mg-promotion-slide-leave-to {
|
|
176
|
+
max-height: 0;
|
|
177
|
+
opacity: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.mg-promotion-slide-enter-to,
|
|
181
|
+
.mg-promotion-slide-leave-from {
|
|
182
|
+
max-height: 60px;
|
|
183
|
+
opacity: 1;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { default as AuthService } from "../services/authService";
|
|
2
|
+
|
|
3
|
+
export function useLogout() {
|
|
4
|
+
const authStore = useAuthStore();
|
|
5
|
+
const runtimeConfig = useRuntimeConfig();
|
|
6
|
+
const accountsBaseUrl = runtimeConfig.public.mgSharedUi?.accountsBaseUrl || runtimeConfig.public.accountsBaseUrl;
|
|
7
|
+
|
|
8
|
+
const performLogout = async (redirectTo?: string) => {
|
|
9
|
+
try {
|
|
10
|
+
await AuthService.logout();
|
|
11
|
+
} catch {
|
|
12
|
+
// continue with cleanup even if server call fails
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Clear auth state
|
|
16
|
+
authStore.clearUser();
|
|
17
|
+
authStore.$reset();
|
|
18
|
+
|
|
19
|
+
// Clear cookies client-side
|
|
20
|
+
if (typeof document !== "undefined") {
|
|
21
|
+
const cookiesToClear = ["oauth_token", "browser_id", "mundo_gamer_network_session", "XSRF-TOKEN"];
|
|
22
|
+
cookiesToClear.forEach((name) => {
|
|
23
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
24
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`;
|
|
25
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname};`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Clear localStorage auth data
|
|
30
|
+
if (typeof localStorage !== "undefined") {
|
|
31
|
+
["accessToken", "userData", "userAbility"].forEach((key) => {
|
|
32
|
+
localStorage.removeItem(key);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Redirect
|
|
37
|
+
const target = redirectTo || `${accountsBaseUrl}/login`;
|
|
38
|
+
window.location.href = target;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return { performLogout };
|
|
42
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { ref, reactive, provide, inject, computed } from "vue";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
3
|
+
import { usePaymentMethods } from "./usePaymentMethods";
|
|
4
|
+
import type { PaymentMethod } from "./usePaymentMethods";
|
|
5
|
+
|
|
6
|
+
export interface CartItem {
|
|
7
|
+
id: number;
|
|
8
|
+
item_id: number;
|
|
9
|
+
item_type: string;
|
|
10
|
+
quantity: number;
|
|
11
|
+
price: number;
|
|
12
|
+
price_with_discount: number;
|
|
13
|
+
discount: number;
|
|
14
|
+
currency_id: number;
|
|
15
|
+
item: {
|
|
16
|
+
id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
image_url?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CartData {
|
|
25
|
+
items: CartItem[];
|
|
26
|
+
original_total: number;
|
|
27
|
+
total_with_discount: number;
|
|
28
|
+
total_discount?: number;
|
|
29
|
+
formatted_original_total?: string;
|
|
30
|
+
formatted_total_with_discount?: string;
|
|
31
|
+
formatted_total_discount?: string;
|
|
32
|
+
discount_coupon?: any;
|
|
33
|
+
discount_coupon_code?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CheckoutResponse {
|
|
37
|
+
order: { id: string; status: string };
|
|
38
|
+
payer_action: string;
|
|
39
|
+
payment_type: "redirect" | "pix" | "free";
|
|
40
|
+
pix_data?: {
|
|
41
|
+
qr_code: string;
|
|
42
|
+
qr_code_base64: string;
|
|
43
|
+
expires_at: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CHECKOUT_KEY = "mg-checkout-instance";
|
|
48
|
+
|
|
49
|
+
export function useMgCheckout(httpService: AxiosInstance) {
|
|
50
|
+
// Payment methods (dynamic from backend)
|
|
51
|
+
const paymentMethods = usePaymentMethods(httpService);
|
|
52
|
+
|
|
53
|
+
// Cart state
|
|
54
|
+
const cart = ref<CartData | null>(null);
|
|
55
|
+
const cartLoading = ref(false);
|
|
56
|
+
const cartError = ref<string | null>(null);
|
|
57
|
+
|
|
58
|
+
// Checkout state
|
|
59
|
+
const checkoutLoading = ref(false);
|
|
60
|
+
const checkoutResponse = ref<CheckoutResponse | null>(null);
|
|
61
|
+
const checkoutError = ref<string | null>(null);
|
|
62
|
+
|
|
63
|
+
// Guest state
|
|
64
|
+
const guestEmail = ref("");
|
|
65
|
+
const emailError = ref("");
|
|
66
|
+
|
|
67
|
+
// Terms
|
|
68
|
+
const termsAccepted = ref(false);
|
|
69
|
+
|
|
70
|
+
// Coupon
|
|
71
|
+
const couponCode = ref("");
|
|
72
|
+
const couponLoading = ref(false);
|
|
73
|
+
const couponError = ref<string | null>(null);
|
|
74
|
+
const couponInfo = ref<any>(null);
|
|
75
|
+
|
|
76
|
+
// Cart operations
|
|
77
|
+
async function fetchCart() {
|
|
78
|
+
cartLoading.value = true;
|
|
79
|
+
cartError.value = null;
|
|
80
|
+
try {
|
|
81
|
+
const res = await httpService.get("/shopping-cart");
|
|
82
|
+
cart.value = res.data?.data ?? null;
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
cartError.value = e?.message ?? "Failed to load cart";
|
|
85
|
+
} finally {
|
|
86
|
+
cartLoading.value = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function addItem(itemId: number, itemType: string, quantity = 1) {
|
|
91
|
+
cartLoading.value = true;
|
|
92
|
+
try {
|
|
93
|
+
const res = await httpService.post("/shopping-cart/add", {
|
|
94
|
+
item_id: itemId,
|
|
95
|
+
item_type: itemType,
|
|
96
|
+
quantity,
|
|
97
|
+
});
|
|
98
|
+
cart.value = res.data?.data ?? cart.value;
|
|
99
|
+
} finally {
|
|
100
|
+
cartLoading.value = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function removeItem(itemId: number, itemType: string, quantity = 1) {
|
|
105
|
+
cartLoading.value = true;
|
|
106
|
+
try {
|
|
107
|
+
const res = await httpService.post("/shopping-cart/remove", {
|
|
108
|
+
item_id: itemId,
|
|
109
|
+
item_type: itemType,
|
|
110
|
+
quantity,
|
|
111
|
+
});
|
|
112
|
+
cart.value = res.data?.data ?? cart.value;
|
|
113
|
+
} finally {
|
|
114
|
+
cartLoading.value = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function clearCart() {
|
|
119
|
+
cartLoading.value = true;
|
|
120
|
+
try {
|
|
121
|
+
await httpService.post("/shopping-cart/clear");
|
|
122
|
+
cart.value = null;
|
|
123
|
+
} finally {
|
|
124
|
+
cartLoading.value = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Coupon
|
|
129
|
+
async function applyCoupon(code: string) {
|
|
130
|
+
couponLoading.value = true;
|
|
131
|
+
couponError.value = null;
|
|
132
|
+
try {
|
|
133
|
+
const res = await httpService.post("/shopping-cart/apply-coupon", {
|
|
134
|
+
code: code.toUpperCase(),
|
|
135
|
+
});
|
|
136
|
+
const data = res.data?.data ?? res.data;
|
|
137
|
+
if (res.data?.warnings?.length) {
|
|
138
|
+
couponError.value = res.data.warnings[0]?.message ?? "Invalid coupon";
|
|
139
|
+
} else {
|
|
140
|
+
cart.value = data;
|
|
141
|
+
couponInfo.value = data?.discount_coupon ?? null;
|
|
142
|
+
}
|
|
143
|
+
} catch (e: any) {
|
|
144
|
+
couponError.value = e?.response?.data?.message ?? "Invalid coupon code";
|
|
145
|
+
} finally {
|
|
146
|
+
couponLoading.value = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function removeCoupon() {
|
|
151
|
+
couponLoading.value = true;
|
|
152
|
+
try {
|
|
153
|
+
const res = await httpService.post("/shopping-cart/remove-coupon");
|
|
154
|
+
cart.value = res.data?.data ?? cart.value;
|
|
155
|
+
couponInfo.value = null;
|
|
156
|
+
couponCode.value = "";
|
|
157
|
+
} finally {
|
|
158
|
+
couponLoading.value = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Checkout
|
|
163
|
+
async function checkout(successUrl: string, cancelUrl: string) {
|
|
164
|
+
if (!termsAccepted.value) {
|
|
165
|
+
checkoutError.value = "You must accept the terms of use";
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Guest email validation
|
|
170
|
+
if (guestEmail.value && !isValidEmail(guestEmail.value)) {
|
|
171
|
+
emailError.value = "Please enter a valid email";
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
checkoutLoading.value = true;
|
|
176
|
+
checkoutError.value = null;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const params: Record<string, string> = {
|
|
180
|
+
payment_method: paymentMethods.selectedMethod.value,
|
|
181
|
+
success_url: successUrl,
|
|
182
|
+
cancel_url: cancelUrl,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (guestEmail.value) {
|
|
186
|
+
params.email = guestEmail.value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const res = await httpService.post("/shopping-cart/checkout", params);
|
|
190
|
+
const data = res.data;
|
|
191
|
+
|
|
192
|
+
checkoutResponse.value = {
|
|
193
|
+
order: data.order,
|
|
194
|
+
payer_action: data.payer_action,
|
|
195
|
+
payment_type: data.payment_type ?? "redirect",
|
|
196
|
+
pix_data: data.pix_data ?? undefined,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// For redirect-based payments (Stripe, PayPal, free), redirect immediately
|
|
200
|
+
if (
|
|
201
|
+
checkoutResponse.value.payment_type === "redirect" ||
|
|
202
|
+
checkoutResponse.value.payment_type === "free"
|
|
203
|
+
) {
|
|
204
|
+
window.location.href = checkoutResponse.value.payer_action;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// For Pix, the component will show the QR code
|
|
208
|
+
return checkoutResponse.value;
|
|
209
|
+
} catch (e: any) {
|
|
210
|
+
checkoutError.value =
|
|
211
|
+
e?.response?.data?.message ?? "Checkout failed. Please try again.";
|
|
212
|
+
return null;
|
|
213
|
+
} finally {
|
|
214
|
+
checkoutLoading.value = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Computed
|
|
219
|
+
const hasItems = computed(
|
|
220
|
+
() => (cart.value?.items?.length ?? 0) > 0
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const hasDiscount = computed(
|
|
224
|
+
() =>
|
|
225
|
+
cart.value != null &&
|
|
226
|
+
cart.value.original_total !== cart.value.total_with_discount
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const cartTotal = computed(() => cart.value?.total_with_discount ?? 0);
|
|
230
|
+
const cartOriginalTotal = computed(() => cart.value?.original_total ?? 0);
|
|
231
|
+
|
|
232
|
+
// Helpers
|
|
233
|
+
function isValidEmail(email: string): boolean {
|
|
234
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const instance = {
|
|
238
|
+
// Payment methods
|
|
239
|
+
paymentMethods,
|
|
240
|
+
|
|
241
|
+
// Cart
|
|
242
|
+
cart,
|
|
243
|
+
cartLoading,
|
|
244
|
+
cartError,
|
|
245
|
+
fetchCart,
|
|
246
|
+
addItem,
|
|
247
|
+
removeItem,
|
|
248
|
+
clearCart,
|
|
249
|
+
hasItems,
|
|
250
|
+
hasDiscount,
|
|
251
|
+
cartTotal,
|
|
252
|
+
cartOriginalTotal,
|
|
253
|
+
|
|
254
|
+
// Coupon
|
|
255
|
+
couponCode,
|
|
256
|
+
couponLoading,
|
|
257
|
+
couponError,
|
|
258
|
+
couponInfo,
|
|
259
|
+
applyCoupon,
|
|
260
|
+
removeCoupon,
|
|
261
|
+
|
|
262
|
+
// Guest
|
|
263
|
+
guestEmail,
|
|
264
|
+
emailError,
|
|
265
|
+
|
|
266
|
+
// Terms
|
|
267
|
+
termsAccepted,
|
|
268
|
+
|
|
269
|
+
// Checkout
|
|
270
|
+
checkout,
|
|
271
|
+
checkoutLoading,
|
|
272
|
+
checkoutResponse,
|
|
273
|
+
checkoutError,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return instance;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type MgCheckoutInstance = ReturnType<typeof useMgCheckout>;
|
|
280
|
+
|
|
281
|
+
export function provideMgCheckout(instance: MgCheckoutInstance) {
|
|
282
|
+
provide(CHECKOUT_KEY, instance);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function injectMgCheckout(): MgCheckoutInstance | undefined {
|
|
286
|
+
return inject<MgCheckoutInstance>(CHECKOUT_KEY);
|
|
287
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
getAllNotifications,
|
|
4
|
+
markNotificationAsRead,
|
|
5
|
+
markAllNotificationsAsRead,
|
|
6
|
+
deleteNotification,
|
|
7
|
+
} from "../services/notificationsService";
|
|
8
|
+
import type { MgNotification } from "../types";
|
|
9
|
+
|
|
10
|
+
export function useMgUserNotifications() {
|
|
11
|
+
const notifications = ref<MgNotification[]>([]);
|
|
12
|
+
const error = ref<string | null>(null);
|
|
13
|
+
const loading = ref<boolean>(false);
|
|
14
|
+
const totalNotifications = ref<number>(0);
|
|
15
|
+
const page = ref<number>(1);
|
|
16
|
+
const notificationCount = ref(0);
|
|
17
|
+
|
|
18
|
+
const getSystemId = () => {
|
|
19
|
+
const config = useRuntimeConfig();
|
|
20
|
+
return config.public.mgSharedUi?.systemId || import.meta.env.VITE_SYSTEM_ID;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getNotifications = async (p: number = 1) => {
|
|
24
|
+
loading.value = true;
|
|
25
|
+
try {
|
|
26
|
+
if (p === 1) {
|
|
27
|
+
notifications.value = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const response = await getAllNotifications({
|
|
31
|
+
"filter[mg_network_system_id]": getSystemId(),
|
|
32
|
+
sort: "created_at",
|
|
33
|
+
order: "desc",
|
|
34
|
+
per_page: 6,
|
|
35
|
+
page: p,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
totalNotifications.value = response.data.meta.pagination.total;
|
|
39
|
+
page.value = response.data.meta.pagination.current_page;
|
|
40
|
+
notifications.value.push(
|
|
41
|
+
...response.data.data.map((notification: any) => ({
|
|
42
|
+
notification_id: notification.data.notification_id || notification.id || null,
|
|
43
|
+
type: notification.mg_network_system_id,
|
|
44
|
+
object: notification.data.object || null,
|
|
45
|
+
id: notification.id || null,
|
|
46
|
+
slug: notification.data.slug || null,
|
|
47
|
+
url: notification.data.url || null,
|
|
48
|
+
title: notification.data.title,
|
|
49
|
+
message: notification.data.message,
|
|
50
|
+
image: notification.data.image,
|
|
51
|
+
read_at: notification.read_at,
|
|
52
|
+
created_at: notification.created_at_diff,
|
|
53
|
+
})),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
getNotificationsUnreadCount();
|
|
57
|
+
} catch {
|
|
58
|
+
error.value = "Failed to fetch notifications";
|
|
59
|
+
} finally {
|
|
60
|
+
loading.value = false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getNotificationsUnreadCount = async () => {
|
|
65
|
+
const response = await getAllNotifications({
|
|
66
|
+
"filter[mg_network_system_id]": getSystemId(),
|
|
67
|
+
"filter[read]": 0,
|
|
68
|
+
total: 1,
|
|
69
|
+
});
|
|
70
|
+
notificationCount.value = response.data.total;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const markAsRead = async (notificationId: string) => {
|
|
74
|
+
try {
|
|
75
|
+
await markNotificationAsRead(notificationId);
|
|
76
|
+
notifications.value = notifications.value.map((notification: any) =>
|
|
77
|
+
notification.id === notificationId
|
|
78
|
+
? { ...notification, read_at: new Date().toISOString() }
|
|
79
|
+
: notification,
|
|
80
|
+
);
|
|
81
|
+
getNotificationsUnreadCount();
|
|
82
|
+
} catch {
|
|
83
|
+
error.value = "Failed to mark notification as read";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const readAll = async () => {
|
|
88
|
+
try {
|
|
89
|
+
await markAllNotificationsAsRead();
|
|
90
|
+
notificationCount.value = 0;
|
|
91
|
+
notifications.value = notifications.value.map((notification) => ({
|
|
92
|
+
...notification,
|
|
93
|
+
read_at: new Date().toISOString(),
|
|
94
|
+
}));
|
|
95
|
+
} catch {
|
|
96
|
+
error.value = "Failed to mark all notifications as read";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const deleteNotificationFunc = async (notificationId: string) => {
|
|
101
|
+
try {
|
|
102
|
+
await deleteNotification(notificationId);
|
|
103
|
+
notifications.value = notifications.value.filter((notification: any) => notification.id !== notificationId);
|
|
104
|
+
getNotificationsUnreadCount();
|
|
105
|
+
} catch {
|
|
106
|
+
error.value = "Failed to delete notification";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
notifications,
|
|
112
|
+
error,
|
|
113
|
+
loading,
|
|
114
|
+
getNotifications,
|
|
115
|
+
markAsRead,
|
|
116
|
+
readAll,
|
|
117
|
+
totalNotifications,
|
|
118
|
+
page,
|
|
119
|
+
notificationCount,
|
|
120
|
+
deleteNotification: deleteNotificationFunc,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ref, computed } from "vue";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
3
|
+
|
|
4
|
+
export interface PaymentMethod {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function usePaymentMethods(httpService: AxiosInstance) {
|
|
10
|
+
const methods = ref<PaymentMethod[]>([]);
|
|
11
|
+
const selectedMethod = ref<string>("");
|
|
12
|
+
const loading = ref(false);
|
|
13
|
+
const error = ref<string | null>(null);
|
|
14
|
+
|
|
15
|
+
async function fetchMethods(
|
|
16
|
+
context: "checkout" | "subscription" = "checkout"
|
|
17
|
+
) {
|
|
18
|
+
loading.value = true;
|
|
19
|
+
error.value = null;
|
|
20
|
+
try {
|
|
21
|
+
const endpoint =
|
|
22
|
+
context === "subscription"
|
|
23
|
+
? "/payment-methods/subscriptions"
|
|
24
|
+
: "/payment-methods";
|
|
25
|
+
const res = await httpService.get(endpoint);
|
|
26
|
+
methods.value = res.data?.data ?? [];
|
|
27
|
+
|
|
28
|
+
// Auto-select if only one method available
|
|
29
|
+
if (methods.value.length === 1) {
|
|
30
|
+
selectedMethod.value = methods.value[0].name.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
// Fallback: if endpoint doesn't exist yet (404), provide default methods
|
|
34
|
+
console.warn("[usePaymentMethods] Endpoint not available, using fallback:", e?.response?.status);
|
|
35
|
+
methods.value = [
|
|
36
|
+
{ id: 0, name: "paypal" },
|
|
37
|
+
{ id: 1, name: "Stripe" },
|
|
38
|
+
];
|
|
39
|
+
if (methods.value.length > 0 && !selectedMethod.value) {
|
|
40
|
+
selectedMethod.value = methods.value[0].name.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
error.value = null;
|
|
43
|
+
} finally {
|
|
44
|
+
loading.value = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isPix = computed(
|
|
49
|
+
() => selectedMethod.value.toLowerCase() === "mercadopago"
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const isStripe = computed(
|
|
53
|
+
() => selectedMethod.value.toLowerCase() === "stripe"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const isPaypal = computed(
|
|
57
|
+
() => selectedMethod.value.toLowerCase() === "paypal"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
function selectMethod(name: string) {
|
|
61
|
+
selectedMethod.value = name.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
methods,
|
|
66
|
+
selectedMethod,
|
|
67
|
+
loading,
|
|
68
|
+
error,
|
|
69
|
+
fetchMethods,
|
|
70
|
+
selectMethod,
|
|
71
|
+
isPix,
|
|
72
|
+
isStripe,
|
|
73
|
+
isPaypal,
|
|
74
|
+
};
|
|
75
|
+
}
|