Skip to content

Commit c571fdc

Browse files
committed
feat: Add duplicate pack UI (fixes #1605)
Adds frontend support for duplicating packs from 'All Packs' to 'My Packs'. Frontend changes: - New hook: useDuplicatePack.ts - handles API call and navigation - Updated PackCard.tsx - adds 'Duplicate' action for non-owned packs - Updated hooks/index.ts - exports new hook Users can now: - Browse 'All Packs' (public packs from other users) - Tap the actions menu on any pack - Select 'Duplicate' to copy it to their own packs - The duplicated pack opens immediately for editing The duplicated pack is private by default and can be customized after copying.
1 parent 42da402 commit c571fdc

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

apps/expo/features/packs/components/PackCard.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { router } from 'expo-router';
77
import { isArray } from 'radash';
88
import { useRef } from 'react';
99
import { Image, Pressable, View } from 'react-native';
10-
import { useDeletePack, usePackDetailsFromStore } from '../hooks';
10+
import { useDeletePack, useDuplicatePack, usePackDetailsFromStore } from '../hooks';
1111
import { usePackOwnershipCheck } from '../hooks/usePackOwnershipCheck';
1212
import type { Pack, PackInStore } from '../types';
1313

@@ -19,6 +19,7 @@ type PackCardProps = {
1919

2020
export function PackCard({ pack: packArg, onPress, isGenUI = false }: PackCardProps) {
2121
const deletePack = useDeletePack();
22+
const duplicatePack = useDuplicatePack();
2223
const { colors } = useColorScheme();
2324
const { showActionSheetWithOptions } = useActionSheet();
2425
const alertRef = useRef<AlertRef>(null);
@@ -27,15 +28,15 @@ export function PackCard({ pack: packArg, onPress, isGenUI = false }: PackCardPr
2728
const pack = (isOwnedByUser ? packFromStore : packArg) as Pack; // Use passed pack for non user owned pack.
2829

2930
const handleActionsPress = () => {
30-
const options =
31-
isOwnedByUser && !isGenUI
32-
? ['View Details', 'Edit', 'Delete', 'Cancel']
33-
: ['View Details', 'Cancel'];
31+
const options = isOwnedByUser
32+
? ['View Details', 'Edit', 'Delete', 'Cancel']
33+
: ['View Details', 'Duplicate', 'Cancel'];
3434

3535
const cancelButtonIndex = options.length - 1;
3636
const destructiveButtonIndex = options.indexOf('Delete');
3737
const viewDetailsIndex = 0;
3838
const editIndex = options.indexOf('Edit');
39+
const duplicateIndex = options.indexOf('Duplicate');
3940

4041
showActionSheetWithOptions(
4142
{
@@ -66,6 +67,9 @@ export function PackCard({ pack: packArg, onPress, isGenUI = false }: PackCardPr
6667
case editIndex:
6768
router.push({ pathname: '/pack/[id]/edit', params: { id: pack.id } });
6869
break;
70+
case duplicateIndex:
71+
duplicatePack.mutate({ packId: pack.id });
72+
break;
6973
case destructiveButtonIndex:
7074
alertRef.current?.alert({
7175
title: 'Delete pack?',

apps/expo/features/packs/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './useCreatePackWithItems';
77
export * from './useCurrentPack';
88
export * from './useDeletePack';
99
export * from './useDeletePackItem';
10+
export * from './useDuplicatePack';
1011
export * from './useDetailedPacks';
1112
export * from './useHasMinimumInventory';
1213
export * from './useImagePicker';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
import { useAuth } from 'expo-app/features/auth/hooks/useAuth';
3+
import { useRouter } from 'expo-router';
4+
5+
export function useDuplicatePack() {
6+
const { token } = useAuth();
7+
const queryClient = useQueryClient();
8+
const router = useRouter();
9+
10+
return useMutation({
11+
mutationFn: async ({
12+
packId,
13+
name,
14+
}: {
15+
packId: string;
16+
name?: string;
17+
}): Promise<{
18+
id: string;
19+
name: string;
20+
description: string | null;
21+
category: string;
22+
totalWeight: number;
23+
baseWeight: number;
24+
}> => {
25+
const response = await fetch(
26+
`${process.env.EXPO_PUBLIC_API_URL}/api/packs/${packId}/duplicate`,
27+
{
28+
method: 'POST',
29+
headers: {
30+
'Content-Type': 'application/json',
31+
Authorization: `Bearer ${token}`,
32+
},
33+
body: JSON.stringify({ name }),
34+
},
35+
);
36+
37+
if (!response.ok) {
38+
const error = await response.json();
39+
throw new Error(error.error || 'Failed to duplicate pack');
40+
}
41+
42+
return response.json();
43+
},
44+
onSuccess: (data) => {
45+
// Invalidate packs query to refresh the list
46+
queryClient.invalidateQueries({ queryKey: ['packs'] });
47+
// Navigate to the new pack
48+
router.push({ pathname: '/pack/[id]', params: { id: data.id } });
49+
},
50+
});
51+
}

0 commit comments

Comments
 (0)