@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.
Files changed (87) hide show
  1. package/README.md +283 -0
  2. package/components/PressKit/AssetGallery.vue +349 -0
  3. package/components/PressKit/Awards.vue +100 -0
  4. package/components/PressKit/Credits.vue +78 -0
  5. package/components/PressKit/FactSheet.vue +204 -0
  6. package/components/PressKit/Hero.vue +143 -0
  7. package/components/PressKit/Quotes.vue +80 -0
  8. package/components/PressKit/VideoPlayer.vue +134 -0
  9. package/components/checkout/MgCartItemList.vue +214 -0
  10. package/components/checkout/MgCartSummary.vue +204 -0
  11. package/components/checkout/MgCheckoutSidebar.vue +230 -0
  12. package/components/checkout/MgGuestEmailForm.vue +97 -0
  13. package/components/checkout/MgPaymentMethodSelector.vue +162 -0
  14. package/components/checkout/MgPixQRCode.vue +222 -0
  15. package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
  16. package/components/indie-wall/MuralCanvas.vue +481 -0
  17. package/components/indie-wall/StepBlock.vue +314 -0
  18. package/components/indie-wall/StepCustomize.vue +530 -0
  19. package/components/indie-wall/StepGoal.vue +169 -0
  20. package/components/indie-wall/StepPackage.vue +145 -0
  21. package/components/indie-wall/StepPay.vue +209 -0
  22. package/components/indie-wall/SupportStepper.vue +372 -0
  23. package/components/invoices/MgInvoiceDownload.vue +50 -0
  24. package/components/pricing/MgBillingToggle.vue +74 -0
  25. package/components/pricing/MgPricingCard.vue +245 -0
  26. package/components/ui/Header/MgMessageCard.vue +147 -0
  27. package/components/ui/Header/MgMessageModal.vue +414 -0
  28. package/components/ui/Header/MgNotificationCard.vue +200 -0
  29. package/components/ui/Header/MgNotificationsModal.vue +125 -0
  30. package/components/ui/MgAnnouncementBanner.vue +147 -0
  31. package/components/ui/MgBanners.vue +23 -0
  32. package/components/ui/MgHeaderComponent.vue +283 -0
  33. package/components/ui/MgHeaderUIConfig.vue +225 -0
  34. package/components/ui/MgHeaderUIUser.vue +301 -0
  35. package/components/ui/MgLoginModal.vue +156 -0
  36. package/components/ui/MgPromotionBanner.vue +185 -0
  37. package/composables/useLogout.ts +42 -0
  38. package/composables/useMgCheckout.ts +287 -0
  39. package/composables/useMgUserNotifications.ts +122 -0
  40. package/composables/usePaymentMethods.ts +75 -0
  41. package/composables/useSubscription.ts +163 -0
  42. package/middleware/auth.global.ts +40 -0
  43. package/nuxt.config.ts +31 -0
  44. package/package.json +40 -0
  45. package/pages/[slug]/index.vue +112 -0
  46. package/pages/about.vue +133 -0
  47. package/pages/blog.vue +430 -0
  48. package/pages/careers.vue +329 -0
  49. package/pages/contact.vue +339 -0
  50. package/pages/faq.vue +317 -0
  51. package/pages/health-check.vue +20 -0
  52. package/pages/icons.vue +58 -0
  53. package/pages/magazine/[slug].vue +209 -0
  54. package/pages/magazine/index.vue +267 -0
  55. package/pages/media-kit/[slug].vue +625 -0
  56. package/pages/mural/[slug].vue +1058 -0
  57. package/pages/partners.vue +290 -0
  58. package/pages/press.vue +237 -0
  59. package/pages/presskit/[slug].vue +191 -0
  60. package/pages/roadmap.vue +355 -0
  61. package/pages/status.vue +199 -0
  62. package/pages/team.vue +266 -0
  63. package/pages/wall/[slug].vue +11 -0
  64. package/plugins/auth.client.ts +17 -0
  65. package/plugins/echo.client.ts +132 -0
  66. package/services/authService.ts +95 -0
  67. package/services/chatService.ts +53 -0
  68. package/services/contactService.ts +35 -0
  69. package/services/documentService.ts +16 -0
  70. package/services/httpService.ts +95 -0
  71. package/services/indieWallService.ts +174 -0
  72. package/services/institutionalService.ts +248 -0
  73. package/services/mediaKitService.ts +51 -0
  74. package/services/notificationsService.ts +20 -0
  75. package/services/pressKitService.ts +55 -0
  76. package/stores/announcement.ts +129 -0
  77. package/stores/auth.ts +86 -0
  78. package/stores/chat.ts +150 -0
  79. package/stores/contact.ts +28 -0
  80. package/stores/document.ts +27 -0
  81. package/stores/index.ts +34 -0
  82. package/stores/institutional.ts +231 -0
  83. package/stores/login.ts +27 -0
  84. package/stores/notifications.ts +133 -0
  85. package/stores/promotion.ts +154 -0
  86. package/types/index.ts +135 -0
  87. 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
+ >&times;</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
+ }