@mohasinac/appkit 2.7.9 → 2.7.11
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/dist/_internal/shared/config/index.d.ts +1 -1
- package/dist/_internal/shared/config/schema.d.ts +95 -1
- package/dist/client-entry.d.ts +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/configs/next.js +17 -1
- package/dist/features/account/components/UserOffersPanel.js +17 -5
- package/dist/features/auctions/components/AuctionDetailPageView.js +17 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +10 -5
- package/dist/features/blog/components/BlogIndexListing.js +8 -1
- package/dist/features/categories/components/BundleBuyNowCta.js +13 -6
- package/dist/features/categories/components/CategoriesIndexListing.js +9 -2
- package/dist/features/events/components/EventPollWidget.js +7 -1
- package/dist/features/events/components/EventsIndexListing.js +8 -1
- package/dist/features/events/components/SpinWheelView.js +12 -5
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +10 -3
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +11 -7
- package/dist/features/products/components/AuctionsIndexListing.js +31 -2
- package/dist/features/products/components/MakeOfferButton.js +16 -8
- package/dist/features/products/components/PrizeDrawsIndexListing.js +5 -2
- package/dist/features/products/components/PrizeRevealModal.js +18 -5
- package/dist/features/products/components/ProductDetailPageView.js +23 -1
- package/dist/features/products/components/ProductsIndexListing.js +12 -8
- package/dist/features/reviews/components/ReviewsIndexListing.js +8 -1
- package/dist/features/seller/components/SellerOffersPanel.js +17 -5
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -1
- package/dist/features/stores/components/StoresIndexListing.js +25 -11
- package/dist/features/wishlist/components/WishlistToggleButton.js +26 -12
- package/dist/http/api-handler.js +7 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +8 -6
- package/dist/ui/components/Button.style.css +8 -5
- package/dist/ui/components/ListingLayout.style.css +19 -9
- package/dist/ui/components/ListingToolbar.d.ts +7 -4
- package/dist/ui/components/ListingToolbar.js +4 -3
- package/dist/ui/components/LoginRequiredModal.d.ts +9 -0
- package/dist/ui/components/LoginRequiredModal.js +11 -0
- package/dist/ui/components/SiteLogo.js +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/auth-error.d.ts +8 -0
- package/dist/utils/auth-error.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { AppkitConfig, AppkitSmokeRoute, AppkitThemeProbeRoute, AppkitAuthFixture } from "./schema";
|
|
1
|
+
export type { AppkitConfig, AppkitSmokeRoute, AppkitThemeProbeRoute, AppkitAuthFixture, AppkitFirebaseConfig, AppkitFirebaseExtensions, AppkitVercelConfig, AppkitBrandConfig, AppkitSeoConfig, AppkitI18nConfig, AppkitImagePattern, FirestoreIndex, FirestoreIndexField, FirestoreFieldOverride, } from "./schema";
|
|
@@ -39,15 +39,99 @@ export interface AppkitAuthFixture {
|
|
|
39
39
|
/** Cookie header value, e.g. "session=FIXTURE_TOKEN" */
|
|
40
40
|
cookie: string;
|
|
41
41
|
}
|
|
42
|
+
export interface FirestoreIndexField {
|
|
43
|
+
fieldPath: string;
|
|
44
|
+
order?: "ASCENDING" | "DESCENDING";
|
|
45
|
+
arrayConfig?: "CONTAINS";
|
|
46
|
+
}
|
|
47
|
+
export interface FirestoreIndex {
|
|
48
|
+
collectionGroup: string;
|
|
49
|
+
queryScope: "COLLECTION" | "COLLECTION_GROUP";
|
|
50
|
+
fields: FirestoreIndexField[];
|
|
51
|
+
}
|
|
52
|
+
export interface FirestoreFieldOverride {
|
|
53
|
+
collectionGroup: string;
|
|
54
|
+
fieldPath: string;
|
|
55
|
+
indexes: Array<{
|
|
56
|
+
order?: "ASCENDING" | "DESCENDING";
|
|
57
|
+
arrayConfig?: "CONTAINS";
|
|
58
|
+
queryScope: "COLLECTION" | "COLLECTION_GROUP";
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
/** Consumer-specific Firebase extensions merged on top of the appkit base by firebase-merge.mjs. */
|
|
62
|
+
export interface AppkitFirebaseExtensions {
|
|
63
|
+
/** Additional Firestore composite indexes merged on top of the appkit base (deduplicated). */
|
|
64
|
+
indexes?: FirestoreIndex[];
|
|
65
|
+
/** Additional Firestore field overrides appended to the base list. */
|
|
66
|
+
fieldOverrides?: FirestoreFieldOverride[];
|
|
67
|
+
/** Additional RTDB rule paths deep-merged into the base rules object. */
|
|
68
|
+
database?: Record<string, unknown>;
|
|
69
|
+
/** Raw Firestore security rules block injected inside the top-level match block. */
|
|
70
|
+
firestoreRules?: string;
|
|
71
|
+
/** Raw Storage security rules block injected inside the bucket match block. */
|
|
72
|
+
storageRules?: string;
|
|
73
|
+
}
|
|
42
74
|
export interface AppkitFirebaseConfig {
|
|
43
75
|
/** Firebase project ID */
|
|
44
76
|
projectId: string;
|
|
45
|
-
/** Path to firestore.indexes.json base file */
|
|
77
|
+
/** Path to firestore.indexes.json base file (default: appkit/firebase/base/firestore.indexes.json) */
|
|
46
78
|
indexesPath?: string;
|
|
47
79
|
/** Collections to include in reset (undefined = all) */
|
|
48
80
|
resetCollections?: string[];
|
|
49
81
|
/** Firebase Functions region */
|
|
50
82
|
functionsRegion?: string;
|
|
83
|
+
/** Consumer-specific extensions merged on top of the appkit base by `npm run firebase:generate`. */
|
|
84
|
+
extensions?: AppkitFirebaseExtensions;
|
|
85
|
+
}
|
|
86
|
+
export interface AppkitBrandConfig {
|
|
87
|
+
/** Display name of the brand, e.g. "LetItRip" */
|
|
88
|
+
name: string;
|
|
89
|
+
/** Short / abbreviated name, e.g. "LT" */
|
|
90
|
+
shortName?: string;
|
|
91
|
+
/** One-line brand description used in meta tags and footers */
|
|
92
|
+
description?: string;
|
|
93
|
+
/** Tagline shown in footer / about sections */
|
|
94
|
+
madeInText?: string;
|
|
95
|
+
socialUrls?: {
|
|
96
|
+
instagram?: string;
|
|
97
|
+
twitter?: string;
|
|
98
|
+
whatsapp?: string;
|
|
99
|
+
youtube?: string;
|
|
100
|
+
facebook?: string;
|
|
101
|
+
linkedin?: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export interface AppkitSeoConfig {
|
|
105
|
+
/** Canonical site URL, e.g. "https://letitrip.in" */
|
|
106
|
+
siteUrl: string;
|
|
107
|
+
/** Default page <title> */
|
|
108
|
+
defaultTitle?: string;
|
|
109
|
+
/** Default meta description */
|
|
110
|
+
defaultDescription?: string;
|
|
111
|
+
/** Default OG image path (relative to public/) */
|
|
112
|
+
defaultImage?: string;
|
|
113
|
+
/** OG site name */
|
|
114
|
+
siteName?: string;
|
|
115
|
+
/** Twitter/X handle including @, e.g. "@letitrip" */
|
|
116
|
+
twitterHandle?: string;
|
|
117
|
+
/** BCP 47 locale tag for OG/schema.org, e.g. "en-IN" */
|
|
118
|
+
locale?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface AppkitI18nConfig {
|
|
121
|
+
/** next-intl localePrefix strategy (default: "never") */
|
|
122
|
+
localePrefix?: "never" | "always" | "as-needed";
|
|
123
|
+
/**
|
|
124
|
+
* Set false to suppress the Set-Cookie: Next-Locale header, which forces
|
|
125
|
+
* cache-control: private and breaks Vercel ISR. Disable when running a single
|
|
126
|
+
* locale (default: false).
|
|
127
|
+
*/
|
|
128
|
+
enableLocaleCookie?: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface AppkitImagePattern {
|
|
131
|
+
protocol?: "http" | "https";
|
|
132
|
+
hostname: string;
|
|
133
|
+
port?: string;
|
|
134
|
+
pathname?: string;
|
|
51
135
|
}
|
|
52
136
|
export interface AppkitVercelConfig {
|
|
53
137
|
/** Vercel project ID */
|
|
@@ -56,12 +140,22 @@ export interface AppkitVercelConfig {
|
|
|
56
140
|
team?: string;
|
|
57
141
|
/** Path to .env file to sync */
|
|
58
142
|
envFile?: string;
|
|
143
|
+
/** Deployment regions, e.g. ["bom1"] */
|
|
144
|
+
regions?: string[];
|
|
59
145
|
}
|
|
60
146
|
export interface AppkitConfig {
|
|
61
147
|
/** Base URL for smoke tests and theme probes (default: http://localhost:3000) */
|
|
62
148
|
baseUrl?: string;
|
|
63
149
|
/** Supported locales (default: ["en"]) */
|
|
64
150
|
locales?: string[];
|
|
151
|
+
/** Brand identity — name, social URLs, taglines */
|
|
152
|
+
brand?: AppkitBrandConfig;
|
|
153
|
+
/** SEO defaults — siteUrl, defaultTitle, OG image, Twitter handle */
|
|
154
|
+
seo?: AppkitSeoConfig;
|
|
155
|
+
/** i18n / next-intl routing options */
|
|
156
|
+
i18n?: AppkitI18nConfig;
|
|
157
|
+
/** next/image remotePatterns for external image hosts (demo / seed data) */
|
|
158
|
+
externalImagePatterns?: AppkitImagePattern[];
|
|
65
159
|
/** Route configuration for smoke tests and theme probing */
|
|
66
160
|
routes?: {
|
|
67
161
|
/** Routes to SSR-verify: must return 200 + expected strings in initial HTML */
|
package/dist/client-entry.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export type { AppkitLabelSet } from "./_internal/client/i18n/LabelsProvider";
|
|
|
18
18
|
export { AppShell, DashboardScaffold } from "./_internal/client/scaffolds/index";
|
|
19
19
|
export type { AppShellProps, AppShellRenderContext, DashboardScaffoldProps, DashboardScaffoldRenderContext, } from "./_internal/client/scaffolds/index";
|
|
20
20
|
export { toClient, clientInitial } from "./_internal/shared/serialization/index";
|
|
21
|
-
export type { AppkitConfig } from "./_internal/shared/config/schema";
|
|
21
|
+
export type { AppkitConfig, AppkitFirebaseConfig, AppkitFirebaseExtensions, AppkitVercelConfig, AppkitBrandConfig, AppkitSeoConfig, AppkitI18nConfig, AppkitImagePattern, FirestoreIndex, FirestoreIndexField, FirestoreFieldOverride, } from "./_internal/shared/config/schema";
|
|
22
22
|
export { SEMANTIC_COLORS, SEMANTIC_COLORS_DARK, SEMANTIC_RADIUS, SEMANTIC_SHADOWS, SEMANTIC_Z_INDEX, MOTION_TOKENS, BREAKPOINTS, PLATFORM_LIMITS, } from "./_internal/shared/tokens/index";
|
|
23
23
|
export type { Responsive, Breakpoint, SemanticColor } from "./_internal/shared/tokens/index";
|
package/dist/client.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export { Modal } from "./ui/components/Modal";
|
|
|
8
8
|
export { SideDrawer } from "./ui/components/SideDrawer";
|
|
9
9
|
export { SideModal } from "./ui/components/SideModal";
|
|
10
10
|
export { UnsavedChangesModal } from "./ui/components/UnsavedChangesModal";
|
|
11
|
+
export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
|
|
12
|
+
export type { LoginRequiredModalProps } from "./ui/components/LoginRequiredModal";
|
|
13
|
+
export { isAuthError } from "./utils/auth-error";
|
|
11
14
|
export { useToast } from "./ui/components/Toast";
|
|
12
15
|
export { useAuth } from "./react/contexts/SessionContext";
|
|
13
16
|
export { useCamera } from "./react/hooks/useCamera";
|
package/dist/client.js
CHANGED
|
@@ -27,6 +27,11 @@ export { SideModal } from "./ui/components/SideModal";
|
|
|
27
27
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
28
28
|
// UnsavedChangesModal - Component for unsaved changes modal.
|
|
29
29
|
export { UnsavedChangesModal } from "./ui/components/UnsavedChangesModal";
|
|
30
|
+
// [CLIENT-ONLY]-Cannot run in SSR mode — uses browser-only APIs (window.location).
|
|
31
|
+
// LoginRequiredModal - Modal prompting unauthenticated users to log in.
|
|
32
|
+
export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
|
|
33
|
+
// isAuthError - Detects auth/authorization errors from server actions or fetch responses.
|
|
34
|
+
export { isAuthError } from "./utils/auth-error";
|
|
30
35
|
// [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
|
|
31
36
|
// useToast - React hook for use toast.
|
|
32
37
|
export { useToast } from "./ui/components/Toast";
|
package/dist/configs/next.js
CHANGED
|
@@ -65,7 +65,7 @@ const FIREBASE_EXTERNAL_PACKAGES = [
|
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
67
|
export function defineNextConfig(override = {}) {
|
|
68
|
-
const { serverExternalPackages: consumerExternal = [], experimental: consumerExperimental = {}, webpack: consumerWebpack, outputFileTracingIncludes: consumerOutputFileTracingIncludes = {}, ...rest } = override;
|
|
68
|
+
const { serverExternalPackages: consumerExternal = [], experimental: consumerExperimental = {}, webpack: consumerWebpack, outputFileTracingIncludes: consumerOutputFileTracingIncludes = {}, turbopack: consumerTurbopack = {}, ...rest } = override;
|
|
69
69
|
const mergedExternal = [
|
|
70
70
|
...FIREBASE_EXTERNAL_PACKAGES,
|
|
71
71
|
...consumerExternal.filter((p) => !FIREBASE_EXTERNAL_PACKAGES.includes(p)),
|
|
@@ -177,6 +177,8 @@ export function defineNextConfig(override = {}) {
|
|
|
177
177
|
// disagree on which apps exist, causing "No Firebase App '[DEFAULT]'" errors.
|
|
178
178
|
// Pinning every firebase/* import to the root copy ensures the singleton
|
|
179
179
|
// registry is shared regardless of which file imports firebase.
|
|
180
|
+
// NOTE: This fixes webpack (dev --webpack) only. Turbopack (next build) uses
|
|
181
|
+
// turbopack.resolveAlias in the returned config object — see below.
|
|
180
182
|
const _firebaseRoot = path.resolve(process.cwd(), "node_modules", "firebase");
|
|
181
183
|
config.resolve.alias = {
|
|
182
184
|
...(config.resolve.alias ?? {}),
|
|
@@ -265,6 +267,19 @@ export function defineNextConfig(override = {}) {
|
|
|
265
267
|
...consumerRemotePatterns.filter((p) => !defaultRemotePatterns.some((d) => d.hostname ===
|
|
266
268
|
p.hostname)),
|
|
267
269
|
];
|
|
270
|
+
// Turbopack (used by `next build`) has its own alias system separate from
|
|
271
|
+
// webpack. Mirror the firebase deduplication alias so prod builds share the
|
|
272
|
+
// same Firebase app registry as dev builds.
|
|
273
|
+
const firebaseRoot = path.resolve(process.cwd(), "node_modules", "firebase");
|
|
274
|
+
const consumerTurbopackResolved = consumerTurbopack;
|
|
275
|
+
const consumerTurbopackAlias = (consumerTurbopackResolved.resolveAlias ?? {});
|
|
276
|
+
const mergedTurbopack = {
|
|
277
|
+
...consumerTurbopackResolved,
|
|
278
|
+
resolveAlias: {
|
|
279
|
+
...consumerTurbopackAlias,
|
|
280
|
+
firebase: firebaseRoot,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
268
283
|
return {
|
|
269
284
|
images: {
|
|
270
285
|
...consumerImages,
|
|
@@ -275,5 +290,6 @@ export function defineNextConfig(override = {}) {
|
|
|
275
290
|
experimental: mergedExperimental,
|
|
276
291
|
outputFileTracingIncludes: mergedOutputFileTracingIncludes,
|
|
277
292
|
webpack: mergedWebpack,
|
|
293
|
+
turbopack: mergedTurbopack,
|
|
278
294
|
};
|
|
279
295
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState, useTransition } from "react";
|
|
4
|
-
import { Alert, Badge, Button, Div, Heading, Spinner, Text } from "../../../ui";
|
|
4
|
+
import { Alert, Badge, Button, Div, Heading, LoginRequiredModal, Spinner, Text } from "../../../ui";
|
|
5
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
5
6
|
function formatRupees(amount) {
|
|
6
7
|
if (amount === undefined || amount === null)
|
|
7
8
|
return "—";
|
|
@@ -32,7 +33,7 @@ function statusVariant(status) {
|
|
|
32
33
|
default: return "default";
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
function BuyerOfferCard({ offer, onAcceptCounter, onWithdraw, onCheckout, onUpdate }) {
|
|
36
|
+
function BuyerOfferCard({ offer, onAcceptCounter, onWithdraw, onCheckout, onUpdate, onNeedsLogin }) {
|
|
36
37
|
const [error, setError] = useState("");
|
|
37
38
|
const [isPending, startTransition] = useTransition();
|
|
38
39
|
const [confirming, setConfirming] = useState(null);
|
|
@@ -45,7 +46,12 @@ function BuyerOfferCard({ offer, onAcceptCounter, onWithdraw, onCheckout, onUpda
|
|
|
45
46
|
setConfirming(null);
|
|
46
47
|
}
|
|
47
48
|
catch (err) {
|
|
48
|
-
|
|
49
|
+
if (isAuthError(err)) {
|
|
50
|
+
onNeedsLogin();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
});
|
|
51
57
|
}
|
|
@@ -55,13 +61,19 @@ export function UserOffersPanel({ fetchEndpoint = "/api/user/offers", onAcceptCo
|
|
|
55
61
|
const [offers, setOffers] = useState([]);
|
|
56
62
|
const [loading, setLoading] = useState(true);
|
|
57
63
|
const [fetchError, setFetchError] = useState("");
|
|
64
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
58
65
|
const loadOffers = useCallback(async () => {
|
|
59
66
|
setLoading(true);
|
|
60
67
|
setFetchError("");
|
|
61
68
|
try {
|
|
62
69
|
const res = await fetch(fetchEndpoint);
|
|
63
|
-
if (!res.ok)
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
if (res.status === 401 || res.status === 403) {
|
|
72
|
+
setShowLoginModal(true);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
64
75
|
throw new Error(`Error ${res.status}`);
|
|
76
|
+
}
|
|
65
77
|
const json = (await res.json());
|
|
66
78
|
const items = Array.isArray(json) ? json : (json.items ?? []);
|
|
67
79
|
setOffers(items);
|
|
@@ -77,5 +89,5 @@ export function UserOffersPanel({ fetchEndpoint = "/api/user/offers", onAcceptCo
|
|
|
77
89
|
function handleUpdate(id, patch) {
|
|
78
90
|
setOffers((prev) => prev.map((o) => (o.id === id ? { ...o, ...patch } : o)));
|
|
79
91
|
}
|
|
80
|
-
return (_jsxs(Div, { className: `space-y-4 ${className}`, children: [_jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Heading, { level: 2, className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "My Offers" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: loadOffers, disabled: loading, className: "border border-zinc-300 dark:border-zinc-600 text-xs", children: loading ? "Refreshing…" : "Refresh" })] }), fetchError && _jsx(Alert, { variant: "error", children: _jsx(Text, { className: "text-sm", children: fetchError }) }), loading && (_jsx(Div, { className: "flex justify-center py-12", children: _jsx(Spinner, { size: "lg" }) })), !loading && offers.length === 0 && (_jsx(Div, { className: "text-center py-12", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500 text-sm", children: "No offers yet" }) })), !loading && offers.length > 0 && (_jsx(Div, { className: "space-y-3", children: offers.map((offer) => (_jsx(BuyerOfferCard, { offer: offer, onAcceptCounter: onAcceptCounter, onWithdraw: onWithdraw, onCheckout: onCheckout, onUpdate: handleUpdate }, offer.id))) }))] }));
|
|
92
|
+
return (_jsxs(Div, { className: `space-y-4 ${className}`, children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to manage your offers. Please log in or create an account to continue." }), _jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Heading, { level: 2, className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: "My Offers" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: loadOffers, disabled: loading, className: "border border-zinc-300 dark:border-zinc-600 text-xs", children: loading ? "Refreshing…" : "Refresh" })] }), fetchError && _jsx(Alert, { variant: "error", children: _jsx(Text, { className: "text-sm", children: fetchError }) }), loading && (_jsx(Div, { className: "flex justify-center py-12", children: _jsx(Spinner, { size: "lg" }) })), !loading && offers.length === 0 && (_jsx(Div, { className: "text-center py-12", children: _jsx(Text, { className: "text-zinc-400 dark:text-zinc-500 text-sm", children: "No offers yet" }) })), !loading && offers.length > 0 && (_jsx(Div, { className: "space-y-3", children: offers.map((offer) => (_jsx(BuyerOfferCard, { offer: offer, onAcceptCounter: onAcceptCounter, onWithdraw: onWithdraw, onCheckout: onCheckout, onUpdate: handleUpdate, onNeedsLogin: () => setShowLoginModal(true) }, offer.id))) }))] }));
|
|
81
93
|
}
|
|
@@ -137,8 +137,24 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, pr
|
|
|
137
137
|
}));
|
|
138
138
|
return _jsx(CollapsibleBidHistory, { bids: bids, currency: currency });
|
|
139
139
|
}, renderRelated: () => {
|
|
140
|
+
const now = new Date();
|
|
140
141
|
const related = relatedDocs
|
|
141
|
-
.filter((r) =>
|
|
142
|
+
.filter((r) => {
|
|
143
|
+
if (r.id === product.id || r.listingType !== "auction")
|
|
144
|
+
return false;
|
|
145
|
+
const s = r.status;
|
|
146
|
+
if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
|
|
147
|
+
return false;
|
|
148
|
+
if (r.isSold === true)
|
|
149
|
+
return false;
|
|
150
|
+
const end = r.auctionEndDate;
|
|
151
|
+
if (!end)
|
|
152
|
+
return true;
|
|
153
|
+
const endDate = typeof end.toDate === "function"
|
|
154
|
+
? end.toDate()
|
|
155
|
+
: end instanceof Date ? end : new Date(String(end));
|
|
156
|
+
return endDate > now;
|
|
157
|
+
})
|
|
142
158
|
.slice(0, 4)
|
|
143
159
|
.map((r) => ({
|
|
144
160
|
id: String(r.id ?? ""),
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
4
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
5
|
-
import {
|
|
5
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
6
|
+
import { Button, Div, Input, LoginRequiredModal, Row, Span, Stack, Text } from "../../../ui";
|
|
6
7
|
export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidCount, tags = [], onPlaceBid, }) {
|
|
7
8
|
const minBid = currentBid + minBidIncrement;
|
|
8
9
|
const [bidAmount, setBidAmount] = useState(String(minBid));
|
|
9
10
|
const [isPending, startTransition] = useTransition();
|
|
10
11
|
const [error, setError] = useState(null);
|
|
11
12
|
const [success, setSuccess] = useState(false);
|
|
13
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
12
14
|
function handleSubmit(e) {
|
|
13
15
|
e.preventDefault();
|
|
14
16
|
const amount = Number(bidAmount);
|
|
@@ -33,11 +35,14 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
|
|
|
33
35
|
setBidAmount(String(amount + minBidIncrement));
|
|
34
36
|
}
|
|
35
37
|
catch (err) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (isAuthError(err)) {
|
|
39
|
+
setShowLoginModal(true);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
setError(err instanceof Error ? err.message : "Failed to place bid. Please try again.");
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
});
|
|
41
46
|
}
|
|
42
|
-
return (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", value: bidAmount, onChange: (e) => setBidAmount(e.target.value), placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, step: minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded || isPending }), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Bid placed successfully!" })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, type: "submit", children: isPending ? "Placing Bid…" : isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }) }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] }));
|
|
47
|
+
return (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to place a bid. Please log in or create an account to continue." }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", value: bidAmount, onChange: (e) => setBidAmount(e.target.value), placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, step: minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded || isPending }), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Bid placed successfully!" })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, type: "submit", children: isPending ? "Placing Bid…" : isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }) }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] }));
|
|
43
48
|
}
|
|
@@ -14,6 +14,13 @@ export function BlogIndexListing({ initialData }) {
|
|
|
14
14
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "-publishedAt" } });
|
|
15
15
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
16
16
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
17
|
+
const [view, setView] = useState(table.get("view") || "grid");
|
|
18
|
+
const handleViewToggle = (next) => {
|
|
19
|
+
if (next === "table")
|
|
20
|
+
return;
|
|
21
|
+
setView(next);
|
|
22
|
+
table.set("view", next);
|
|
23
|
+
};
|
|
17
24
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
18
25
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
19
26
|
const pendingTable = useMemo(() => ({
|
|
@@ -78,5 +85,5 @@ export function BlogIndexListing({ initialData }) {
|
|
|
78
85
|
const commitSearch = useCallback(() => {
|
|
79
86
|
table.set("q", searchInput.trim());
|
|
80
87
|
}, [searchInput, table]);
|
|
81
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-publishedAt", sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); },
|
|
88
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search posts...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-publishedAt", sortOptions: BLOG_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-5 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/4" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" })] })] }, i))) })) : posts.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No posts found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: posts.map((post) => (_jsx(BlogCard, { post: post, href: String(ROUTES.BLOG.ARTICLE(post.slug)) }, post.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(BlogFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
82
89
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* BundleBuyNowCta — direct-checkout CTA for bundle detail pages.
|
|
5
5
|
*
|
|
@@ -8,13 +8,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* No qty input — bundles are always qty=1 per checkout.
|
|
9
9
|
*/
|
|
10
10
|
import { useState, useCallback } from "react";
|
|
11
|
-
import { Button, Stack, Text } from "../../../ui";
|
|
11
|
+
import { Button, LoginRequiredModal, Stack, Text } from "../../../ui";
|
|
12
12
|
import { useToast } from "../../../ui";
|
|
13
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
13
14
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
14
15
|
export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
|
|
15
16
|
const { showToast } = useToast();
|
|
16
17
|
const [submitting, setSubmitting] = useState(false);
|
|
17
18
|
const [error, setError] = useState(null);
|
|
19
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
18
20
|
const handleClick = useCallback(async () => {
|
|
19
21
|
setError(null);
|
|
20
22
|
setSubmitting(true);
|
|
@@ -22,9 +24,14 @@ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
|
|
|
22
24
|
await onBuyNow({ bundleSlug });
|
|
23
25
|
}
|
|
24
26
|
catch (err) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if (isAuthError(err)) {
|
|
28
|
+
setShowLoginModal(true);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const message = err instanceof Error ? err.message : BUNDLE_COPY.detail.ctaErrorFallback;
|
|
32
|
+
setError(message);
|
|
33
|
+
showToast(message, "error");
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
36
|
finally {
|
|
30
37
|
setSubmitting(false);
|
|
@@ -33,5 +40,5 @@ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
|
|
|
33
40
|
if (outOfStock) {
|
|
34
41
|
return (_jsxs(Stack, { gap: "xs", "aria-live": "polite", children: [_jsx(Button, { variant: "primary", disabled: true, "aria-disabled": true, children: BUNDLE_COPY.detail.ctaOutOfStock }), _jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }));
|
|
35
42
|
}
|
|
36
|
-
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", onClick: handleClick, disabled: submitting, "aria-busy": submitting, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] }));
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to purchase this bundle. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", onClick: handleClick, disabled: submitting, "aria-busy": submitting, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
|
|
37
44
|
}
|
|
@@ -19,6 +19,13 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
|
|
|
19
19
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "name" } });
|
|
20
20
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
21
21
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
22
|
+
const [view, setView] = useState(table.get("view") || "grid");
|
|
23
|
+
const handleViewToggle = (next) => {
|
|
24
|
+
if (next === "table")
|
|
25
|
+
return;
|
|
26
|
+
setView(next);
|
|
27
|
+
table.set("view", next);
|
|
28
|
+
};
|
|
22
29
|
const sort = table.get("sort") || "name";
|
|
23
30
|
const page = table.getNumber("page", 1);
|
|
24
31
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
@@ -112,9 +119,9 @@ export function CategoriesIndexListing({ initialData: _, brandsOnly = false }) {
|
|
|
112
119
|
activeTab === tab.key
|
|
113
120
|
? "border-primary text-primary dark:text-primary-400 dark:border-primary-400"
|
|
114
121
|
: "border-transparent text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200",
|
|
115
|
-
].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); },
|
|
122
|
+
].join(" "), children: tab.label }, tab.key))) })), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: activeTab === "brands" || brandsOnly ? "Search brands..." : "Search categories...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: sort, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-[4/3] bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3.5 space-y-2", children: [_jsx("div", { className: "h-3.5 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" })] })] }, i))) })) : categories.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: activeSearch
|
|
116
123
|
? `No ${activeTab === "brands" || brandsOnly ? "brands" : "categories"} matching "${activeSearch}"`
|
|
117
124
|
: activeTab === "brands" || brandsOnly
|
|
118
125
|
? "No brands found"
|
|
119
|
-
: "No categories found" })) : (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: categories.map((category) => (_jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(CategoryFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
126
|
+
: "No categories found" })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: categories.map((category) => (_jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id))) })) : (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4", children: categories.map((category) => (_jsx(CategoryCard, { category: category, href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug)) }, category.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(CategoryFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
120
127
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { useAuth } from "../../../react/contexts/SessionContext";
|
|
5
5
|
import { ROUTES } from "../../../next";
|
|
6
|
+
import { LoginRequiredModal } from "../../../ui";
|
|
6
7
|
export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries, entriesEndpoint, className = "", }) {
|
|
7
8
|
const { user } = useAuth();
|
|
8
9
|
const endpoint = entriesEndpoint ?? `/api/events/${eventId}/entries`;
|
|
@@ -13,6 +14,7 @@ export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries
|
|
|
13
14
|
const [isLoading, setIsLoading] = useState(false);
|
|
14
15
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
15
16
|
const [error, setError] = useState(null);
|
|
17
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
16
18
|
const toggleVote = (id) => {
|
|
17
19
|
if (isMulti) {
|
|
18
20
|
setSelectedVotes((prev) => prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id]);
|
|
@@ -37,6 +39,10 @@ export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries
|
|
|
37
39
|
credentials: "include",
|
|
38
40
|
});
|
|
39
41
|
if (!res.ok) {
|
|
42
|
+
if (res.status === 401 || res.status === 403) {
|
|
43
|
+
setShowLoginModal(true);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
40
46
|
const data = (await res.json().catch(() => ({})));
|
|
41
47
|
throw new Error(data.error ?? "Failed to submit vote");
|
|
42
48
|
}
|
|
@@ -58,5 +64,5 @@ export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries
|
|
|
58
64
|
if (isSubmitted) {
|
|
59
65
|
return (_jsxs("div", { className: `rounded-xl border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 px-6 py-8 text-center space-y-2 ${className}`, "data-section": "eventpollwidget-div-5", children: [_jsx("p", { className: "font-semibold text-green-700 dark:text-green-300", children: "Vote recorded!" }), _jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Thanks for participating." })] }));
|
|
60
66
|
}
|
|
61
|
-
return (_jsxs("div", { className: `space-y-4 ${className}`, "data-section": "eventpollwidget-div-6", children: [_jsx("p", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-200", children: isMulti ? "Select all that apply:" : "Choose one:" }), _jsx("div", { className: "space-y-2", "data-section": "eventpollwidget-div-7", children: pollConfig.options.map((opt) => (_jsxs("label", { className: "flex items-center gap-3 cursor-pointer rounded-lg border border-zinc-200 dark:border-zinc-700 px-4 py-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors", children: [_jsx("input", { type: isMulti ? "checkbox" : "radio", name: `poll-${eventId}`, value: opt.id, checked: selectedVotes.includes(opt.id), onChange: () => toggleVote(opt.id), className: "accent-primary" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: opt.label })] }, opt.id))) }), pollConfig.allowComment && (_jsx("textarea", { value: comment, onChange: (e) => setComment(e.target.value), placeholder: "Add a comment (optional)", rows: 3, className: "w-full rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-800 dark:text-zinc-200 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary" })), error && _jsx("p", { className: "text-sm text-red-500", children: error }), _jsx("button", { type: "button", onClick: handleSubmit, disabled: isLoading || selectedVotes.length === 0, className: "w-full rounded-xl bg-primary px-6 py-3 text-sm font-semibold text-white hover:bg-primary-600 disabled:opacity-60", children: isLoading ? "Submitting…" : "Submit Vote" })] }));
|
|
67
|
+
return (_jsxs("div", { className: `space-y-4 ${className}`, "data-section": "eventpollwidget-div-6", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to vote in this poll. Please log in or create an account to continue." }), _jsx("p", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-200", children: isMulti ? "Select all that apply:" : "Choose one:" }), _jsx("div", { className: "space-y-2", "data-section": "eventpollwidget-div-7", children: pollConfig.options.map((opt) => (_jsxs("label", { className: "flex items-center gap-3 cursor-pointer rounded-lg border border-zinc-200 dark:border-zinc-700 px-4 py-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors", children: [_jsx("input", { type: isMulti ? "checkbox" : "radio", name: `poll-${eventId}`, value: opt.id, checked: selectedVotes.includes(opt.id), onChange: () => toggleVote(opt.id), className: "accent-primary" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: opt.label })] }, opt.id))) }), pollConfig.allowComment && (_jsx("textarea", { value: comment, onChange: (e) => setComment(e.target.value), placeholder: "Add a comment (optional)", rows: 3, className: "w-full rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-800 dark:text-zinc-200 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary" })), error && _jsx("p", { className: "text-sm text-red-500", children: error }), _jsx("button", { type: "button", onClick: handleSubmit, disabled: isLoading || selectedVotes.length === 0, className: "w-full rounded-xl bg-primary px-6 py-3 text-sm font-semibold text-white hover:bg-primary-600 disabled:opacity-60", children: isLoading ? "Submitting…" : "Submit Vote" })] }));
|
|
62
68
|
}
|
|
@@ -13,6 +13,13 @@ export function EventsIndexListing({ initialData }) {
|
|
|
13
13
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: "startsAt" } });
|
|
14
14
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
15
15
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
16
|
+
const [view, setView] = useState(table.get("view") || "grid");
|
|
17
|
+
const handleViewToggle = (next) => {
|
|
18
|
+
if (next === "table")
|
|
19
|
+
return;
|
|
20
|
+
setView(next);
|
|
21
|
+
table.set("view", next);
|
|
22
|
+
};
|
|
16
23
|
// Pending filter state — buffered until "Apply Filters" clicked
|
|
17
24
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
18
25
|
const pendingTable = useMemo(() => ({
|
|
@@ -101,5 +108,5 @@ export function EventsIndexListing({ initialData }) {
|
|
|
101
108
|
const commitSearch = useCallback(() => {
|
|
102
109
|
table.set("q", searchInput.trim());
|
|
103
110
|
}, [searchInput, table]);
|
|
104
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "startsAt", sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); },
|
|
111
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search events...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "startsAt", sortOptions: EVENT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-video bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-4 space-y-2", children: [_jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-full" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-2/3" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded mt-2" })] })] }, i))) })) : events.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No events found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) : (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(EventFilters, { table: pendingTable, variant: "public" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
105
112
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useMemo, useState } from "react";
|
|
4
|
-
import { Button, Div, Heading, Span, Text } from "../../../ui";
|
|
4
|
+
import { Button, Div, Heading, LoginRequiredModal, Span, Text } from "../../../ui";
|
|
5
|
+
import { isAuthError } from "../../../utils/auth-error";
|
|
5
6
|
const DEFAULT_LABELS = {
|
|
6
7
|
heading: "Spin the Wheel",
|
|
7
8
|
spinButton: "Spin",
|
|
@@ -20,6 +21,7 @@ export function SpinWheelView({ eventId, prizes, alreadyUsed, initialPrizeId, in
|
|
|
20
21
|
const [resultPrizeId, setResultPrizeId] = useState(initialPrizeId);
|
|
21
22
|
const [resultCoupon, setResultCoupon] = useState(initialCouponCode);
|
|
22
23
|
const [error, setError] = useState(null);
|
|
24
|
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
23
25
|
const now = Date.now();
|
|
24
26
|
const startMs = windowStart ? new Date(windowStart).getTime() : null;
|
|
25
27
|
const endMs = windowEnd ? new Date(windowEnd).getTime() : null;
|
|
@@ -43,15 +45,20 @@ export function SpinWheelView({ eventId, prizes, alreadyUsed, initialPrizeId, in
|
|
|
43
45
|
}
|
|
44
46
|
}, ANIMATION_MS);
|
|
45
47
|
}
|
|
46
|
-
catch {
|
|
48
|
+
catch (err) {
|
|
47
49
|
setSpinning(false);
|
|
48
|
-
|
|
50
|
+
if (isAuthError(err)) {
|
|
51
|
+
setShowLoginModal(true);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
setError(l.errorFallback);
|
|
55
|
+
}
|
|
49
56
|
}
|
|
50
57
|
}, [disabled, eventId, l.errorFallback, onSpin]);
|
|
51
58
|
const wonPrize = resultPrizeId
|
|
52
59
|
? activePrizes.find((p) => p.id === resultPrizeId)
|
|
53
60
|
: undefined;
|
|
54
|
-
return (_jsxs(Div, { className: "space-y-4", children: [_jsxs(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: ["\uD83C\uDFA1 ", l.heading] }), _jsxs(Div, { className: "relative mx-auto aspect-square w-64 overflow-hidden rounded-full border-4 border-amber-400 bg-gradient-to-br from-amber-100 via-rose-100 to-violet-100 dark:from-amber-900/40 dark:via-rose-900/40 dark:to-violet-900/40", "aria-label": "Spin wheel", children: [_jsx(Div, { className: "absolute inset-0 flex items-center justify-center", style: {
|
|
61
|
+
return (_jsxs(Div, { className: "space-y-4", children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to spin the wheel. Please log in or create an account to continue." }), _jsxs(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: ["\uD83C\uDFA1 ", l.heading] }), _jsxs(Div, { className: "relative mx-auto aspect-square w-64 overflow-hidden rounded-full border-4 border-amber-400 bg-gradient-to-br from-amber-100 via-rose-100 to-violet-100 dark:from-amber-900/40 dark:via-rose-900/40 dark:to-violet-900/40", "aria-label": "Spin wheel", children: [_jsx(Div, { className: "absolute inset-0 flex items-center justify-center", style: {
|
|
55
62
|
animation: spinning
|
|
56
63
|
? `lir-spin ${ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)`
|
|
57
64
|
: undefined,
|