@vendure-platform/create 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/assets/storefront/messages/de.json +2 -0
  3. package/dist/assets/storefront/messages/en.json +2 -0
  4. package/dist/assets/storefront/package.json +1 -1
  5. package/dist/assets/storefront/src/app/[locale]/(catalog)/product/[slug]/page.tsx +7 -2
  6. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/activity-feed.tsx +152 -0
  7. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/add-item-search.tsx +130 -0
  8. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/add-member-form.tsx +124 -0
  9. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/members-panel.tsx +240 -0
  10. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/page.tsx +47 -0
  11. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/rename-dialog.tsx +96 -0
  12. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/shared-cart-detail-client.tsx +555 -0
  13. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/actions.ts +260 -0
  14. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/new/new-shared-cart-form.tsx +77 -0
  15. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/new/page.tsx +47 -0
  16. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/page.tsx +263 -0
  17. package/dist/assets/storefront/src/app/[locale]/cart/order-summary.tsx +23 -0
  18. package/dist/assets/storefront/src/components/commerce/account-nav-items.ts +2 -0
  19. package/dist/assets/storefront/src/components/commerce/add-to-shared-cart.tsx +225 -0
  20. package/dist/assets/storefront/src/components/commerce/product-info.tsx +14 -1
  21. package/dist/assets/storefront/src/components/commerce/shared-cart-role.ts +39 -0
  22. package/dist/assets/storefront/src/components/commerce/shared-cart-status-badge.tsx +24 -0
  23. package/dist/assets/storefront/src/graphql-env.d.ts +18 -5
  24. package/dist/assets/storefront/src/lib/vendure/api.ts +8 -4
  25. package/dist/assets/storefront/src/lib/vendure/mutations.ts +96 -0
  26. package/dist/assets/storefront/src/lib/vendure/queries.ts +142 -0
  27. package/dist/assets/storefront/src/schema.graphql +134 -0
  28. package/dist/plugin-registry.js +1 -1
  29. package/package.json +2 -2
