@burdenoff/microfe-coolandlovely 2026.1031.2 → 2026.1101.2
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/coolandlovely/CoolAndLovelyRoot.d.ts.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoot.js +16 -4
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.d.ts.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.js +19 -15
- package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
- package/dist/coolandlovely/components/ActivityTrail.js +1 -1
- package/dist/coolandlovely/components/ActivityTrail.js.map +1 -1
- package/dist/coolandlovely/components/ActorName.js.map +1 -1
- package/dist/coolandlovely/components/AddToCartPanel.js.map +1 -1
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
- package/dist/coolandlovely/components/BrandCard.js.map +1 -1
- package/dist/coolandlovely/components/BrandCover.js.map +1 -1
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -1
- package/dist/coolandlovely/components/CollaborationFeeCard.js.map +1 -1
- package/dist/coolandlovely/components/CollectionCard.js.map +1 -1
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -1
- package/dist/coolandlovely/components/Comments.js.map +1 -1
- package/dist/coolandlovely/components/EmptyState.js.map +1 -1
- package/dist/coolandlovely/components/FeedbackWidget.js.map +1 -1
- package/dist/coolandlovely/components/Field.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js.map +1 -1
- package/dist/coolandlovely/components/FollowButton.js.map +1 -1
- package/dist/coolandlovely/components/FulfilmentTimeline.js.map +1 -1
- package/dist/coolandlovely/components/ImageUploadField.js +3 -1
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
- package/dist/coolandlovely/components/LookCard.js.map +1 -1
- package/dist/coolandlovely/components/LookbookCard.js.map +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/PageLayout.js.map +1 -1
- package/dist/coolandlovely/components/PayForOrder.js.map +1 -1
- package/dist/coolandlovely/components/RefundPanel.js +1 -1
- package/dist/coolandlovely/components/RefundPanel.js.map +1 -1
- package/dist/coolandlovely/components/ReturnRequests.js +1 -1
- package/dist/coolandlovely/components/ReturnRequests.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -1
- package/dist/coolandlovely/components/StateViews.js.map +1 -1
- package/dist/coolandlovely/components/StatusBadge.js +1 -1
- package/dist/coolandlovely/components/StatusBadge.js.map +1 -1
- package/dist/coolandlovely/components/StepTracker.js.map +1 -1
- package/dist/coolandlovely/components/StockEditor.js.map +1 -1
- package/dist/coolandlovely/components/StoreListingCard.js.map +1 -1
- package/dist/coolandlovely/components/TagList.js.map +1 -1
- package/dist/coolandlovely/components/Thumbnail.js +1 -1
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -1
- package/dist/coolandlovely/components/WardrobeItemCard.js.map +1 -1
- package/dist/coolandlovely/components/index.js +2 -1
- package/dist/coolandlovely/context/CoolAndLovelyContext.js +2 -1
- package/dist/coolandlovely/context/CoolAndLovelyContext.js.map +1 -1
- package/dist/coolandlovely/context/index.js +2 -1
- package/dist/coolandlovely/hooks/index.js +4 -3
- package/dist/coolandlovely/hooks/useActivity.d.ts +1 -1
- package/dist/coolandlovely/hooks/useActivity.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useActivity.js.map +1 -1
- package/dist/coolandlovely/hooks/useAssistantEntity.js.map +1 -1
- package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts +4 -4
- package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useBrandOrdersApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useCatalogImport.js.map +1 -1
- package/dist/coolandlovely/hooks/useComments.d.ts +1 -1
- package/dist/coolandlovely/hooks/useComments.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useComments.js.map +1 -1
- package/dist/coolandlovely/hooks/useCommerceApi.d.ts +7 -7
- package/dist/coolandlovely/hooks/useCommerceApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCommerceApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts +24 -24
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useEditorialApi.d.ts +95 -0
- package/dist/coolandlovely/hooks/useEditorialApi.d.ts.map +1 -0
- package/dist/coolandlovely/hooks/useEditorialApi.js +107 -0
- package/dist/coolandlovely/hooks/useEditorialApi.js.map +1 -0
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.d.ts +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useTr.js.map +1 -1
- package/dist/coolandlovely/index.js +6 -5
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminBrandsPage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminCollaborationsPage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminFeaturedItemsPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/ImportCatalogPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/BrandOrdersPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/CartPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/CreatorEarningsPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/OrdersPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
- package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts +2 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts.map +1 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.js +486 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.js.map +1 -0
- package/dist/coolandlovely/pages/items/StoreListedPieceView.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/dist/coolandlovely/store/coolandlovelyStore.js.map +1 -1
- package/dist/coolandlovely/store/index.js +2 -1
- package/dist/coolandlovely/types/commerce.d.ts.map +1 -1
- package/dist/coolandlovely/types/index.d.ts.map +1 -1
- package/dist/coolandlovely/utils/cn.js +1 -1
- package/dist/coolandlovely/utils/cn.js.map +1 -1
- package/dist/coolandlovely/utils/formatters.js.map +1 -1
- package/dist/coolandlovely/utils/icons.js.map +1 -1
- package/dist/coolandlovely/utils/index.js +2 -1
- package/dist/coolandlovely/utils/navigation.js.map +1 -1
- package/dist/coolandlovely/utils/paymentProviders.js +22 -22
- package/dist/coolandlovely/utils/paymentProviders.js.map +1 -1
- package/dist/coolandlovely/utils/sparkline.js +13 -15
- package/dist/coolandlovely/utils/sparkline.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +225 -146
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/stories/DiscoverPage.stories.d.ts.map +1 -1
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface EditorialArticle {
|
|
2
|
+
id: string;
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
authorName: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
title: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
body: string;
|
|
9
|
+
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
|
10
|
+
tags: string[];
|
|
11
|
+
styleItemIds: string[];
|
|
12
|
+
lookIds: string[];
|
|
13
|
+
collectionIds: string[];
|
|
14
|
+
brandIds: string[];
|
|
15
|
+
publishedAt: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type EditorialInput = Omit<EditorialArticle, 'id' | 'workspaceId' | 'status' | 'publishedAt'>;
|
|
18
|
+
export interface TrendReport {
|
|
19
|
+
id: string;
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
periodStart: string;
|
|
23
|
+
periodEnd: string;
|
|
24
|
+
methodology: string;
|
|
25
|
+
metrics: {
|
|
26
|
+
catalogItems: number;
|
|
27
|
+
publicLookTags: number;
|
|
28
|
+
palettes: Array<{
|
|
29
|
+
label: string;
|
|
30
|
+
count: number;
|
|
31
|
+
}>;
|
|
32
|
+
brandGrowth: Array<{
|
|
33
|
+
brandId: string;
|
|
34
|
+
name: string;
|
|
35
|
+
current: number;
|
|
36
|
+
previous: number;
|
|
37
|
+
change: number;
|
|
38
|
+
}>;
|
|
39
|
+
mostFeatured: Array<{
|
|
40
|
+
styleItemId: string;
|
|
41
|
+
name: string;
|
|
42
|
+
publicLookCount: number;
|
|
43
|
+
}>;
|
|
44
|
+
wearCountsIncluded: boolean;
|
|
45
|
+
mostWorn: Array<{
|
|
46
|
+
rank: number;
|
|
47
|
+
name: string;
|
|
48
|
+
wearCount: number;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
publishedAt: string | null;
|
|
52
|
+
}
|
|
53
|
+
interface Connection<T> {
|
|
54
|
+
items: T[];
|
|
55
|
+
pageInfo: {
|
|
56
|
+
hasMore: boolean;
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export declare function useEditorialFeatures(kind: 'BRAND' | 'STYLE_ITEM' | 'LOOK' | 'COLLECTION', offset: number): import('@tanstack/react-query').UseQueryResult<Connection<{
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
}>, Error>;
|
|
64
|
+
export declare function useMyArticles(offset: number): import('@tanstack/react-query').UseQueryResult<Connection<EditorialArticle>, Error>;
|
|
65
|
+
export declare function useMyTrendReports(offset: number): import('@tanstack/react-query').UseQueryResult<Connection<TrendReport>, Error>;
|
|
66
|
+
type EditorialAction = {
|
|
67
|
+
kind: 'save';
|
|
68
|
+
id?: string;
|
|
69
|
+
input: EditorialInput;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'status';
|
|
72
|
+
id: string;
|
|
73
|
+
status: EditorialArticle['status'];
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'delete';
|
|
76
|
+
id: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'report';
|
|
79
|
+
input: {
|
|
80
|
+
title: string;
|
|
81
|
+
periodStart: string;
|
|
82
|
+
periodEnd: string;
|
|
83
|
+
includeWearCounts: boolean;
|
|
84
|
+
};
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'publishReport';
|
|
87
|
+
id: string;
|
|
88
|
+
publish: boolean;
|
|
89
|
+
} | {
|
|
90
|
+
kind: 'deleteReport';
|
|
91
|
+
id: string;
|
|
92
|
+
};
|
|
93
|
+
export declare function useEditorialAction(): import('@tanstack/react-query').UseMutationResult<void, Error, EditorialAction, unknown>;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=useEditorialApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEditorialApi.d.ts","sourceRoot":"","sources":["../../../src/coolandlovely/hooks/useEditorialApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useCoolAndLovely as e } from "../context/CoolAndLovelyContext.js";
|
|
2
|
+
import "../context/index.js";
|
|
3
|
+
import { coolQuery as t } from "./useCoolAndLovelyApi.js";
|
|
4
|
+
import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
|
|
5
|
+
//#region src/coolandlovely/hooks/useEditorialApi.ts
|
|
6
|
+
var a = "id workspaceId authorName slug title summary body status tags styleItemIds lookIds collectionIds brandIds publishedAt", o = "id workspaceId title periodStart periodEnd methodology metrics publishedAt";
|
|
7
|
+
function s(n, i) {
|
|
8
|
+
let { workspaceId: a, apiGatewayUrl: o, currentUser: s } = e();
|
|
9
|
+
return r({
|
|
10
|
+
queryKey: [
|
|
11
|
+
"coolandlovely",
|
|
12
|
+
"editorial",
|
|
13
|
+
a,
|
|
14
|
+
s?.id,
|
|
15
|
+
"features",
|
|
16
|
+
n,
|
|
17
|
+
i
|
|
18
|
+
],
|
|
19
|
+
queryFn: async () => (await t("query($kind:CooEditorialFeatureKind!,$pagination:PaginationInput){cooEditorialFeatureOptions(kind:$kind,pagination:$pagination){items{id name}pageInfo{hasMore cursor}}}", {
|
|
20
|
+
kind: n,
|
|
21
|
+
pagination: {
|
|
22
|
+
offset: i,
|
|
23
|
+
limit: 20
|
|
24
|
+
}
|
|
25
|
+
}, o)).cooEditorialFeatureOptions
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function c(n) {
|
|
29
|
+
let { workspaceId: i, apiGatewayUrl: o, currentUser: s } = e();
|
|
30
|
+
return r({
|
|
31
|
+
queryKey: [
|
|
32
|
+
"coolandlovely",
|
|
33
|
+
"editorial",
|
|
34
|
+
i,
|
|
35
|
+
s?.id,
|
|
36
|
+
"articles",
|
|
37
|
+
n
|
|
38
|
+
],
|
|
39
|
+
queryFn: async () => (await t(`query($pagination: PaginationInput) { cooMyArticles(pagination: $pagination) { items { ${a} } pageInfo { hasMore cursor } } }`, { pagination: {
|
|
40
|
+
offset: n,
|
|
41
|
+
limit: 20
|
|
42
|
+
} }, o)).cooMyArticles
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function l(n) {
|
|
46
|
+
let { workspaceId: i, apiGatewayUrl: a, currentUser: s } = e();
|
|
47
|
+
return r({
|
|
48
|
+
queryKey: [
|
|
49
|
+
"coolandlovely",
|
|
50
|
+
"editorial",
|
|
51
|
+
i,
|
|
52
|
+
s?.id,
|
|
53
|
+
"reports",
|
|
54
|
+
n
|
|
55
|
+
],
|
|
56
|
+
queryFn: async () => (await t(`query($pagination: PaginationInput) { cooMyTrendReports(pagination: $pagination) { items { ${o} } pageInfo { hasMore cursor } } }`, { pagination: {
|
|
57
|
+
offset: n,
|
|
58
|
+
limit: 20
|
|
59
|
+
} }, a)).cooMyTrendReports
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function u() {
|
|
63
|
+
let { apiGatewayUrl: r, workspaceId: a, currentUser: o } = e(), s = i();
|
|
64
|
+
return n({
|
|
65
|
+
mutationFn: async (e) => {
|
|
66
|
+
switch (e.kind) {
|
|
67
|
+
case "save":
|
|
68
|
+
e.id ? await t("mutation($id:ID!,$input:CooEditorialArticleInput!){cooUpdateArticle(id:$id,input:$input){id}}", {
|
|
69
|
+
id: e.id,
|
|
70
|
+
input: e.input
|
|
71
|
+
}, r) : await t("mutation($input:CooEditorialArticleInput!){cooCreateArticle(input:$input){id}}", { input: e.input }, r);
|
|
72
|
+
break;
|
|
73
|
+
case "status":
|
|
74
|
+
await t("mutation($id:ID!,$status:CooEditorialStatus!){cooSetArticleStatus(id:$id,status:$status){id}}", {
|
|
75
|
+
id: e.id,
|
|
76
|
+
status: e.status
|
|
77
|
+
}, r);
|
|
78
|
+
break;
|
|
79
|
+
case "delete":
|
|
80
|
+
await t("mutation($id:ID!){cooDeleteArticle(id:$id)}", { id: e.id }, r);
|
|
81
|
+
break;
|
|
82
|
+
case "report":
|
|
83
|
+
await t("mutation($input:CooGenerateTrendReportInput!){cooGenerateTrendReport(input:$input){id}}", { input: e.input }, r);
|
|
84
|
+
break;
|
|
85
|
+
case "publishReport":
|
|
86
|
+
await t("mutation($id:ID!,$publish:Boolean!){cooPublishTrendReport(id:$id,publish:$publish){id}}", {
|
|
87
|
+
id: e.id,
|
|
88
|
+
publish: e.publish
|
|
89
|
+
}, r);
|
|
90
|
+
break;
|
|
91
|
+
case "deleteReport": await t("mutation($id:ID!){cooDeleteTrendReport(id:$id)}", { id: e.id }, r);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
onSuccess: async () => {
|
|
95
|
+
await s.invalidateQueries({ queryKey: [
|
|
96
|
+
"coolandlovely",
|
|
97
|
+
"editorial",
|
|
98
|
+
a,
|
|
99
|
+
o?.id
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { u as useEditorialAction, s as useEditorialFeatures, c as useMyArticles, l as useMyTrendReports };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=useEditorialApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEditorialApi.js","names":[],"sources":["../../../src/coolandlovely/hooks/useEditorialApi.ts"],"sourcesContent":["import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useCoolAndLovely } from '../context';\nimport { coolQuery } from './useCoolAndLovelyApi';\n\nexport interface EditorialArticle {\n id: string;\n workspaceId: string;\n authorName: string;\n slug: string;\n title: string;\n summary: string;\n body: string;\n status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';\n tags: string[];\n styleItemIds: string[];\n lookIds: string[];\n collectionIds: string[];\n brandIds: string[];\n publishedAt: string | null;\n}\n\nexport type EditorialInput = Omit<\n EditorialArticle,\n 'id' | 'workspaceId' | 'status' | 'publishedAt'\n>;\n\nexport interface TrendReport {\n id: string;\n workspaceId: string;\n title: string;\n periodStart: string;\n periodEnd: string;\n methodology: string;\n metrics: {\n catalogItems: number;\n publicLookTags: number;\n palettes: Array<{ label: string; count: number }>;\n brandGrowth: Array<{\n brandId: string;\n name: string;\n current: number;\n previous: number;\n change: number;\n }>;\n mostFeatured: Array<{ styleItemId: string; name: string; publicLookCount: number }>;\n wearCountsIncluded: boolean;\n mostWorn: Array<{ rank: number; name: string; wearCount: number }>;\n };\n publishedAt: string | null;\n}\n\ninterface Connection<T> {\n items: T[];\n pageInfo: { hasMore: boolean; cursor: string | null };\n}\n\nconst ARTICLE_FIELDS =\n 'id workspaceId authorName slug title summary body status tags styleItemIds lookIds collectionIds brandIds publishedAt';\nconst REPORT_FIELDS = 'id workspaceId title periodStart periodEnd methodology metrics publishedAt';\n\nexport function useEditorialFeatures(\n kind: 'BRAND' | 'STYLE_ITEM' | 'LOOK' | 'COLLECTION',\n offset: number\n) {\n const { workspaceId, apiGatewayUrl, currentUser } = useCoolAndLovely();\n return useQuery({\n queryKey: [\n 'coolandlovely',\n 'editorial',\n workspaceId,\n currentUser?.id,\n 'features',\n kind,\n offset,\n ],\n queryFn: async () =>\n (\n await coolQuery<{ cooEditorialFeatureOptions: Connection<{ id: string; name: string }> }>(\n `query($kind:CooEditorialFeatureKind!,$pagination:PaginationInput){cooEditorialFeatureOptions(kind:$kind,pagination:$pagination){items{id name}pageInfo{hasMore cursor}}}`,\n { kind, pagination: { offset, limit: 20 } },\n apiGatewayUrl\n )\n ).cooEditorialFeatureOptions,\n });\n}\n\nexport function useMyArticles(offset: number) {\n const { workspaceId, apiGatewayUrl, currentUser } = useCoolAndLovely();\n return useQuery({\n queryKey: ['coolandlovely', 'editorial', workspaceId, currentUser?.id, 'articles', offset],\n queryFn: async () =>\n (\n await coolQuery<{ cooMyArticles: Connection<EditorialArticle> }>(\n `query($pagination: PaginationInput) { cooMyArticles(pagination: $pagination) { items { ${ARTICLE_FIELDS} } pageInfo { hasMore cursor } } }`,\n { pagination: { offset, limit: 20 } },\n apiGatewayUrl\n )\n ).cooMyArticles,\n });\n}\n\nexport function useMyTrendReports(offset: number) {\n const { workspaceId, apiGatewayUrl, currentUser } = useCoolAndLovely();\n return useQuery({\n queryKey: ['coolandlovely', 'editorial', workspaceId, currentUser?.id, 'reports', offset],\n queryFn: async () =>\n (\n await coolQuery<{ cooMyTrendReports: Connection<TrendReport> }>(\n `query($pagination: PaginationInput) { cooMyTrendReports(pagination: $pagination) { items { ${REPORT_FIELDS} } pageInfo { hasMore cursor } } }`,\n { pagination: { offset, limit: 20 } },\n apiGatewayUrl\n )\n ).cooMyTrendReports,\n });\n}\n\ntype EditorialAction =\n | { kind: 'save'; id?: string; input: EditorialInput }\n | { kind: 'status'; id: string; status: EditorialArticle['status'] }\n | { kind: 'delete'; id: string }\n | {\n kind: 'report';\n input: { title: string; periodStart: string; periodEnd: string; includeWearCounts: boolean };\n }\n | { kind: 'publishReport'; id: string; publish: boolean }\n | { kind: 'deleteReport'; id: string };\n\nexport function useEditorialAction() {\n const { apiGatewayUrl, workspaceId, currentUser } = useCoolAndLovely();\n const client = useQueryClient();\n return useMutation({\n mutationFn: async (action: EditorialAction): Promise<void> => {\n switch (action.kind) {\n case 'save':\n if (action.id)\n await coolQuery(\n `mutation($id:ID!,$input:CooEditorialArticleInput!){cooUpdateArticle(id:$id,input:$input){id}}`,\n { id: action.id, input: action.input },\n apiGatewayUrl\n );\n else\n await coolQuery(\n `mutation($input:CooEditorialArticleInput!){cooCreateArticle(input:$input){id}}`,\n { input: action.input },\n apiGatewayUrl\n );\n break;\n case 'status':\n await coolQuery(\n `mutation($id:ID!,$status:CooEditorialStatus!){cooSetArticleStatus(id:$id,status:$status){id}}`,\n { id: action.id, status: action.status },\n apiGatewayUrl\n );\n break;\n case 'delete':\n await coolQuery(\n `mutation($id:ID!){cooDeleteArticle(id:$id)}`,\n { id: action.id },\n apiGatewayUrl\n );\n break;\n case 'report':\n await coolQuery(\n `mutation($input:CooGenerateTrendReportInput!){cooGenerateTrendReport(input:$input){id}}`,\n { input: action.input },\n apiGatewayUrl\n );\n break;\n case 'publishReport':\n await coolQuery(\n `mutation($id:ID!,$publish:Boolean!){cooPublishTrendReport(id:$id,publish:$publish){id}}`,\n { id: action.id, publish: action.publish },\n apiGatewayUrl\n );\n break;\n case 'deleteReport':\n await coolQuery(\n `mutation($id:ID!){cooDeleteTrendReport(id:$id)}`,\n { id: action.id },\n apiGatewayUrl\n );\n }\n },\n onSuccess: async () => {\n await client.invalidateQueries({\n queryKey: ['coolandlovely', 'editorial', workspaceId, currentUser?.id],\n });\n },\n });\n}\n"],"mappings":";;;;;AAyDA,IAAM,IACJ,yHACI,IAAgB;AAEtB,SAAgB,EACd,GACA,GACA;CACA,IAAM,EAAE,gBAAa,kBAAe,mBAAgB,EAAiB;CACrE,OAAO,EAAS;EACd,UAAU;GACR;GACA;GACA;GACA,GAAa;GACb;GACA;GACA;EACF;EACA,SAAS,aAEL,MAAM,EACJ,4KACA;GAAE;GAAM,YAAY;IAAE;IAAQ,OAAO;GAAG;EAAE,GAC1C,CACF,EAAA,CACA;CACN,CAAC;AACH;AAEA,SAAgB,EAAc,GAAgB;CAC5C,IAAM,EAAE,gBAAa,kBAAe,mBAAgB,EAAiB;CACrE,OAAO,EAAS;EACd,UAAU;GAAC;GAAiB;GAAa;GAAa,GAAa;GAAI;GAAY;EAAM;EACzF,SAAS,aAEL,MAAM,EACJ,0FAA0F,EAAe,qCACzG,EAAE,YAAY;GAAE;GAAQ,OAAO;EAAG,EAAE,GACpC,CACF,EAAA,CACA;CACN,CAAC;AACH;AAEA,SAAgB,EAAkB,GAAgB;CAChD,IAAM,EAAE,gBAAa,kBAAe,mBAAgB,EAAiB;CACrE,OAAO,EAAS;EACd,UAAU;GAAC;GAAiB;GAAa;GAAa,GAAa;GAAI;GAAW;EAAM;EACxF,SAAS,aAEL,MAAM,EACJ,8FAA8F,EAAc,qCAC5G,EAAE,YAAY;GAAE;GAAQ,OAAO;EAAG,EAAE,GACpC,CACF,EAAA,CACA;CACN,CAAC;AACH;AAaA,SAAgB,IAAqB;CACnC,IAAM,EAAE,kBAAe,gBAAa,mBAAgB,EAAiB,GAC/D,IAAS,EAAe;CAC9B,OAAO,EAAY;EACjB,YAAY,OAAO,MAA2C;GAC5D,QAAQ,EAAO,MAAf;IACE,KAAK;KACH,AAAI,EAAO,KACT,MAAM,EACJ,iGACA;MAAE,IAAI,EAAO;MAAI,OAAO,EAAO;KAAM,GACrC,CACF,IAEA,MAAM,EACJ,kFACA,EAAE,OAAO,EAAO,MAAM,GACtB,CACF;KACF;IACF,KAAK;KACH,MAAM,EACJ,iGACA;MAAE,IAAI,EAAO;MAAI,QAAQ,EAAO;KAAO,GACvC,CACF;KACA;IACF,KAAK;KACH,MAAM,EACJ,+CACA,EAAE,IAAI,EAAO,GAAG,GAChB,CACF;KACA;IACF,KAAK;KACH,MAAM,EACJ,2FACA,EAAE,OAAO,EAAO,MAAM,GACtB,CACF;KACA;IACF,KAAK;KACH,MAAM,EACJ,2FACA;MAAE,IAAI,EAAO;MAAI,SAAS,EAAO;KAAQ,GACzC,CACF;KACA;IACF,KAAK,gBACH,MAAM,EACJ,mDACA,EAAE,IAAI,EAAO,GAAG,GAChB,CACF;GACJ;EACF;EACA,WAAW,YAAY;GACrB,MAAM,EAAO,kBAAkB,EAC7B,UAAU;IAAC;IAAiB;IAAa;IAAa,GAAa;GAAE,EACvE,CAAC;EACH;CACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFiles.js","names":[],"sources":["../../../src/coolandlovely/hooks/useFiles.ts"],"sourcesContent":["/**\n * File hooks — image upload + display via wspace-files-svc (NO duplication).\n * @module coolandlovely/hooks\n *\n * CoolAndLovely never stores blobs. Brand logos/covers, collection covers,\n * style-item photos, look photos, wardrobe photos and lookbook covers are\n * wspace-files `File` records referenced by id (the coo* inputs take *FileId;\n * the coo* types expose logo/cover/image: File resolved via federation).\n *\n * Upload uses the SAME graphqlFetch transport as every other coo* call — NOT\n * Apollo — driving the wspace-files 3-step presigned flow directly:\n * ensureFolderByPath -> initiateUpload -> HTTP PUT -> completeUpload\n * Display resolves a File id to a short-lived presigned URL via\n * getFileDownloadUrl.\n *\n * Two fleet gotchas are handled here:\n * (a) `fileSize` is REQUIRED on completeUpload even though this MFE's generated\n * `CompleteUploadInput` is stale and omits it. Because we send a hand-written\n * query with `Record<string, unknown>` variables (not typed against the\n * generated input) we simply include it and it works at runtime.\n * (b) `getFileDownloadUrl(expiresIn)` must be <= 604800 (S3 7-day cap → 500).\n * We use 21600 (6h); do NOT raise it.\n */\n\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nimport { useCoolAndLovely } from '../context';\n\n/**\n * 6 hours. getFileDownloadUrl rejects TTLs > 604800 (S3 7-day cap → 500); the\n * safe fleet ceiling is 21600. Do NOT raise this.\n */\nconst DOWNLOAD_URL_TTL_SECONDS = 21_600;\n\n/** Per-entity folders in wspace-files. */\nexport const COO_FILE_FOLDERS = {\n brands: '/CoolAndLovely/Brands',\n collections: '/CoolAndLovely/Collections',\n items: '/CoolAndLovely/Items',\n looks: '/CoolAndLovely/Looks',\n wardrobe: '/CoolAndLovely/Wardrobe',\n lookbooks: '/CoolAndLovely/Lookbooks',\n} as const;\n\nexport type CooFileFolder = (typeof COO_FILE_FOLDERS)[keyof typeof COO_FILE_FOLDERS];\n\nexport interface UploadedImage {\n id: string;\n url: string;\n name: string;\n}\n\n/** Thin wrapper over the workspace gateway using the MFE's proven transport. */\nasync function filesFetch<TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'Files request failed');\n }\n if (!response.data) {\n throw new Error('Files response did not include data');\n }\n // Drop fe-libs' ~15s query response cache after a mutation so a follow-up\n // getFileDownloadUrl reads fresh (mirrors coolQuery in useCoolAndLovelyApi).\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlFetchResponseCache();\n }\n return response.data;\n}\n\nconst ENSURE_FOLDER = `\n mutation CooEnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) { id }\n }\n`;\n\nconst INITIATE_UPLOAD = `\n mutation CooInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = `\n mutation CooCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }\n`;\n\nconst GET_FILE_DOWNLOAD_URL = `\n query CooGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\nfunction fileExtension(name: string, mimeType: string): string {\n const dot = name.lastIndexOf('.');\n if (dot > -1 && dot < name.length - 1) return name.slice(dot + 1).toLowerCase();\n return (mimeType.split('/')[1] ?? 'bin').toLowerCase();\n}\n\n/** Upload progress emitted by {@link uploadImageToFilesWithProgress}. */\nexport interface UploadProgress {\n /** Bytes transferred so far. */\n loaded: number;\n /** Total bytes to transfer. */\n total: number;\n /** Rounded percentage complete, 0–100. */\n pct: number;\n}\n\n/**\n * PUT the bytes straight to storage via XMLHttpRequest so `upload.onprogress`\n * can surface a real progress bar (the presigned upload URL points at object\n * storage, so a plain PUT is all that is needed — no auth header). Content-Type\n * must match what was sent to `initiateUpload`.\n */\nfunction putWithProgress(\n uploadUrl: string,\n file: File,\n mimeType: string,\n onProgress?: (progress: UploadProgress) => void\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', uploadUrl, true);\n xhr.setRequestHeader('Content-Type', mimeType);\n if (onProgress) {\n xhr.upload.onprogress = (event: ProgressEvent) => {\n const total = event.lengthComputable ? event.total : file.size;\n const loaded = Math.min(event.loaded, total);\n const pct = total > 0 ? Math.round((loaded / total) * 100) : 0;\n onProgress({ loaded, total, pct });\n };\n }\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n onProgress?.({ loaded: file.size, total: file.size, pct: 100 });\n resolve();\n } else {\n reject(new Error(`Upload to storage failed (${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Upload to storage failed — a network error occurred.'));\n xhr.onabort = () => reject(new Error('Upload was cancelled.'));\n xhr.send(file);\n });\n}\n\n/**\n * The full presigned upload flow (ensureFolder -> initiateUpload -> PUT ->\n * completeUpload -> download URL). Shared by both public upload helpers; the\n * PUT step reports progress when `onProgress` is supplied.\n */\nasync function runUpload(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items,\n onProgress?: (progress: UploadProgress) => void\n): Promise<UploadedImage> {\n const mimeType = file.type || 'application/octet-stream';\n const fileSize = String(file.size); // BigInt scalar — serialize as string.\n\n // 1. Resolve (or create) the destination folder. Best-effort: fall back to\n // the files root (folderId is optional on both initiate + complete).\n let folderId: string | undefined;\n try {\n const folder = await filesFetch<{ ensureFolderByPath?: { id: string } | null }>(\n ENSURE_FOLDER,\n { path: folderPath },\n baseUrl\n );\n folderId = folder.ensureFolderByPath?.id ?? undefined;\n } catch {\n folderId = undefined;\n }\n\n // 2. Open a presigned upload session.\n const initiated = await filesFetch<{\n initiateUpload?: { uploadUrl?: string | null; session: { id: string } } | null;\n }>(\n INITIATE_UPLOAD,\n {\n input: {\n fileName: file.name,\n extension: fileExtension(file.name, mimeType),\n fileSize, // required, BigInt-as-string\n mimeType,\n ...(folderId ? { folderId } : {}),\n },\n },\n baseUrl\n );\n const uploadUrl = initiated.initiateUpload?.uploadUrl;\n const sessionId = initiated.initiateUpload?.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('The files service did not return an upload URL.');\n }\n\n // 3. PUT the bytes straight to storage (XHR → progress). Content-Type must\n // match initiateUpload.\n await putWithProgress(uploadUrl, file, mimeType, onProgress);\n\n // 4. Finalize — creates the File record. fileSize is REQUIRED here even though\n // the MFE's generated CompleteUploadInput is stale and omits it.\n const completed = await filesFetch<{\n completeUpload?: { fileId: string; file: { id: string; name: string } } | null;\n }>(\n COMPLETE_UPLOAD,\n {\n input: {\n sessionId,\n fileSize, // <-- stale-type gotcha: the live schema requires this\n ...(folderId ? { folderId } : {}),\n },\n },\n baseUrl\n );\n const record = completed.completeUpload?.file;\n const newId = completed.completeUpload?.fileId ?? record?.id;\n if (!newId) {\n throw new Error('The upload could not be finalized.');\n }\n\n // 5. Best-effort presigned URL for immediate preview.\n let url = '';\n try {\n const urlData = await filesFetch<{ getFileDownloadUrl?: string | null }>(\n GET_FILE_DOWNLOAD_URL,\n { id: newId, expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n baseUrl\n );\n url = urlData.getFileDownloadUrl ?? '';\n } catch {\n url = '';\n }\n\n return { id: newId, url, name: record?.name ?? file.name };\n}\n\n/**\n * Upload an image to wspace-files-svc and return its id + a presigned URL. The\n * caller persists `id` (e.g. cooUpdateBrand({ logoFileId })); `url` is for an\n * immediate preview. `baseUrl` is the shell's apiGatewayUrl.\n */\nexport async function uploadImageToFiles(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items\n): Promise<UploadedImage> {\n return runUpload(file, baseUrl, folderPath);\n}\n\n/**\n * Same as {@link uploadImageToFiles} but reports transfer progress via\n * `onProgress` (XHR `upload.onprogress`) so callers can render a live progress\n * bar. `baseUrl` is the shell's apiGatewayUrl.\n */\nexport async function uploadImageToFilesWithProgress(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items,\n onProgress?: (progress: UploadProgress) => void\n): Promise<UploadedImage> {\n return runUpload(file, baseUrl, folderPath, onProgress);\n}\n\n/** Resolve a wspace-files File id to a presigned URL for display. */\nexport function useFileUrl(fileId?: string | null) {\n const { apiGatewayUrl } = useCoolAndLovely();\n return useQuery({\n queryKey: ['coolandlovely', 'file-url', fileId ?? ''],\n enabled: Boolean(fileId),\n staleTime: 20 * 60 * 1000, // refetch well inside the 6h TTL\n retry: 1,\n queryFn: async () => {\n const data = await filesFetch<{ getFileDownloadUrl?: string | null }>(\n GET_FILE_DOWNLOAD_URL,\n { id: fileId, expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n apiGatewayUrl\n );\n return data.getFileDownloadUrl ?? null;\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useFiles.js","names":[],"sources":["../../../src/coolandlovely/hooks/useFiles.ts"],"sourcesContent":["/**\n * File hooks — image upload + display via wspace-files-svc (NO duplication).\n * @module coolandlovely/hooks\n *\n * CoolAndLovely never stores blobs. Brand logos/covers, collection covers,\n * style-item photos, look photos, wardrobe photos and lookbook covers are\n * wspace-files `File` records referenced by id (the coo* inputs take *FileId;\n * the coo* types expose logo/cover/image: File resolved via federation).\n *\n * Upload uses the SAME graphqlFetch transport as every other coo* call — NOT\n * Apollo — driving the wspace-files 3-step presigned flow directly:\n * ensureFolderByPath -> initiateUpload -> HTTP PUT -> completeUpload\n * Display resolves a File id to a short-lived presigned URL via\n * getFileDownloadUrl.\n *\n * Two fleet gotchas are handled here:\n * (a) `fileSize` is REQUIRED on completeUpload even though this MFE's generated\n * `CompleteUploadInput` is stale and omits it. Because we send a hand-written\n * query with `Record<string, unknown>` variables (not typed against the\n * generated input) we simply include it and it works at runtime.\n * (b) `getFileDownloadUrl(expiresIn)` must be <= 604800 (S3 7-day cap → 500).\n * We use 21600 (6h); do NOT raise it.\n */\n\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql/fetch';\n\nimport { useCoolAndLovely } from '../context';\n\n/**\n * 6 hours. getFileDownloadUrl rejects TTLs > 604800 (S3 7-day cap → 500); the\n * safe fleet ceiling is 21600. Do NOT raise this.\n */\nconst DOWNLOAD_URL_TTL_SECONDS = 21_600;\n\n/** Per-entity folders in wspace-files. */\nexport const COO_FILE_FOLDERS = {\n brands: '/CoolAndLovely/Brands',\n collections: '/CoolAndLovely/Collections',\n items: '/CoolAndLovely/Items',\n looks: '/CoolAndLovely/Looks',\n wardrobe: '/CoolAndLovely/Wardrobe',\n lookbooks: '/CoolAndLovely/Lookbooks',\n} as const;\n\nexport type CooFileFolder = (typeof COO_FILE_FOLDERS)[keyof typeof COO_FILE_FOLDERS];\n\nexport interface UploadedImage {\n id: string;\n url: string;\n name: string;\n}\n\n/** Thin wrapper over the workspace gateway using the MFE's proven transport. */\nasync function filesFetch<TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'Files request failed');\n }\n if (!response.data) {\n throw new Error('Files response did not include data');\n }\n // Drop fe-libs' ~15s query response cache after a mutation so a follow-up\n // getFileDownloadUrl reads fresh (mirrors coolQuery in useCoolAndLovelyApi).\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlFetchResponseCache();\n }\n return response.data;\n}\n\nconst ENSURE_FOLDER = `\n mutation CooEnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) { id }\n }\n`;\n\nconst INITIATE_UPLOAD = `\n mutation CooInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = `\n mutation CooCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }\n`;\n\nconst GET_FILE_DOWNLOAD_URL = `\n query CooGetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\nfunction fileExtension(name: string, mimeType: string): string {\n const dot = name.lastIndexOf('.');\n if (dot > -1 && dot < name.length - 1) return name.slice(dot + 1).toLowerCase();\n return (mimeType.split('/')[1] ?? 'bin').toLowerCase();\n}\n\n/** Upload progress emitted by {@link uploadImageToFilesWithProgress}. */\nexport interface UploadProgress {\n /** Bytes transferred so far. */\n loaded: number;\n /** Total bytes to transfer. */\n total: number;\n /** Rounded percentage complete, 0–100. */\n pct: number;\n}\n\n/**\n * PUT the bytes straight to storage via XMLHttpRequest so `upload.onprogress`\n * can surface a real progress bar (the presigned upload URL points at object\n * storage, so a plain PUT is all that is needed — no auth header). Content-Type\n * must match what was sent to `initiateUpload`.\n */\nfunction putWithProgress(\n uploadUrl: string,\n file: File,\n mimeType: string,\n onProgress?: (progress: UploadProgress) => void\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', uploadUrl, true);\n xhr.setRequestHeader('Content-Type', mimeType);\n if (onProgress) {\n xhr.upload.onprogress = (event: ProgressEvent) => {\n const total = event.lengthComputable ? event.total : file.size;\n const loaded = Math.min(event.loaded, total);\n const pct = total > 0 ? Math.round((loaded / total) * 100) : 0;\n onProgress({ loaded, total, pct });\n };\n }\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n onProgress?.({ loaded: file.size, total: file.size, pct: 100 });\n resolve();\n } else {\n reject(new Error(`Upload to storage failed (${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Upload to storage failed — a network error occurred.'));\n xhr.onabort = () => reject(new Error('Upload was cancelled.'));\n xhr.send(file);\n });\n}\n\n/**\n * The full presigned upload flow (ensureFolder -> initiateUpload -> PUT ->\n * completeUpload -> download URL). Shared by both public upload helpers; the\n * PUT step reports progress when `onProgress` is supplied.\n */\nasync function runUpload(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items,\n onProgress?: (progress: UploadProgress) => void\n): Promise<UploadedImage> {\n const mimeType = file.type || 'application/octet-stream';\n const fileSize = String(file.size); // BigInt scalar — serialize as string.\n\n // 1. Resolve (or create) the destination folder. Best-effort: fall back to\n // the files root (folderId is optional on both initiate + complete).\n let folderId: string | undefined;\n try {\n const folder = await filesFetch<{ ensureFolderByPath?: { id: string } | null }>(\n ENSURE_FOLDER,\n { path: folderPath },\n baseUrl\n );\n folderId = folder.ensureFolderByPath?.id ?? undefined;\n } catch {\n folderId = undefined;\n }\n\n // 2. Open a presigned upload session.\n const initiated = await filesFetch<{\n initiateUpload?: { uploadUrl?: string | null; session: { id: string } } | null;\n }>(\n INITIATE_UPLOAD,\n {\n input: {\n fileName: file.name,\n extension: fileExtension(file.name, mimeType),\n fileSize, // required, BigInt-as-string\n mimeType,\n ...(folderId ? { folderId } : {}),\n },\n },\n baseUrl\n );\n const uploadUrl = initiated.initiateUpload?.uploadUrl;\n const sessionId = initiated.initiateUpload?.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('The files service did not return an upload URL.');\n }\n\n // 3. PUT the bytes straight to storage (XHR → progress). Content-Type must\n // match initiateUpload.\n await putWithProgress(uploadUrl, file, mimeType, onProgress);\n\n // 4. Finalize — creates the File record. fileSize is REQUIRED here even though\n // the MFE's generated CompleteUploadInput is stale and omits it.\n const completed = await filesFetch<{\n completeUpload?: { fileId: string; file: { id: string; name: string } } | null;\n }>(\n COMPLETE_UPLOAD,\n {\n input: {\n sessionId,\n fileSize, // <-- stale-type gotcha: the live schema requires this\n ...(folderId ? { folderId } : {}),\n },\n },\n baseUrl\n );\n const record = completed.completeUpload?.file;\n const newId = completed.completeUpload?.fileId ?? record?.id;\n if (!newId) {\n throw new Error('The upload could not be finalized.');\n }\n\n // 5. Best-effort presigned URL for immediate preview.\n let url = '';\n try {\n const urlData = await filesFetch<{ getFileDownloadUrl?: string | null }>(\n GET_FILE_DOWNLOAD_URL,\n { id: newId, expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n baseUrl\n );\n url = urlData.getFileDownloadUrl ?? '';\n } catch {\n url = '';\n }\n\n return { id: newId, url, name: record?.name ?? file.name };\n}\n\n/**\n * Upload an image to wspace-files-svc and return its id + a presigned URL. The\n * caller persists `id` (e.g. cooUpdateBrand({ logoFileId })); `url` is for an\n * immediate preview. `baseUrl` is the shell's apiGatewayUrl.\n */\nexport async function uploadImageToFiles(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items\n): Promise<UploadedImage> {\n return runUpload(file, baseUrl, folderPath);\n}\n\n/**\n * Same as {@link uploadImageToFiles} but reports transfer progress via\n * `onProgress` (XHR `upload.onprogress`) so callers can render a live progress\n * bar. `baseUrl` is the shell's apiGatewayUrl.\n */\nexport async function uploadImageToFilesWithProgress(\n file: File,\n baseUrl?: string,\n folderPath: string = COO_FILE_FOLDERS.items,\n onProgress?: (progress: UploadProgress) => void\n): Promise<UploadedImage> {\n return runUpload(file, baseUrl, folderPath, onProgress);\n}\n\n/** Resolve a wspace-files File id to a presigned URL for display. */\nexport function useFileUrl(fileId?: string | null) {\n const { apiGatewayUrl } = useCoolAndLovely();\n return useQuery({\n queryKey: ['coolandlovely', 'file-url', fileId ?? ''],\n enabled: Boolean(fileId),\n staleTime: 20 * 60 * 1000, // refetch well inside the 6h TTL\n retry: 1,\n queryFn: async () => {\n const data = await filesFetch<{ getFileDownloadUrl?: string | null }>(\n GET_FILE_DOWNLOAD_URL,\n { id: fileId, expiresIn: DOWNLOAD_URL_TTL_SECONDS },\n apiGatewayUrl\n );\n return data.getFileDownloadUrl ?? null;\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CooSetStyleItemInventoryInput, CooStyleItemInventory, CooStyleItemStatus } from '../types';
|
|
2
2
|
|
|
3
3
|
/** The stock picture of one style item. */
|
|
4
|
-
export declare const useStyleItemInventory: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<
|
|
4
|
+
export declare const useStyleItemInventory: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<CooStyleItemInventory | null, Error>;
|
|
5
5
|
/** Turn stock tracking on or off and set the stock of a style item's variants. */
|
|
6
6
|
export declare const useSetStyleItemInventory: () => import('@tanstack/react-query').UseMutationResult<{
|
|
7
7
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useInventoryApi.d.ts","sourceRoot":"","sources":["../../../src/coolandlovely/hooks/useInventoryApi.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EACV,6BAA6B,EAC7B,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAUlB,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB,GAAI,IAAI,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"useInventoryApi.d.ts","sourceRoot":"","sources":["../../../src/coolandlovely/hooks/useInventoryApi.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EACV,6BAA6B,EAC7B,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAUlB,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB,GAAI,IAAI,MAAM,GAAG,SAAS,wFAkB3D,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,wBAAwB;QAOvB,MAAM;YACF,kBAAkB;eACf,qBAAqB;;QALE,MAAM;WAAS,6BAA6B;WAwBvF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useInventoryApi.js","names":[],"sources":["../../../src/coolandlovely/hooks/useInventoryApi.ts"],"sourcesContent":["/**\n * Per-variant inventory hooks (BOFF-9637)\n * @module coolandlovely/hooks\n *\n * Reads a style item's stock per size/colour (`CooStyleItem.inventory`) and lets a brand turn\n * stock tracking on and set the stock of each variant (`cooSetStyleItemInventory`). While a piece\n * tracks stock, checkout reserves it and the piece goes ACTIVE / OUT_OF_STOCK on its own.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useCoolAndLovely } from '../context';\nimport type {\n CooSetStyleItemInventoryInput,\n CooStyleItemInventory,\n CooStyleItemStatus,\n} from '../types';\nimport { coolQuery } from './useCoolAndLovelyApi';\n\nconst QK = 'coolandlovely';\n\nconst INVENTORY_FIELDS = `\n trackInventory totalAvailable totalReserved\n variants { size colour stockQuantity reservedQuantity inStock }\n`;\n\n/** The stock picture of one style item. */\nexport const useStyleItemInventory = (id: string | undefined) => {\n const { apiGatewayUrl } = useCoolAndLovely();\n return useQuery({\n queryKey: [QK, 'style-item-inventory', id],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await coolQuery<{\n cooStyleItem: { id: string; inventory: CooStyleItemInventory } | null;\n }>(\n `query CooStyleItemInventory($id: ID!) {\n cooStyleItem(id: $id) { id inventory { ${INVENTORY_FIELDS} } }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.cooStyleItem?.inventory ?? null;\n },\n });\n};\n\n/** Turn stock tracking on or off and set the stock of a style item's variants. */\nexport const useSetStyleItemInventory = () => {\n const { apiGatewayUrl } = useCoolAndLovely();\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: CooSetStyleItemInventoryInput }) => {\n const data = await coolQuery<{\n cooSetStyleItemInventory: {\n id: string;\n status: CooStyleItemStatus;\n inventory: CooStyleItemInventory;\n };\n }>(\n `mutation CooSetStyleItemInventory($id: ID!, $input: CooSetStyleItemInventoryInput!) {\n cooSetStyleItemInventory(id: $id, input: $input) {\n id status inventory { ${INVENTORY_FIELDS} }\n }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return data.cooSetStyleItemInventory;\n },\n onSuccess: (item) => {\n queryClient.setQueryData([QK, 'style-item-inventory', item.id], item.inventory);\n void queryClient.invalidateQueries({ queryKey: [QK, 'style-item', item.id] });\n void queryClient.invalidateQueries({ queryKey: [QK, 'style-items'] });\n },\n });\n};\n"],"mappings":";;;;;AAmBA,IAAM,IAAK,iBAEL,IAAmB,wHAMZ,KAAyB,MAA2B;CAC/D,IAAM,EAAE,qBAAkB,
|
|
1
|
+
{"version":3,"file":"useInventoryApi.js","names":[],"sources":["../../../src/coolandlovely/hooks/useInventoryApi.ts"],"sourcesContent":["/**\n * Per-variant inventory hooks (BOFF-9637)\n * @module coolandlovely/hooks\n *\n * Reads a style item's stock per size/colour (`CooStyleItem.inventory`) and lets a brand turn\n * stock tracking on and set the stock of each variant (`cooSetStyleItemInventory`). While a piece\n * tracks stock, checkout reserves it and the piece goes ACTIVE / OUT_OF_STOCK on its own.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useCoolAndLovely } from '../context';\nimport type {\n CooSetStyleItemInventoryInput,\n CooStyleItemInventory,\n CooStyleItemStatus,\n} from '../types';\nimport { coolQuery } from './useCoolAndLovelyApi';\n\nconst QK = 'coolandlovely';\n\nconst INVENTORY_FIELDS = `\n trackInventory totalAvailable totalReserved\n variants { size colour stockQuantity reservedQuantity inStock }\n`;\n\n/** The stock picture of one style item. */\nexport const useStyleItemInventory = (id: string | undefined) => {\n const { apiGatewayUrl } = useCoolAndLovely();\n return useQuery({\n queryKey: [QK, 'style-item-inventory', id],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await coolQuery<{\n cooStyleItem: { id: string; inventory: CooStyleItemInventory } | null;\n }>(\n `query CooStyleItemInventory($id: ID!) {\n cooStyleItem(id: $id) { id inventory { ${INVENTORY_FIELDS} } }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.cooStyleItem?.inventory ?? null;\n },\n });\n};\n\n/** Turn stock tracking on or off and set the stock of a style item's variants. */\nexport const useSetStyleItemInventory = () => {\n const { apiGatewayUrl } = useCoolAndLovely();\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: CooSetStyleItemInventoryInput }) => {\n const data = await coolQuery<{\n cooSetStyleItemInventory: {\n id: string;\n status: CooStyleItemStatus;\n inventory: CooStyleItemInventory;\n };\n }>(\n `mutation CooSetStyleItemInventory($id: ID!, $input: CooSetStyleItemInventoryInput!) {\n cooSetStyleItemInventory(id: $id, input: $input) {\n id status inventory { ${INVENTORY_FIELDS} }\n }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return data.cooSetStyleItemInventory;\n },\n onSuccess: (item) => {\n queryClient.setQueryData([QK, 'style-item-inventory', item.id], item.inventory);\n void queryClient.invalidateQueries({ queryKey: [QK, 'style-item', item.id] });\n void queryClient.invalidateQueries({ queryKey: [QK, 'style-items'] });\n },\n });\n};\n"],"mappings":";;;;;AAmBA,IAAM,IAAK,iBAEL,IAAmB,wHAMZ,KAAyB,MAA2B;CAC/D,IAAM,EAAE,qBAAkB,EAAiB;CAC3C,OAAO,EAAS;EACd,UAAU;GAAC;GAAI;GAAwB;EAAE;EACzC,SAAS,EAAQ;EACjB,SAAS,aAUA,MATY,EAGjB;mDAC2C,EAAiB;YAE5D,EAAE,MAAG,GACL,CACF,EAAA,CACY,cAAc,aAAa;CAE3C,CAAC;AACH,GAGa,UAAiC;CAC5C,IAAM,EAAE,qBAAkB,EAAiB,GACrC,IAAc,EAAe;CACnC,OAAO,EAAY;EACjB,YAAY,OAAO,EAAE,OAAI,gBAgBhB,MAfY,EAOjB;;oCAE4B,EAAiB;;YAG7C;GAAE;GAAI;EAAM,GACZ,CACF,EAAA,CACY;EAEd,YAAY,MAAS;GAGnB,AAFA,EAAY,aAAa;IAAC;IAAI;IAAwB,EAAK;GAAE,GAAG,EAAK,SAAS,GAC9E,EAAiB,kBAAkB,EAAE,UAAU;IAAC;IAAI;IAAc,EAAK;GAAE,EAAE,CAAC,GAC5E,EAAiB,kBAAkB,EAAE,UAAU,CAAC,GAAI,aAAa,EAAE,CAAC;EACtE;CACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTr.js","names":[],"sources":["../../../src/coolandlovely/hooks/useTr.ts"],"sourcesContent":["/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('coolandlovely.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('coolandlovely.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;\n */\nimport { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../../src/coolandlovely/hooks/useTr.ts"],"sourcesContent":["/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('coolandlovely.dashboard.title', 'Dashboard')}</h1>;\n * return <p>{tr('coolandlovely.delete.confirm', 'Delete {{count}} items?', { count: 5 })}</p>;\n */\nimport { useCallback } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>\n) => string {\n const { t } = useI18n();\n return useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) => str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v)),\n raw\n );\n },\n [t]\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,EAAQ;CACtB,OAAO,GACJ,GAAa,GAAkB,MAAqD;EACnF,IAAM,IAAa,EAAE,CAAG,GAClB,IAAM,MAAe,IAAM,IAAW;EAE5C,OADK,IACE,OAAO,QAAQ,CAAM,CAAC,CAAC,QAC3B,GAAK,CAAC,GAAG,OAAO,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,GAAG,GAAG,OAAO,CAAC,CAAC,GAC3E,CACF,IAJoB;CAKtB,GACA,CAAC,CAAC,CACJ;AACF"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import "./context/CoolAndLovelyContext.js";
|
|
2
|
-
import "./utils/cn.js";
|
|
1
|
+
import { CoolAndLovelyProvider as e, useCoolAndLovely as t } from "./context/CoolAndLovelyContext.js";
|
|
2
|
+
import { cn as n } from "./utils/cn.js";
|
|
3
3
|
import "./context/index.js";
|
|
4
|
-
import "./utils/formatters.js";
|
|
5
|
-
import "./CoolAndLovelyRoot.js";
|
|
6
|
-
import "./store/coolandlovelyStore.js";
|
|
4
|
+
import { formatDate as r, formatNumber as i, formatPrice as a, formatRelativeTime as o, titleCase as s } from "./utils/formatters.js";
|
|
5
|
+
import { CoolAndLovelyRoot as c } from "./CoolAndLovelyRoot.js";
|
|
6
|
+
import { useCoolAndLovelyStore as l } from "./store/coolandlovelyStore.js";
|
|
7
7
|
import "./store/index.js";
|
|
8
|
+
export { e as CoolAndLovelyProvider, c as CoolAndLovelyRoot, n as cn, r as formatDate, i as formatNumber, a as formatPrice, o as formatRelativeTime, s as titleCase, t as useCoolAndLovely, l as useCoolAndLovelyStore };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/coolandlovely/pages/HomePage.tsx"],"sourcesContent":["/**\n * Overview / Home Page\n * @module coolandlovely/pages\n *\n * The CoolAndLovely command center landing page: a style-operating-system\n * dashboard rolling up live catalog (brands, items), social (looks,\n * collaborations) and the shopper's own wardrobe — all from real `coo*` ops.\n */\n\nimport { useMemo } from 'react';\nimport {\n Store,\n Shirt,\n Sparkles,\n Handshake,\n Shapes,\n Heart,\n ArrowRight,\n Star,\n Plus,\n BookHeart,\n UserCircle,\n Compass,\n TrendingUp,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { Thumbnail } from '../components/Thumbnail';\nimport { LookCard } from '../components/LookCard';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport {\n useBrands,\n useStyleItems,\n useLooks,\n useCollaborations,\n useWardrobeItems,\n useTr,\n} from '../hooks';\nimport { cn } from '../utils/cn';\nimport { buildCreatedAtSparkline, sparklineDeltaLabel, sparklineTrendDirection } from '../utils';\nimport type { DashboardStats } from '../types';\n\ninterface QuickAction {\n labelKey: string;\n descriptionKey: string;\n label: string;\n description: string;\n icon: React.ReactNode;\n path: string;\n color: string;\n tourAttr?: string;\n}\n\ninterface ModuleItem {\n labelKey: string;\n descriptionKey: string;\n label: string;\n description: string;\n icon: React.ReactNode;\n path: string;\n color: string;\n tourAttr?: string;\n}\n\nconst quickActions: QuickAction[] = [\n {\n labelKey: 'newBrand',\n descriptionKey: 'newBrandDescription',\n label: 'New Brand',\n description: 'Launch a storefront',\n icon: <Store className=\"h-5 w-5\" />,\n path: '/brands/new',\n color: 'bg-accent-purple-subtle text-text-primary',\n },\n {\n labelKey: 'addStyleItem',\n descriptionKey: 'addStyleItemDescription',\n label: 'Add Style Item',\n description: 'List a piece',\n icon: <Shirt className=\"h-5 w-5\" />,\n path: '/items/new',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n },\n {\n labelKey: 'publishLook',\n descriptionKey: 'publishLookDescription',\n label: 'Publish a Look',\n description: 'Shop-the-look outfit',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/looks/new',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n {\n labelKey: 'myWardrobe',\n descriptionKey: 'myWardrobeDescription',\n label: 'My Wardrobe',\n description: 'What you own',\n icon: <Heart className=\"h-5 w-5\" />,\n path: '/wardrobe',\n color: 'bg-status-warning-bg-subtle text-status-warning-text',\n tourAttr: 'coolandlovely-wardrobe-link',\n },\n];\n\nconst modules: ModuleItem[] = [\n {\n labelKey: 'brands',\n descriptionKey: 'brandsDescription',\n label: 'Brands',\n description: 'Storefronts with a publish lifecycle',\n icon: <Store className=\"h-6 w-6\" />,\n path: '/brands',\n color: 'bg-accent-purple-subtle text-text-primary',\n tourAttr: 'coolandlovely-brands-link',\n },\n {\n labelKey: 'collections',\n descriptionKey: 'collectionsDescription',\n label: 'Collections',\n description: 'Seasonal drops & curated edits',\n icon: <Shapes className=\"h-6 w-6\" />,\n path: '/collections',\n color: 'bg-accent-blue-subtle text-accent-blue',\n },\n {\n labelKey: 'styleItems',\n descriptionKey: 'styleItemsDescription',\n label: 'Style Items',\n description: 'The shoppable catalogue',\n icon: <Shirt className=\"h-6 w-6\" />,\n path: '/items',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n },\n {\n labelKey: 'looks',\n descriptionKey: 'looksDescription',\n label: 'Looks',\n description: 'Published shop-the-look outfits',\n icon: <Sparkles className=\"h-6 w-6\" />,\n path: '/looks',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n {\n labelKey: 'collaborations',\n descriptionKey: 'collaborationsDescription',\n label: 'Collaborations',\n description: 'Brand × creator partnerships',\n icon: <Handshake className=\"h-6 w-6\" />,\n path: '/collaborations',\n color: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n {\n labelKey: 'wardrobe',\n descriptionKey: 'wardrobeDescription',\n label: 'Wardrobe',\n description: 'Your digital closet & wear log',\n icon: <Heart className=\"h-6 w-6\" />,\n path: '/wardrobe',\n color: 'bg-status-error-bg-subtle text-status-error-text',\n },\n {\n labelKey: 'lookbooks',\n descriptionKey: 'lookbooksDescription',\n label: 'Lookbooks',\n description: 'Themed boards of saved pieces',\n icon: <BookHeart className=\"h-6 w-6\" />,\n path: '/lookbooks',\n color: 'bg-accent-purple-subtle text-text-primary',\n },\n {\n labelKey: 'styleProfile',\n descriptionKey: 'styleProfileDescription',\n label: 'Style Profile',\n description: 'Your taste, palette & sizing',\n icon: <UserCircle className=\"h-6 w-6\" />,\n path: '/profile',\n color: 'bg-accent-blue-subtle text-accent-blue',\n tourAttr: 'coolandlovely-style-profile-link',\n },\n {\n labelKey: 'discover',\n descriptionKey: 'discoverDescription',\n label: 'Discover',\n description: 'A feed tuned to your taste',\n icon: <Compass className=\"h-6 w-6\" />,\n path: '/discover',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n tourAttr: 'coolandlovely-discover-feed',\n },\n {\n labelKey: 'trends',\n descriptionKey: 'trendsDescription',\n label: 'Trends',\n description: 'Live signals from platform activity',\n icon: <TrendingUp className=\"h-6 w-6\" />,\n path: '/trends',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n];\n\nexport function HomePage() {\n const { navigateTo, currentUser } = useCoolAndLovely();\n const tr = useTr();\n\n const greeting = useMemo(() => {\n const hour = new Date().getHours();\n if (hour < 12) return tr('coolandlovely.greeting.morning', 'Good morning');\n if (hour < 18) return tr('coolandlovely.greeting.afternoon', 'Good afternoon');\n return tr('coolandlovely.greeting.evening', 'Good evening');\n }, [tr]);\n\n const brands = useBrands();\n const items = useStyleItems();\n const looks = useLooks();\n const collaborations = useCollaborations();\n const wardrobe = useWardrobeItems();\n\n const firstName =\n currentUser?.firstName ?? currentUser?.name ?? tr('coolandlovely.greeting.there', 'there');\n\n const isLoading =\n brands.isLoading ||\n items.isLoading ||\n looks.isLoading ||\n collaborations.isLoading ||\n wardrobe.isLoading;\n\n const isError = brands.isError && items.isError && looks.isError && collaborations.isError;\n\n const stats = useMemo<DashboardStats>(() => {\n const brandItems = brands.data?.items ?? [];\n const styleItems = items.data?.items ?? [];\n const lookItems = looks.data?.items ?? [];\n const collabs = collaborations.data ?? [];\n const wardrobeItems = wardrobe.data ?? [];\n return {\n totalBrands: brandItems.length,\n publishedBrands: brandItems.filter((b) => b.status === 'PUBLISHED').length,\n totalStyleItems: styleItems.length,\n activeStyleItems: styleItems.filter((i) => i.status === 'ACTIVE').length,\n totalLooks: lookItems.length,\n featuredLooks: lookItems.filter((l) => l.isFeatured).length,\n activeCollaborations: collabs.filter(\n (c) => c.status === 'ACCEPTED' || c.status === 'IN_PROGRESS'\n ).length,\n wardrobeItems: wardrobeItems.length,\n };\n }, [brands.data, items.data, looks.data, collaborations.data, wardrobe.data]);\n\n const latestLooks = useMemo(() => [...(looks.data?.items ?? [])].slice(0, 5), [looks.data]);\n\n const recentBrands = useMemo(\n () =>\n [...(brands.data?.items ?? [])]\n .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())\n .slice(0, 4),\n [brands.data]\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('coolandlovely.appName', 'CoolAndLovely')}>\n <LoadingView\n label={tr('coolandlovely.loading.dashboard', 'Loading your style command center…')}\n />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={tr('coolandlovely.appName', 'CoolAndLovely')}>\n <ErrorView\n title={tr('coolandlovely.error.loadTitle', 'Could not load CoolAndLovely')}\n message={tr(\n 'coolandlovely.error.loadMessage',\n 'The fashion service did not respond. Please try again.'\n )}\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n void collaborations.refetch();\n void wardrobe.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n // Real (non-fabricated) 7-day sparkline series derived from each entity's\n // own createdAt — falls back to the plain text sub-label when there is no\n // meaningful history yet (e.g. everything was seeded at once).\n const brandSparkline = buildCreatedAtSparkline(brands.data?.items ?? []);\n const itemSparkline = buildCreatedAtSparkline(items.data?.items ?? []);\n const lookSparkline = buildCreatedAtSparkline(looks.data?.items ?? []);\n const collabSparkline = buildCreatedAtSparkline(collaborations.data ?? []);\n\n const metrics = [\n {\n icon: <Store className=\"h-5 w-5\" />,\n value: stats.totalBrands.toString(),\n labelKey: 'brands',\n label: 'Brands',\n sub:\n sparklineDeltaLabel(brandSparkline) ??\n tr('coolandlovely.metrics.publishedBrands', '{{count}} published', {\n count: stats.publishedBrands,\n }),\n sparklineData: brandSparkline,\n trend: sparklineTrendDirection(brandSparkline),\n },\n {\n icon: <Shirt className=\"h-5 w-5\" />,\n value: stats.totalStyleItems.toString(),\n labelKey: 'styleItems',\n label: 'Style Items',\n sub:\n sparklineDeltaLabel(itemSparkline) ??\n tr('coolandlovely.metrics.activeItems', '{{count}} active', {\n count: stats.activeStyleItems,\n }),\n sparklineData: itemSparkline,\n trend: sparklineTrendDirection(itemSparkline),\n },\n {\n icon: <Sparkles className=\"h-5 w-5\" />,\n value: stats.totalLooks.toString(),\n labelKey: 'looks',\n label: 'Looks',\n sub:\n sparklineDeltaLabel(lookSparkline) ??\n tr('coolandlovely.metrics.featuredLooks', '{{count}} featured', {\n count: stats.featuredLooks,\n }),\n sparklineData: lookSparkline,\n trend: sparklineTrendDirection(lookSparkline),\n },\n {\n icon: <Handshake className=\"h-5 w-5\" />,\n value: stats.activeCollaborations.toString(),\n labelKey: 'activeCollabs',\n label: 'Active Collabs',\n sub:\n sparklineDeltaLabel(collabSparkline) ??\n tr('coolandlovely.metrics.inWardrobe', '{{count}} in wardrobe', {\n count: stats.wardrobeItems,\n }),\n sparklineData: collabSparkline,\n trend: sparklineTrendDirection(collabSparkline),\n },\n ];\n\n return (\n <PageLayout\n title={`${greeting}, ${firstName}`}\n description={tr(\n 'coolandlovely.dashboard.subtitle',\n 'Your fashion & lifestyle command center'\n )}\n hero\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'coolandlovely.dashboard.purpose',\n 'CoolAndLovely connects the three sides of style on one workspace-native model: brands launch storefronts and seasonal collections, you build a wardrobe and lookbooks, and creators publish shop-the-look outfits and run brand collaborations. Jump into any module below — all data is live from the CoolAndLovely service.'\n )}\n </PagePurpose>\n\n {/* Headline metrics — the one emphasis zone for this page */}\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.atAGlance', 'At a glance')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m, index) => (\n <MetricsCard\n key={m.labelKey}\n icon={m.icon}\n value={m.value}\n label={tr(`coolandlovely.module.${m.labelKey}`, m.label)}\n trendValue={m.sub}\n trend={m.trend}\n sparklineData={m.sparklineData}\n featured={index === 0}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Quick Actions */}\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.quickActions', 'Quick actions')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {quickActions.map((action) => (\n <button\n key={action.labelKey}\n type=\"button\"\n onClick={() => navigateTo(action.path)}\n data-tour={action.tourAttr}\n className={cn(\n 'flex items-start gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left shadow-sm transition-all',\n 'hover:border-accent-foreground/20 hover:shadow-md'\n )}\n >\n <div\n className={cn(\n 'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg',\n action.color\n )}\n >\n {action.icon}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(`coolandlovely.quickAction.${action.labelKey}`, action.label)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr(`coolandlovely.quickAction.${action.descriptionKey}`, action.description)}\n </p>\n </div>\n <Plus className=\"mt-1 h-4 w-4 shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n </section>\n\n {/* Modules */}\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.explore', 'Explore')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {modules.map((mod) => (\n <button\n key={mod.labelKey}\n type=\"button\"\n onClick={() => navigateTo(mod.path)}\n data-tour={mod.tourAttr}\n className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-5 text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <div\n className={cn('flex h-12 w-12 items-center justify-center rounded-full', mod.color)}\n >\n {mod.icon}\n </div>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(`coolandlovely.module.${mod.labelKey}`, mod.label)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr(`coolandlovely.module.${mod.descriptionKey}`, mod.description)}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {/* Latest looks */}\n <section className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.latestLooks', 'Latest looks')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr('coolandlovely.actions.viewAll', 'View all')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {latestLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-6 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.empty.latestLooks',\n 'No looks published yet. Be the first to publish a shop-the-look outfit.'\n )}\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5\">\n {latestLooks.map((look) => (\n <LookCard\n key={look.id}\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Recent brands */}\n {recentBrands.length > 0 && (\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.recentBrands', 'Recently updated brands')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr('coolandlovely.actions.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {recentBrands.map((brand) => (\n <button\n key={brand.id}\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}`)}\n className=\"group flex items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-3 text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={<Store className=\"h-5 w-5\" />}\n seed={brand.id}\n fileId={brand.logo?.id ?? brand.cover?.id}\n alt={brand.name}\n className=\"h-11 w-11 shrink-0 rounded-full ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{brand.name}</p>\n {brand.isFeatured && (\n <Star className=\"h-3.5 w-3.5 shrink-0 fill-status-warning-text text-status-warning-text\" />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">@{brand.handle}</p>\n </div>\n <StatusBadge status={brand.status} />\n </button>\n ))}\n </div>\n </section>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmEA,IAAM,IAA8B;CAClC;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EACtC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACP,UAAU;EACX;CACF,EAEK,IAAwB;CAC5B;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACP,UAAU;EACX;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACpC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;EACtC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;EACvC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;EACvC,MAAM;EACN,OAAO;EACR;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACxC,MAAM;EACN,OAAO;EACP,UAAU;EACX;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;EACrC,MAAM;EACN,OAAO;EACP,UAAU;EACX;CACD;EACE,UAAU;EACV,gBAAgB;EAChB,OAAO;EACP,aAAa;EACb,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EACxC,MAAM;EACN,OAAO;EACR;CACF;AAED,SAAgB,IAAW;CACzB,IAAM,EAAE,eAAY,mBAAgB,GAAkB,EAChD,IAAK,GAAO,EAEZ,IAAW,QAAc;EAC7B,IAAM,qBAAO,IAAI,MAAM,EAAC,UAAU;AAGlC,SAFI,IAAO,KAAW,EAAG,kCAAkC,eAAe,GACtE,IAAO,KAAW,EAAG,oCAAoC,iBAAiB,GACvE,EAAG,kCAAkC,eAAe;IAC1D,CAAC,EAAG,CAAC,EAEF,IAAS,GAAW,EACpB,IAAQ,GAAe,EACvB,IAAQ,GAAU,EAClB,IAAiB,GAAmB,EACpC,IAAW,GAAkB,EAE7B,IACJ,GAAa,aAAa,GAAa,QAAQ,EAAG,gCAAgC,QAAQ,EAEtF,IACJ,EAAO,aACP,EAAM,aACN,EAAM,aACN,EAAe,aACf,EAAS,WAEL,IAAU,EAAO,WAAW,EAAM,WAAW,EAAM,WAAW,EAAe,SAE7E,IAAQ,QAA8B;EAC1C,IAAM,IAAa,EAAO,MAAM,SAAS,EAAE,EACrC,IAAa,EAAM,MAAM,SAAS,EAAE,EACpC,IAAY,EAAM,MAAM,SAAS,EAAE,EACnC,IAAU,EAAe,QAAQ,EAAE,EACnC,IAAgB,EAAS,QAAQ,EAAE;AACzC,SAAO;GACL,aAAa,EAAW;GACxB,iBAAiB,EAAW,QAAQ,MAAM,EAAE,WAAW,YAAY,CAAC;GACpE,iBAAiB,EAAW;GAC5B,kBAAkB,EAAW,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC;GAClE,YAAY,EAAU;GACtB,eAAe,EAAU,QAAQ,MAAM,EAAE,WAAW,CAAC;GACrD,sBAAsB,EAAQ,QAC3B,MAAM,EAAE,WAAW,cAAc,EAAE,WAAW,cAChD,CAAC;GACF,eAAe,EAAc;GAC9B;IACA;EAAC,EAAO;EAAM,EAAM;EAAM,EAAM;EAAM,EAAe;EAAM,EAAS;EAAK,CAAC,EAEvE,IAAc,QAAc,CAAC,GAAI,EAAM,MAAM,SAAS,EAAE,CAAE,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC,EAAM,KAAK,CAAC,EAErF,IAAe,QAEjB,CAAC,GAAI,EAAO,MAAM,SAAS,EAAE,CAAE,CAC5B,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,CACjF,MAAM,GAAG,EAAE,EAChB,CAAC,EAAO,KAAK,CACd;AAED,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAO,EAAG,yBAAyB,gBAAgB;YAC7D,kBAAC,GAAD,EACE,OAAO,EAAG,mCAAmC,qCAAqC,EAClF,CAAA;EACS,CAAA;AAIjB,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAO,EAAG,yBAAyB,gBAAgB;YAC7D,kBAAC,GAAD;GACE,OAAO,EAAG,iCAAiC,+BAA+B;GAC1E,SAAS,EACP,mCACA,yDACD;GACD,eAAe;AAKR,IAJA,EAAO,SAAS,EAChB,EAAM,SAAS,EACf,EAAM,SAAS,EACf,EAAe,SAAS,EACxB,EAAS,SAAS;;GAEzB,CAAA;EACS,CAAA;CAOjB,IAAM,IAAiB,EAAwB,EAAO,MAAM,SAAS,EAAE,CAAC,EAClE,IAAgB,EAAwB,EAAM,MAAM,SAAS,EAAE,CAAC,EAChE,IAAgB,EAAwB,EAAM,MAAM,SAAS,EAAE,CAAC,EAChE,IAAkB,EAAwB,EAAe,QAAQ,EAAE,CAAC,EAEpE,IAAU;EACd;GACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GACnC,OAAO,EAAM,YAAY,UAAU;GACnC,UAAU;GACV,OAAO;GACP,KACE,EAAoB,EAAe,IACnC,EAAG,yCAAyC,uBAAuB,EACjE,OAAO,EAAM,iBACd,CAAC;GACJ,eAAe;GACf,OAAO,EAAwB,EAAe;GAC/C;EACD;GACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GACnC,OAAO,EAAM,gBAAgB,UAAU;GACvC,UAAU;GACV,OAAO;GACP,KACE,EAAoB,EAAc,IAClC,EAAG,qCAAqC,oBAAoB,EAC1D,OAAO,EAAM,kBACd,CAAC;GACJ,eAAe;GACf,OAAO,EAAwB,EAAc;GAC9C;EACD;GACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GACtC,OAAO,EAAM,WAAW,UAAU;GAClC,UAAU;GACV,OAAO;GACP,KACE,EAAoB,EAAc,IAClC,EAAG,uCAAuC,sBAAsB,EAC9D,OAAO,EAAM,eACd,CAAC;GACJ,eAAe;GACf,OAAO,EAAwB,EAAc;GAC9C;EACD;GACE,MAAM,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;GACvC,OAAO,EAAM,qBAAqB,UAAU;GAC5C,UAAU;GACV,OAAO;GACP,KACE,EAAoB,EAAgB,IACpC,EAAG,oCAAoC,yBAAyB,EAC9D,OAAO,EAAM,eACd,CAAC;GACJ,eAAe;GACf,OAAO,EAAwB,EAAgB;GAChD;EACF;AAED,QACE,kBAAC,GAAD;EACE,OAAO,GAAG,EAAS,IAAI;EACvB,aAAa,EACX,oCACA,0CACD;EACD,MAAA;YANF;GAQE,kBAAC,GAAD;IAAa,WAAU;cACpB,EACC,mCACA,gUACD;IACW,CAAA;GAGd,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,qCAAqC,cAAc;KACpD,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAQ,KAAK,GAAG,MACf,kBAAC,GAAD;MAEE,MAAM,EAAE;MACR,OAAO,EAAE;MACT,OAAO,EAAG,wBAAwB,EAAE,YAAY,EAAE,MAAM;MACxD,YAAY,EAAE;MACd,OAAO,EAAE;MACT,eAAe,EAAE;MACjB,UAAU,MAAU;MACpB,EARK,EAAE,SAQP,CACF;KACE,CAAA,CACQ;;GAGhB,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,wCAAwC,gBAAgB;KACzD,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAa,KAAK,MACjB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,EAAO,KAAK;MACtC,aAAW,EAAO;MAClB,WAAW,EACT,oHACA,oDACD;gBARH;OAUE,kBAAC,OAAD;QACE,WAAW,EACT,kEACA,EAAO,MACR;kBAEA,EAAO;QACJ,CAAA;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAG,6BAA6B,EAAO,YAAY,EAAO,MAAM;SAC/D,CAAA,EACJ,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAG,6BAA6B,EAAO,kBAAkB,EAAO,YAAY;SAC3E,CAAA,CACA;;OACN,kBAAC,GAAD,EAAM,WAAU,6CAA8C,CAAA;OACvD;QA1BF,EAAO,SA0BL,CACT;KACE,CAAA,CACE;;GAGV,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,mCAAmC,UAAU;KAC9C,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAQ,KAAK,MACZ,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,EAAI,KAAK;MACnC,aAAW,EAAI;MACf,WAAU;gBALZ,CAOE,kBAAC,OAAD;OACE,WAAW,EAAG,2DAA2D,EAAI,MAAM;iBAElF,EAAI;OACD,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,wBAAwB,EAAI,YAAY,EAAI,MAAM;OACpD,CAAA,EACJ,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,wBAAwB,EAAI,kBAAkB,EAAI,YAAY;OAChE,CAAA,CACA,EAAA,CAAA,CACC;QAnBF,EAAI,SAmBF,CACT;KACE,CAAA,CACE;;GAGV,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAG,uCAAuC,eAAe;MACvD,CAAA,EACL,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,SAAS;MACnC,WAAU;gBAHZ;OAKG,EAAG,iCAAiC,WAAW;OAAC;OAAC,kBAAC,GAAD,EAAY,WAAU,eAAgB,CAAA;OACjF;QACL;QACL,EAAY,WAAW,IACtB,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,mCACA,0EACD;KACC,CAAA,GAEJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAY,KAAK,MAChB,kBAAC,GAAD;MAEE,IAAI,EAAK;MACT,OAAO,EAAK;MACZ,UAAU,EAAK;MACf,aAAa,EAAK,OAAO;MACzB,WAAW,EAAK;MAChB,aAAa,EAAK;MAClB,aAAa,EAAK;MAClB,cAAc,EAAW,UAAU,EAAK,KAAK;MAC7C,aAAA;MACA,EAVK,EAAK,GAUV,CACF;KACE,CAAA,CAEA;;GAGT,EAAa,SAAS,KACrB,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,wCAAwC,0BAA0B;KACnE,CAAA,EACL,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,UAAU;KACpC,WAAU;eAHZ;MAKG,EAAG,iCAAiC,WAAW;MAAE;MAClD,kBAAC,GAAD,EAAY,WAAU,eAAgB,CAAA;MAC/B;OACL;OACN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAa,KAAK,MACjB,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe,EAAW,WAAW,EAAM,KAAK;KAChD,WAAU;eAJZ;MAME,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;OACnC,MAAM,EAAM;OACZ,QAAQ,EAAM,MAAM,MAAM,EAAM,OAAO;OACvC,KAAK,EAAM;OACX,WAAU;OACV,CAAA;MACF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkD,EAAM;SAAS,CAAA,EAC7E,EAAM,cACL,kBAAC,GAAD,EAAM,WAAU,0EAA2E,CAAA,CAEzF;WACN,kBAAC,KAAD;QAAG,WAAU;kBAAb,CAAoD,KAAE,EAAM,OAAW;UACnE;;MACN,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA;MAC9B;OAtBF,EAAM,GAsBJ,CACT;IACE,CAAA,CACE,EAAA,CAAA;GAED"}
|
|
1
|
+
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/coolandlovely/pages/HomePage.tsx"],"sourcesContent":["/**\n * Overview / Home Page\n * @module coolandlovely/pages\n *\n * The CoolAndLovely command center landing page: a style-operating-system\n * dashboard rolling up live catalog (brands, items), social (looks,\n * collaborations) and the shopper's own wardrobe — all from real `coo*` ops.\n */\n\nimport { useMemo } from 'react';\nimport {\n Store,\n Shirt,\n Sparkles,\n Handshake,\n Shapes,\n Heart,\n ArrowRight,\n Star,\n Plus,\n BookHeart,\n UserCircle,\n Compass,\n TrendingUp,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { Thumbnail } from '../components/Thumbnail';\nimport { LookCard } from '../components/LookCard';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport {\n useBrands,\n useStyleItems,\n useLooks,\n useCollaborations,\n useWardrobeItems,\n useTr,\n} from '../hooks';\nimport { cn } from '../utils/cn';\nimport { buildCreatedAtSparkline, sparklineDeltaLabel, sparklineTrendDirection } from '../utils';\nimport type { DashboardStats } from '../types';\n\ninterface QuickAction {\n labelKey: string;\n descriptionKey: string;\n label: string;\n description: string;\n icon: React.ReactNode;\n path: string;\n color: string;\n tourAttr?: string;\n}\n\ninterface ModuleItem {\n labelKey: string;\n descriptionKey: string;\n label: string;\n description: string;\n icon: React.ReactNode;\n path: string;\n color: string;\n tourAttr?: string;\n}\n\nconst quickActions: QuickAction[] = [\n {\n labelKey: 'newBrand',\n descriptionKey: 'newBrandDescription',\n label: 'New Brand',\n description: 'Launch a storefront',\n icon: <Store className=\"h-5 w-5\" />,\n path: '/brands/new',\n color: 'bg-accent-purple-subtle text-text-primary',\n },\n {\n labelKey: 'addStyleItem',\n descriptionKey: 'addStyleItemDescription',\n label: 'Add Style Item',\n description: 'List a piece',\n icon: <Shirt className=\"h-5 w-5\" />,\n path: '/items/new',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n },\n {\n labelKey: 'publishLook',\n descriptionKey: 'publishLookDescription',\n label: 'Publish a Look',\n description: 'Shop-the-look outfit',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/looks/new',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n {\n labelKey: 'myWardrobe',\n descriptionKey: 'myWardrobeDescription',\n label: 'My Wardrobe',\n description: 'What you own',\n icon: <Heart className=\"h-5 w-5\" />,\n path: '/wardrobe',\n color: 'bg-status-warning-bg-subtle text-status-warning-text',\n tourAttr: 'coolandlovely-wardrobe-link',\n },\n];\n\nconst modules: ModuleItem[] = [\n {\n labelKey: 'brands',\n descriptionKey: 'brandsDescription',\n label: 'Brands',\n description: 'Storefronts with a publish lifecycle',\n icon: <Store className=\"h-6 w-6\" />,\n path: '/brands',\n color: 'bg-accent-purple-subtle text-text-primary',\n tourAttr: 'coolandlovely-brands-link',\n },\n {\n labelKey: 'collections',\n descriptionKey: 'collectionsDescription',\n label: 'Collections',\n description: 'Seasonal drops & curated edits',\n icon: <Shapes className=\"h-6 w-6\" />,\n path: '/collections',\n color: 'bg-accent-blue-subtle text-accent-blue',\n },\n {\n labelKey: 'styleItems',\n descriptionKey: 'styleItemsDescription',\n label: 'Style Items',\n description: 'The shoppable catalogue',\n icon: <Shirt className=\"h-6 w-6\" />,\n path: '/items',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n },\n {\n labelKey: 'looks',\n descriptionKey: 'looksDescription',\n label: 'Looks',\n description: 'Published shop-the-look outfits',\n icon: <Sparkles className=\"h-6 w-6\" />,\n path: '/looks',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n {\n labelKey: 'collaborations',\n descriptionKey: 'collaborationsDescription',\n label: 'Collaborations',\n description: 'Brand × creator partnerships',\n icon: <Handshake className=\"h-6 w-6\" />,\n path: '/collaborations',\n color: 'bg-status-warning-bg-subtle text-status-warning-text',\n },\n {\n labelKey: 'wardrobe',\n descriptionKey: 'wardrobeDescription',\n label: 'Wardrobe',\n description: 'Your digital closet & wear log',\n icon: <Heart className=\"h-6 w-6\" />,\n path: '/wardrobe',\n color: 'bg-status-error-bg-subtle text-status-error-text',\n },\n {\n labelKey: 'lookbooks',\n descriptionKey: 'lookbooksDescription',\n label: 'Lookbooks',\n description: 'Themed boards of saved pieces',\n icon: <BookHeart className=\"h-6 w-6\" />,\n path: '/lookbooks',\n color: 'bg-accent-purple-subtle text-text-primary',\n },\n {\n labelKey: 'styleProfile',\n descriptionKey: 'styleProfileDescription',\n label: 'Style Profile',\n description: 'Your taste, palette & sizing',\n icon: <UserCircle className=\"h-6 w-6\" />,\n path: '/profile',\n color: 'bg-accent-blue-subtle text-accent-blue',\n tourAttr: 'coolandlovely-style-profile-link',\n },\n {\n labelKey: 'discover',\n descriptionKey: 'discoverDescription',\n label: 'Discover',\n description: 'A feed tuned to your taste',\n icon: <Compass className=\"h-6 w-6\" />,\n path: '/discover',\n color: 'bg-[var(--color-accent-soft)] text-text-primary',\n tourAttr: 'coolandlovely-discover-feed',\n },\n {\n labelKey: 'trends',\n descriptionKey: 'trendsDescription',\n label: 'Trends',\n description: 'Live signals from platform activity',\n icon: <TrendingUp className=\"h-6 w-6\" />,\n path: '/trends',\n color: 'bg-status-success-bg-subtle text-status-success-text',\n },\n];\n\nexport function HomePage() {\n const { navigateTo, currentUser } = useCoolAndLovely();\n const tr = useTr();\n\n const greeting = useMemo(() => {\n const hour = new Date().getHours();\n if (hour < 12) return tr('coolandlovely.greeting.morning', 'Good morning');\n if (hour < 18) return tr('coolandlovely.greeting.afternoon', 'Good afternoon');\n return tr('coolandlovely.greeting.evening', 'Good evening');\n }, [tr]);\n\n const brands = useBrands();\n const items = useStyleItems();\n const looks = useLooks();\n const collaborations = useCollaborations();\n const wardrobe = useWardrobeItems();\n\n const firstName =\n currentUser?.firstName ?? currentUser?.name ?? tr('coolandlovely.greeting.there', 'there');\n\n const isLoading =\n brands.isLoading ||\n items.isLoading ||\n looks.isLoading ||\n collaborations.isLoading ||\n wardrobe.isLoading;\n\n const isError = brands.isError && items.isError && looks.isError && collaborations.isError;\n\n const stats = useMemo<DashboardStats>(() => {\n const brandItems = brands.data?.items ?? [];\n const styleItems = items.data?.items ?? [];\n const lookItems = looks.data?.items ?? [];\n const collabs = collaborations.data ?? [];\n const wardrobeItems = wardrobe.data ?? [];\n return {\n totalBrands: brandItems.length,\n publishedBrands: brandItems.filter((b) => b.status === 'PUBLISHED').length,\n totalStyleItems: styleItems.length,\n activeStyleItems: styleItems.filter((i) => i.status === 'ACTIVE').length,\n totalLooks: lookItems.length,\n featuredLooks: lookItems.filter((l) => l.isFeatured).length,\n activeCollaborations: collabs.filter(\n (c) => c.status === 'ACCEPTED' || c.status === 'IN_PROGRESS'\n ).length,\n wardrobeItems: wardrobeItems.length,\n };\n }, [brands.data, items.data, looks.data, collaborations.data, wardrobe.data]);\n\n const latestLooks = useMemo(() => [...(looks.data?.items ?? [])].slice(0, 5), [looks.data]);\n\n const recentBrands = useMemo(\n () =>\n [...(brands.data?.items ?? [])]\n .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())\n .slice(0, 4),\n [brands.data]\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('coolandlovely.appName', 'CoolAndLovely')}>\n <LoadingView\n label={tr('coolandlovely.loading.dashboard', 'Loading your style command center…')}\n />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={tr('coolandlovely.appName', 'CoolAndLovely')}>\n <ErrorView\n title={tr('coolandlovely.error.loadTitle', 'Could not load CoolAndLovely')}\n message={tr(\n 'coolandlovely.error.loadMessage',\n 'The fashion service did not respond. Please try again.'\n )}\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n void collaborations.refetch();\n void wardrobe.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n // Real (non-fabricated) 7-day sparkline series derived from each entity's\n // own createdAt — falls back to the plain text sub-label when there is no\n // meaningful history yet (e.g. everything was seeded at once).\n const brandSparkline = buildCreatedAtSparkline(brands.data?.items ?? []);\n const itemSparkline = buildCreatedAtSparkline(items.data?.items ?? []);\n const lookSparkline = buildCreatedAtSparkline(looks.data?.items ?? []);\n const collabSparkline = buildCreatedAtSparkline(collaborations.data ?? []);\n\n const metrics = [\n {\n icon: <Store className=\"h-5 w-5\" />,\n value: stats.totalBrands.toString(),\n labelKey: 'brands',\n label: 'Brands',\n sub:\n sparklineDeltaLabel(brandSparkline) ??\n tr('coolandlovely.metrics.publishedBrands', '{{count}} published', {\n count: stats.publishedBrands,\n }),\n sparklineData: brandSparkline,\n trend: sparklineTrendDirection(brandSparkline),\n },\n {\n icon: <Shirt className=\"h-5 w-5\" />,\n value: stats.totalStyleItems.toString(),\n labelKey: 'styleItems',\n label: 'Style Items',\n sub:\n sparklineDeltaLabel(itemSparkline) ??\n tr('coolandlovely.metrics.activeItems', '{{count}} active', {\n count: stats.activeStyleItems,\n }),\n sparklineData: itemSparkline,\n trend: sparklineTrendDirection(itemSparkline),\n },\n {\n icon: <Sparkles className=\"h-5 w-5\" />,\n value: stats.totalLooks.toString(),\n labelKey: 'looks',\n label: 'Looks',\n sub:\n sparklineDeltaLabel(lookSparkline) ??\n tr('coolandlovely.metrics.featuredLooks', '{{count}} featured', {\n count: stats.featuredLooks,\n }),\n sparklineData: lookSparkline,\n trend: sparklineTrendDirection(lookSparkline),\n },\n {\n icon: <Handshake className=\"h-5 w-5\" />,\n value: stats.activeCollaborations.toString(),\n labelKey: 'activeCollabs',\n label: 'Active Collabs',\n sub:\n sparklineDeltaLabel(collabSparkline) ??\n tr('coolandlovely.metrics.inWardrobe', '{{count}} in wardrobe', {\n count: stats.wardrobeItems,\n }),\n sparklineData: collabSparkline,\n trend: sparklineTrendDirection(collabSparkline),\n },\n ];\n\n return (\n <PageLayout\n title={`${greeting}, ${firstName}`}\n description={tr(\n 'coolandlovely.dashboard.subtitle',\n 'Your fashion & lifestyle command center'\n )}\n hero\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'coolandlovely.dashboard.purpose',\n 'CoolAndLovely connects the three sides of style on one workspace-native model: brands launch storefronts and seasonal collections, you build a wardrobe and lookbooks, and creators publish shop-the-look outfits and run brand collaborations. Jump into any module below — all data is live from the CoolAndLovely service.'\n )}\n </PagePurpose>\n\n {/* Headline metrics — the one emphasis zone for this page */}\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.atAGlance', 'At a glance')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m, index) => (\n <MetricsCard\n key={m.labelKey}\n icon={m.icon}\n value={m.value}\n label={tr(`coolandlovely.module.${m.labelKey}`, m.label)}\n trendValue={m.sub}\n trend={m.trend}\n sparklineData={m.sparklineData}\n featured={index === 0}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Quick Actions */}\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.quickActions', 'Quick actions')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {quickActions.map((action) => (\n <button\n key={action.labelKey}\n type=\"button\"\n onClick={() => navigateTo(action.path)}\n data-tour={action.tourAttr}\n className={cn(\n 'flex items-start gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left shadow-sm transition-all',\n 'hover:border-accent-foreground/20 hover:shadow-md'\n )}\n >\n <div\n className={cn(\n 'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg',\n action.color\n )}\n >\n {action.icon}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(`coolandlovely.quickAction.${action.labelKey}`, action.label)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr(`coolandlovely.quickAction.${action.descriptionKey}`, action.description)}\n </p>\n </div>\n <Plus className=\"mt-1 h-4 w-4 shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n </section>\n\n {/* Modules */}\n <section className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.explore', 'Explore')}\n </h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {modules.map((mod) => (\n <button\n key={mod.labelKey}\n type=\"button\"\n onClick={() => navigateTo(mod.path)}\n data-tour={mod.tourAttr}\n className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-5 text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <div\n className={cn('flex h-12 w-12 items-center justify-center rounded-full', mod.color)}\n >\n {mod.icon}\n </div>\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(`coolandlovely.module.${mod.labelKey}`, mod.label)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {tr(`coolandlovely.module.${mod.descriptionKey}`, mod.description)}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {/* Latest looks */}\n <section className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.latestLooks', 'Latest looks')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr('coolandlovely.actions.viewAll', 'View all')} <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {latestLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-6 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.empty.latestLooks',\n 'No looks published yet. Be the first to publish a shop-the-look outfit.'\n )}\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5\">\n {latestLooks.map((look) => (\n <LookCard\n key={look.id}\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Recent brands */}\n {recentBrands.length > 0 && (\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('coolandlovely.dashboard.recentBrands', 'Recently updated brands')}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr('coolandlovely.actions.viewAll', 'View all')}{' '}\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {recentBrands.map((brand) => (\n <button\n key={brand.id}\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}`)}\n className=\"group flex items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-3 text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={<Store className=\"h-5 w-5\" />}\n seed={brand.id}\n fileId={brand.logo?.id ?? brand.cover?.id}\n alt={brand.name}\n className=\"h-11 w-11 shrink-0 rounded-full ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{brand.name}</p>\n {brand.isFeatured && (\n <Star className=\"h-3.5 w-3.5 shrink-0 fill-status-warning-text text-status-warning-text\" />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">@{brand.handle}</p>\n </div>\n <StatusBadge status={brand.status} />\n </button>\n ))}\n </div>\n </section>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminBrandsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminBrandsPage.tsx"],"sourcesContent":["/**\n * Admin Brands Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for brand storefront approvals. Lists brands pending review\n * and lets admins approve (PUBLISHED) or reject (DRAFT) them.\n */\n\nimport { Check, Store, X } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { useBrands, useApproveBrand, useRejectBrand } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport type { CooBrand } from '../../types';\n\nfunction BrandReviewRow({ brand }: { brand: CooBrand }) {\n const approve = useApproveBrand();\n const reject = useRejectBrand();\n const isBusy = approve.isPending || reject.isPending;\n\n return (\n <div className=\"flex items-center gap-4 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <Thumbnail\n icon={<Store className=\"h-5 w-5\" />}\n seed={brand.id}\n fileId={brand.logo?.id ?? brand.cover?.id}\n alt={brand.name}\n className=\"h-12 w-12 shrink-0 rounded-lg\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{brand.name}</h3>\n <StatusBadge status={brand.status} />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">@{brand.handle}</p>\n {brand.tagline && (\n <p className=\"mt-1 truncate text-xs text-text-secondary\">{brand.tagline}</p>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => approve.mutate(brand.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-success-bg-subtle text-status-success-text hover:bg-status-success-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Check className=\"h-4 w-4\" /> Approve\n </button>\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => reject.mutate(brand.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-error-bg-subtle text-status-error-text hover:bg-status-error-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <X className=\"h-4 w-4\" /> Reject\n </button>\n </div>\n </div>\n );\n}\n\nexport function AdminBrandsPage() {\n const query = useBrands({ status: 'PENDING_REVIEW' });\n const brands = query.data?.items ?? [];\n\n return (\n <PageLayout title=\"Admin — Brands\" description=\"Approve or reject storefronts pending review\">\n {query.isLoading ? (\n <LoadingView label=\"Loading pending brands…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : brands.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Store className=\"h-10 w-10\" />}\n title=\"No pending brands\"\n description=\"All brand storefronts have been reviewed.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {brands.map((brand) => (\n <BrandReviewRow key={brand.id} brand={brand} />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAkBA,SAAS,EAAe,EAAE,YAA8B;CACtD,IAAM,IAAU,
|
|
1
|
+
{"version":3,"file":"AdminBrandsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminBrandsPage.tsx"],"sourcesContent":["/**\n * Admin Brands Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for brand storefront approvals. Lists brands pending review\n * and lets admins approve (PUBLISHED) or reject (DRAFT) them.\n */\n\nimport { Check, Store, X } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { useBrands, useApproveBrand, useRejectBrand } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport type { CooBrand } from '../../types';\n\nfunction BrandReviewRow({ brand }: { brand: CooBrand }) {\n const approve = useApproveBrand();\n const reject = useRejectBrand();\n const isBusy = approve.isPending || reject.isPending;\n\n return (\n <div className=\"flex items-center gap-4 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <Thumbnail\n icon={<Store className=\"h-5 w-5\" />}\n seed={brand.id}\n fileId={brand.logo?.id ?? brand.cover?.id}\n alt={brand.name}\n className=\"h-12 w-12 shrink-0 rounded-lg\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{brand.name}</h3>\n <StatusBadge status={brand.status} />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">@{brand.handle}</p>\n {brand.tagline && (\n <p className=\"mt-1 truncate text-xs text-text-secondary\">{brand.tagline}</p>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => approve.mutate(brand.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-success-bg-subtle text-status-success-text hover:bg-status-success-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Check className=\"h-4 w-4\" /> Approve\n </button>\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => reject.mutate(brand.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-error-bg-subtle text-status-error-text hover:bg-status-error-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <X className=\"h-4 w-4\" /> Reject\n </button>\n </div>\n </div>\n );\n}\n\nexport function AdminBrandsPage() {\n const query = useBrands({ status: 'PENDING_REVIEW' });\n const brands = query.data?.items ?? [];\n\n return (\n <PageLayout title=\"Admin — Brands\" description=\"Approve or reject storefronts pending review\">\n {query.isLoading ? (\n <LoadingView label=\"Loading pending brands…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : brands.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Store className=\"h-10 w-10\" />}\n title=\"No pending brands\"\n description=\"All brand storefronts have been reviewed.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {brands.map((brand) => (\n <BrandReviewRow key={brand.id} brand={brand} />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAkBA,SAAS,EAAe,EAAE,YAA8B;CACtD,IAAM,IAAU,EAAgB,GAC1B,IAAS,EAAe,GACxB,IAAS,EAAQ,aAAa,EAAO;CAE3C,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;IAClC,MAAM,EAAM;IACZ,QAAQ,EAAM,MAAM,MAAM,EAAM,OAAO;IACvC,KAAK,EAAM;IACX,WAAU;GACX,CAAA;GACD,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAoD,EAAM;MAAS,CAAA,GACjF,kBAAC,GAAD,EAAa,QAAQ,EAAM,OAAS,CAAA,CACjC;;KACL,kBAAC,KAAD;MAAG,WAAU;gBAAb,CAAoD,KAAE,EAAM,MAAU;;KACrE,EAAM,WACL,kBAAC,KAAD;MAAG,WAAU;gBAA6C,EAAM;KAAW,CAAA;IAE1E;;GACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,eAAe,EAAQ,OAAO,EAAM,EAAE;KACtC,WAAW,EACT,6EACA,6FACA,iDACF;eARF,CAUE,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA,GAAC,UACvB;QACR,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,eAAe,EAAO,OAAO,EAAM,EAAE;KACrC,WAAW,EACT,6EACA,uFACA,iDACF;eARF,CAUE,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA,GAAC,SACnB;MACL;;EACF;;AAET;AAEA,SAAgB,IAAkB;CAChC,IAAM,IAAQ,EAAU,EAAE,QAAQ,iBAAiB,CAAC,GAC9C,IAAS,EAAM,MAAM,SAAS,CAAC;CAErC,OACE,kBAAC,GAAD;EAAY,OAAM;EAAiB,aAAY;YAC5C,EAAM,YACL,kBAAC,GAAD,EAAa,OAAM,0BAA2B,CAAA,IAC5C,EAAM,UACR,kBAAC,GAAD,EAAW,eAAe,KAAK,EAAM,QAAQ,EAAI,CAAA,IAC/C,EAAO,WAAW,IACpB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAO,WAAU,YAAa,CAAA;GACpC,OAAM;GACN,aAAY;EACb,CAAA,IAED,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,MACX,kBAAC,GAAD,EAAsC,SAAQ,GAAzB,EAAM,EAAmB,CAC/C;EACE,CAAA;CAEG,CAAA;AAEhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminCollaborationsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminCollaborationsPage.tsx"],"sourcesContent":["/**\n * Admin Collaborations Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for brand × creator collaborations. Lists collaborations that\n * can be advanced through their lifecycle via APPROVE/REJECT admin decisions.\n */\n\nimport { useState } from 'react';\nimport { Check, FileText, Handshake, X } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { ActorName } from '../../components/ActorName';\nimport { useCollaborations, useReviewCollaboration } from '../../hooks';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooCollaboration, CooCollaborationStatus } from '../../types';\n\nconst REVIEWABLE_STATUSES: CooCollaborationStatus[] = ['PROPOSED', 'ACCEPTED', 'IN_PROGRESS'];\n\nconst STATUS_OPTIONS: Array<{ value: CooCollaborationStatus | 'ALL'; label: string }> = [\n { value: 'ALL', label: 'All' },\n { value: 'PROPOSED', label: 'Proposed' },\n { value: 'ACCEPTED', label: 'Accepted' },\n { value: 'IN_PROGRESS', label: 'In progress' },\n];\n\nfunction CollaborationReviewRow({ collaboration }: { collaboration: CooCollaboration }) {\n const review = useReviewCollaboration();\n const [notes, setNotes] = useState('');\n const isReviewable = REVIEWABLE_STATUSES.includes(collaboration.status);\n const isBusy = review.isPending;\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {collaboration.title}\n </h3>\n <StatusBadge status={collaboration.status} />\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {collaboration.brand?.name ?? 'Brand'} × <ActorName id={collaboration.creatorId} />\n </p>\n </div>\n </div>\n\n {collaboration.brief && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{collaboration.brief}</p>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {titleCase(collaboration.role)}\n </span>\n <span>{formatPrice(collaboration.compensation, collaboration.currency)}</span>\n </div>\n\n {isReviewable && (\n <div className=\"mt-4 space-y-3 border-t border-border-seam pt-3\">\n <div className=\"flex items-center gap-2\">\n <FileText className=\"h-4 w-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Optional review notes\"\n className=\"flex-1 rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div className=\"flex items-center justify-end gap-2\">\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() =>\n review.mutate({ id: collaboration.id, input: { decision: 'REJECT', notes } })\n }\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-error-bg-subtle text-status-error-text hover:bg-status-error-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <X className=\"h-4 w-4\" /> Reject\n </button>\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() =>\n review.mutate({ id: collaboration.id, input: { decision: 'APPROVE', notes } })\n }\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-success-bg-subtle text-status-success-text hover:bg-status-success-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Check className=\"h-4 w-4\" /> Approve\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport function AdminCollaborationsPage() {\n const [status, setStatus] = useState<CooCollaborationStatus | 'ALL'>('ALL');\n const query = useCollaborations({ status: status === 'ALL' ? undefined : status });\n const collaborations = query.data ?? [];\n\n return (\n <PageLayout\n title=\"Admin — Collaborations\"\n description=\"Review and advance brand × creator partnerships\"\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {STATUS_OPTIONS.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setStatus(opt.value)}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading collaborations…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : collaborations.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Handshake className=\"h-10 w-10\" />}\n title=\"No collaborations to review\"\n description=\"There are no collaborations matching this filter.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {collaborations.map((collab) => (\n <CollaborationReviewRow key={collab.id} collaboration={collab} />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,IAAM,IAAgD;CAAC;CAAY;CAAY;
|
|
1
|
+
{"version":3,"file":"AdminCollaborationsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminCollaborationsPage.tsx"],"sourcesContent":["/**\n * Admin Collaborations Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for brand × creator collaborations. Lists collaborations that\n * can be advanced through their lifecycle via APPROVE/REJECT admin decisions.\n */\n\nimport { useState } from 'react';\nimport { Check, FileText, Handshake, X } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { ActorName } from '../../components/ActorName';\nimport { useCollaborations, useReviewCollaboration } from '../../hooks';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooCollaboration, CooCollaborationStatus } from '../../types';\n\nconst REVIEWABLE_STATUSES: CooCollaborationStatus[] = ['PROPOSED', 'ACCEPTED', 'IN_PROGRESS'];\n\nconst STATUS_OPTIONS: Array<{ value: CooCollaborationStatus | 'ALL'; label: string }> = [\n { value: 'ALL', label: 'All' },\n { value: 'PROPOSED', label: 'Proposed' },\n { value: 'ACCEPTED', label: 'Accepted' },\n { value: 'IN_PROGRESS', label: 'In progress' },\n];\n\nfunction CollaborationReviewRow({ collaboration }: { collaboration: CooCollaboration }) {\n const review = useReviewCollaboration();\n const [notes, setNotes] = useState('');\n const isReviewable = REVIEWABLE_STATUSES.includes(collaboration.status);\n const isBusy = review.isPending;\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {collaboration.title}\n </h3>\n <StatusBadge status={collaboration.status} />\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {collaboration.brand?.name ?? 'Brand'} × <ActorName id={collaboration.creatorId} />\n </p>\n </div>\n </div>\n\n {collaboration.brief && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{collaboration.brief}</p>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {titleCase(collaboration.role)}\n </span>\n <span>{formatPrice(collaboration.compensation, collaboration.currency)}</span>\n </div>\n\n {isReviewable && (\n <div className=\"mt-4 space-y-3 border-t border-border-seam pt-3\">\n <div className=\"flex items-center gap-2\">\n <FileText className=\"h-4 w-4 text-text-secondary\" />\n <input\n type=\"text\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Optional review notes\"\n className=\"flex-1 rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div className=\"flex items-center justify-end gap-2\">\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() =>\n review.mutate({ id: collaboration.id, input: { decision: 'REJECT', notes } })\n }\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-error-bg-subtle text-status-error-text hover:bg-status-error-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <X className=\"h-4 w-4\" /> Reject\n </button>\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() =>\n review.mutate({ id: collaboration.id, input: { decision: 'APPROVE', notes } })\n }\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-success-bg-subtle text-status-success-text hover:bg-status-success-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Check className=\"h-4 w-4\" /> Approve\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport function AdminCollaborationsPage() {\n const [status, setStatus] = useState<CooCollaborationStatus | 'ALL'>('ALL');\n const query = useCollaborations({ status: status === 'ALL' ? undefined : status });\n const collaborations = query.data ?? [];\n\n return (\n <PageLayout\n title=\"Admin — Collaborations\"\n description=\"Review and advance brand × creator partnerships\"\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {STATUS_OPTIONS.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setStatus(opt.value)}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading collaborations…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : collaborations.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Handshake className=\"h-10 w-10\" />}\n title=\"No collaborations to review\"\n description=\"There are no collaborations matching this filter.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {collaborations.map((collab) => (\n <CollaborationReviewRow key={collab.id} collaboration={collab} />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|