@@ -0,0 +1,240 @@
1
+ 'use client';
2
+
3
+ import {useState, useTransition} from 'react';
4
+ import {toast} from 'sonner';
5
+ import {Badge} from '@/components/ui/badge';
6
+ import {Button} from '@/components/ui/button';
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuRadioGroup,
12
+ DropdownMenuRadioItem,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuSub,
15
+ DropdownMenuSubContent,
16
+ DropdownMenuSubTrigger,
17
+ DropdownMenuTrigger,
18
+ } from '@/components/ui/dropdown-menu';
19
+ import {
20
+ AlertDialog,
21
+ AlertDialogAction,
22
+ AlertDialogCancel,
23
+ AlertDialogContent,
24
+ AlertDialogDescription,
25
+ AlertDialogFooter,
26
+ AlertDialogHeader,
27
+ AlertDialogTitle,
28
+ } from '@/components/ui/alert-dialog';
29
+ import {Crown, MoreHorizontal, ShieldCheck, UserMinus, Loader2} from 'lucide-react';
30
+ import {ROLE_OPTIONS, roleLabel} from '@/components/commerce/shared-cart-role';
31
+ import {
32
+ setSharedCartMemberRole,
33
+ removeSharedCartMember,
34
+ transferSharedCartOwnership,
35
+ type SharedCartData,
36
+ type SharedCartRole,
37
+ } from '../actions';
38
+ import {AddMemberDialog} from './add-member-form';
39
+
40
+ export function MembersPanel({
41
+ cart,
42
+ viewerId,
43
+ canManage,
44
+ onChanged,
45
+ }: {
46
+ cart: SharedCartData;
47
+ viewerId: string;
48
+ canManage: boolean;
49
+ onChanged: () => void;
50
+ }) {
51
+ const [transferTarget, setTransferTarget] = useState<{customerId: string; name: string} | null>(null);
52
+ const isOpen = cart.status === 'Open';
53
+ const existingIds = new Set(cart.members.map(m => m.customer.id));
54
+
55
+ return (
56
+ <section className="border rounded-lg">
57
+ <div className="px-4 py-3 border-b">
58
+ <h2 className="font-semibold">
59
+ Members <span className="text-muted-foreground font-normal">({cart.members.length})</span>
60
+ </h2>
61
+ </div>
62
+
63
+ <ul className="divide-y">
64
+ {cart.members.map(member => {
65
+ const isOwner = member.customer.id === cart.owner.id;
66
+ const isYou = member.customer.id === viewerId;
67
+ const name = `${member.customer.firstName} ${member.customer.lastName}`.trim();
68
+ const showMenu = canManage && isOpen && !isOwner;
69
+ return (
70
+ <li key={member.customer.id} className="flex items-center gap-2 px-4 py-3">
71
+ <div className="min-w-0 flex-1">
72
+ <p className="text-sm font-medium truncate">
73
+ {name}
74
+ {isYou && <span className="text-muted-foreground font-normal"> (you)</span>}
75
+ </p>
76
+ <p className="text-xs text-muted-foreground truncate">{member.customer.emailAddress}</p>
77
+ </div>
78
+
79
+ {isOwner ? (
80
+ <Badge className="shrink-0 gap-1">
81
+ <Crown className="h-3 w-3" />
82
+ Owner
83
+ </Badge>
84
+ ) : (
85
+ <Badge variant="outline" className="shrink-0">
86
+ {roleLabel(member.role)}
87
+ </Badge>
88
+ )}
89
+
90
+ {showMenu && (
91
+ <MemberMenu
92
+ cartId={cart.id}
93
+ member={member}
94
+ onChanged={onChanged}
95
+ onTransfer={() => setTransferTarget({customerId: member.customer.id, name})}
96
+ />
97
+ )}
98
+ </li>
99
+ );
100
+ })}
101
+ </ul>
102
+
103
+ {canManage && isOpen && (
104
+ <div className="px-4 py-3 border-t">
105
+ <AddMemberDialog cartId={cart.id} excludeCustomerIds={existingIds} onAdded={onChanged} />
106
+ </div>
107
+ )}
108
+
109
+ <TransferOwnershipDialog
110
+ cartId={cart.id}
111
+ target={transferTarget}
112
+ onClose={() => setTransferTarget(null)}
113
+ onTransferred={onChanged}
114
+ />
115
+ </section>
116
+ );
117
+ }
118
+
119
+ function MemberMenu({
120
+ cartId,
121
+ member,
122
+ onChanged,
123
+ onTransfer,
124
+ }: {
125
+ cartId: string;
126
+ member: SharedCartData['members'][number];
127
+ onChanged: () => void;
128
+ onTransfer: () => void;
129
+ }) {
130
+ const [isPending, startTransition] = useTransition();
131
+
132
+ function changeRole(role: SharedCartRole) {
133
+ if (role === member.role) return;
134
+ startTransition(async () => {
135
+ const result = await setSharedCartMemberRole(cartId, member.customer.id, role);
136
+ if (!result.success) toast.error(result.message ?? 'Could not change role.');
137
+ onChanged();
138
+ });
139
+ }
140
+
141
+ function remove() {
142
+ startTransition(async () => {
143
+ const result = await removeSharedCartMember(cartId, member.customer.id);
144
+ if (!result.success) toast.error(result.message ?? 'Could not remove member.');
145
+ else toast.success('Member removed.');
146
+ onChanged();
147
+ });
148
+ }
149
+
150
+ return (
151
+ <DropdownMenu>
152
+ <DropdownMenuTrigger asChild>
153
+ <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" disabled={isPending} aria-label="Member actions">
154
+ {isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <MoreHorizontal className="h-4 w-4" />}
155
+ </Button>
156
+ </DropdownMenuTrigger>
157
+ <DropdownMenuContent align="end" className="w-48">
158
+ <DropdownMenuSub>
159
+ <DropdownMenuSubTrigger>
160
+ <ShieldCheck className="h-4 w-4 mr-2" />
161
+ Change role
162
+ </DropdownMenuSubTrigger>
163
+ <DropdownMenuSubContent>
164
+ <DropdownMenuRadioGroup value={member.role} onValueChange={v => changeRole(v as SharedCartRole)}>
165
+ {ROLE_OPTIONS.map(o => (
166
+ <DropdownMenuRadioItem key={o.value} value={o.value}>
167
+ {o.label}
168
+ </DropdownMenuRadioItem>
169
+ ))}
170
+ </DropdownMenuRadioGroup>
171
+ </DropdownMenuSubContent>
172
+ </DropdownMenuSub>
173
+ <DropdownMenuItem onSelect={onTransfer}>
174
+ <Crown className="h-4 w-4 mr-2" />
175
+ Make owner
176
+ </DropdownMenuItem>
177
+ <DropdownMenuSeparator />
178
+ <DropdownMenuItem onSelect={remove} className="text-destructive focus:text-destructive">
179
+ <UserMinus className="h-4 w-4 mr-2" />
180
+ Remove
181
+ </DropdownMenuItem>
182
+ </DropdownMenuContent>
183
+ </DropdownMenu>
184
+ );
185
+ }
186
+
187
+ function TransferOwnershipDialog({
188
+ cartId,
189
+ target,
190
+ onClose,
191
+ onTransferred,
192
+ }: {
193
+ cartId: string;
194
+ target: {customerId: string; name: string} | null;
195
+ onClose: () => void;
196
+ onTransferred: () => void;
197
+ }) {
198
+ const [isPending, startTransition] = useTransition();
199
+
200
+ function confirm() {
201
+ if (!target) return;
202
+ startTransition(async () => {
203
+ const result = await transferSharedCartOwnership(cartId, target.customerId);
204
+ if (!result.success) {
205
+ toast.error(result.message ?? 'Could not transfer ownership.');
206
+ return;
207
+ }
208
+ toast.success(`${target.name} is now the owner.`);
209
+ onTransferred();
210
+ onClose();
211
+ });
212
+ }
213
+
214
+ return (
215
+ <AlertDialog open={target !== null} onOpenChange={open => !open && onClose()}>
216
+ <AlertDialogContent>
217
+ <AlertDialogHeader>
218
+ <AlertDialogTitle>Transfer ownership?</AlertDialogTitle>
219
+ <AlertDialogDescription>
220
+ {target?.name} will become the owner and gain full control of this cart. You&apos;ll keep your current
221
+ role.
222
+ </AlertDialogDescription>
223
+ </AlertDialogHeader>
224
+ <AlertDialogFooter>
225
+ <AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
226
+ <AlertDialogAction
227
+ onClick={e => {
228
+ e.preventDefault();
229
+ confirm();
230
+ }}
231
+ disabled={isPending}
232
+ >
233
+ {isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
234
+ Transfer ownership
235
+ </AlertDialogAction>
236
+ </AlertDialogFooter>
237
+ </AlertDialogContent>
238
+ </AlertDialog>
239
+ );
240
+ }
@@ -0,0 +1,47 @@
1
+ import type {Metadata} from 'next';
2
+ import {Suspense} from 'react';
3
+ import {client} from '@/lib/vendure/api';
4
+ import {GetSharedCartQuery} from '@/lib/vendure/queries';
5
+ import {getActiveCustomer} from '@/lib/vendure/actions';
6
+ import {redirect, Link} from '@/i18n/navigation';
7
+ import {setRequestLocale} from 'next-intl/server';
8
+ import {ArrowLeft} from 'lucide-react';
9
+ import {SharedCartDetailClient} from './shared-cart-detail-client';
10
+
11
+ export const metadata: Metadata = {
12
+ title: 'Shared Cart',
13
+ };
14
+
15
+ export default function SharedCartDetailPage(props: {params: Promise<{locale: string; id: string}>}) {
16
+ return (
17
+ <Suspense fallback={<p className="text-sm text-muted-foreground">Loading shared cart…</p>}>
18
+ <SharedCartDetailContent paramsPromise={props.params} />
19
+ </Suspense>
20
+ );
21
+ }
22
+
23
+ async function SharedCartDetailContent({paramsPromise}: {paramsPromise: Promise<{locale: string; id: string}>}) {
24
+ const {locale, id} = await paramsPromise;
25
+ setRequestLocale(locale);
26
+
27
+ const [activeCustomer, [result, error]] = await Promise.all([
28
+ getActiveCustomer(),
29
+ client.query(GetSharedCartQuery, {id}),
30
+ ]);
31
+
32
+ if (!activeCustomer) return await redirect('/sign-in');
33
+ if (error || !result.data.sharedCart) return await redirect('/account/shared-carts');
34
+
35
+ return (
36
+ <div>
37
+ <Link
38
+ href="/account/shared-carts"
39
+ className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
40
+ >
41
+ <ArrowLeft className="h-4 w-4" />
42
+ Shared carts
43
+ </Link>
44
+ <SharedCartDetailClient initialCart={result.data.sharedCart} viewerId={activeCustomer.id} locale={locale} />
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import {useState, useTransition} from 'react';
4
+ import {toast} from 'sonner';
5
+ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog';
6
+ import {Button} from '@/components/ui/button';
7
+ import {Input} from '@/components/ui/input';
8
+ import {Textarea} from '@/components/ui/textarea';
9
+ import {Label} from '@/components/ui/label';
10
+ import {Loader2} from 'lucide-react';
11
+ import {renameSharedCart} from '../actions';
12
+
13
+ export function RenameSharedCartDialog({
14
+ open,
15
+ onOpenChange,
16
+ cartId,
17
+ currentName,
18
+ currentDescription,
19
+ onRenamed,
20
+ }: {
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ cartId: string;
24
+ currentName: string;
25
+ currentDescription: string;
26
+ onRenamed: () => void;
27
+ }) {
28
+ const [name, setName] = useState(currentName);
29
+ const [description, setDescription] = useState(currentDescription);
30
+ const [wasOpen, setWasOpen] = useState(open);
31
+ const [isPending, startTransition] = useTransition();
32
+
33
+ // Re-seed the fields each time the dialog opens, without an effect (React's
34
+ // "adjust state during render" pattern).
35
+ if (open && !wasOpen) {
36
+ setWasOpen(true);
37
+ setName(currentName);
38
+ setDescription(currentDescription);
39
+ } else if (!open && wasOpen) {
40
+ setWasOpen(false);
41
+ }
42
+
43
+ function handleSubmit(e: React.FormEvent) {
44
+ e.preventDefault();
45
+ const trimmed = name.trim();
46
+ if (!trimmed) return;
47
+ startTransition(async () => {
48
+ const result = await renameSharedCart(cartId, trimmed, description.trim() || undefined);
49
+ if (!result.success) {
50
+ toast.error(result.message ?? 'Could not rename the cart.');
51
+ return;
52
+ }
53
+ toast.success('Cart updated.');
54
+ onRenamed();
55
+ onOpenChange(false);
56
+ });
57
+ }
58
+
59
+ return (
60
+ <Dialog open={open} onOpenChange={onOpenChange}>
61
+ <DialogContent>
62
+ <form onSubmit={handleSubmit}>
63
+ <DialogHeader>
64
+ <DialogTitle>Rename shared cart</DialogTitle>
65
+ <DialogDescription>Update the name and description your colleagues see.</DialogDescription>
66
+ </DialogHeader>
67
+ <div className="space-y-4 py-4">
68
+ <div className="space-y-2">
69
+ <Label htmlFor="rename-name">Name</Label>
70
+ <Input id="rename-name" value={name} onChange={e => setName(e.target.value)} maxLength={255} required autoFocus />
71
+ </div>
72
+ <div className="space-y-2">
73
+ <Label htmlFor="rename-description">Description</Label>
74
+ <Textarea
75
+ id="rename-description"
76
+ value={description}
77
+ onChange={e => setDescription(e.target.value)}
78
+ rows={3}
79
+ maxLength={1000}
80
+ />
81
+ </div>
82
+ </div>
83
+ <DialogFooter>
84
+ <Button type="button" variant="ghost" onClick={() => onOpenChange(false)} disabled={isPending}>
85
+ Cancel
86
+ </Button>
87
+ <Button type="submit" disabled={!name.trim() || isPending}>
88
+ {isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
89
+ Save
90
+ </Button>
91
+ </DialogFooter>
92
+ </form>
93
+ </DialogContent>
94
+ </Dialog>
95
+ );
96
+ }