-
Notifications
You must be signed in to change notification settings - Fork 38
feat(web-ui): extract shared shadcn-style package for apps/landing and apps/guides #2075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5a47586
4153474
5c04b2a
ba33f6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { packItemsStore, packsStore } from 'expo-app/features/packs/store'; | ||
| import { recordPackWeight } from 'expo-app/features/packs/store/packWeightHistory'; | ||
| import type { Pack, PackInput, PackInStore } from 'expo-app/features/packs/types'; | ||
| import { nanoid } from 'nanoid/non-secure'; | ||
| import { useCallback } from 'react'; | ||
|
|
||
| export function useCreatePackFromPack() { | ||
| const createPackFromPack = useCallback((sourcePack: Pack, packData: Partial<PackInput>) => { | ||
| const newPackId = nanoid(); | ||
| const timestamp = new Date().toISOString(); | ||
|
|
||
| // Create the new pack with custom data, falling back to source pack data | ||
| const newPack: PackInStore = { | ||
| id: newPackId, | ||
| name: packData.name || `${sourcePack.name} (Copy)`, | ||
| description: packData.description || sourcePack.description, | ||
| category: packData.category || sourcePack.category, | ||
| isPublic: packData.isPublic !== undefined ? packData.isPublic : false, // Default to private | ||
| image: packData.image !== undefined ? packData.image : sourcePack.image, | ||
| tags: packData.tags || sourcePack.tags || [], | ||
| localCreatedAt: timestamp, | ||
| localUpdatedAt: timestamp, | ||
| deleted: false, | ||
| }; | ||
|
|
||
| // @ts-ignore: Safe because Legend-State uses Proxy | ||
| packsStore[newPackId].set(newPack); | ||
|
Comment on lines
+26
to
+27
|
||
|
|
||
| // Copy each item from the source pack | ||
| if (sourcePack.items) { | ||
| for (const item of sourcePack.items) { | ||
| if (!item.deleted) { | ||
| const newItemId = nanoid(); | ||
| const newItem = { | ||
| ...item, | ||
| id: newItemId, | ||
| packId: newPackId, | ||
| deleted: false, | ||
| createdAt: undefined, // Reset server timestamps | ||
| updatedAt: undefined, | ||
| }; | ||
|
|
||
| // @ts-ignore: Safe because Legend-State uses Proxy | ||
| packItemsStore[newItemId].set(newItem); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Recalculate pack weight | ||
| recordPackWeight(newPackId); | ||
|
|
||
| return newPackId; | ||
| }, []); | ||
|
|
||
| return createPackFromPack; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||
| import axiosInstance from 'expo-app/lib/api/client'; | ||
| import { useCallback } from 'react'; | ||
| import type { Pack, PackInput } from '../types'; | ||
| import { useCreatePackFromPack } from './useCreatePackFromPack'; | ||
|
|
||
| export function useDuplicatePack() { | ||
| const createPackFromPack = useCreatePackFromPack(); | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| const duplicatePackMutation = useMutation({ | ||
| mutationFn: async ({ | ||
| packId, | ||
| packData = {}, | ||
| }: { | ||
| packId: string; | ||
| packData?: Partial<PackInput>; | ||
| }) => { | ||
| // First, fetch the full pack details with items | ||
| const response = await queryClient.fetchQuery({ | ||
| queryKey: ['pack', packId], | ||
| queryFn: async () => { | ||
| const res = await axiosInstance.get(`/api/packs/${packId}`); | ||
| return res.data as Pack; | ||
| }, | ||
| }); | ||
|
|
||
| // Create the new pack from the fetched pack | ||
| const newPackId = createPackFromPack(response, packData); | ||
| return newPackId; | ||
| }, | ||
| onError: (error) => { | ||
| console.error('Error duplicating pack:', error); | ||
| }, | ||
| }); | ||
|
|
||
| const duplicatePack = useCallback( | ||
| (packId: string, packData?: Partial<PackInput>) => { | ||
| return duplicatePackMutation.mutateAsync({ packId, packData }); | ||
| }, | ||
| [duplicatePackMutation], | ||
| ); | ||
|
|
||
| return { | ||
| duplicatePack, | ||
| isLoading: duplicatePackMutation.isPending, | ||
| error: duplicatePackMutation.error, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,6 +57,22 @@ export const TripForm = ({ trip }: { trip?: Trip }) => { | |||||||||||||||||||||||||||||||||||
| const { location, setLocation } = useTripLocation(); | ||||||||||||||||||||||||||||||||||||
| const packs = usePacks(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Initialize location store with trip's location when component mounts or | ||||||||||||||||||||||||||||||||||||
| // trip ID changes. We intentionally depend only on trip?.id (not trip?.location) | ||||||||||||||||||||||||||||||||||||
| // so that after the user picks a new location via location-search, a | ||||||||||||||||||||||||||||||||||||
| // re-render of the same trip object does not overwrite their selection in | ||||||||||||||||||||||||||||||||||||
| // the store. | ||||||||||||||||||||||||||||||||||||
| // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — see comment above; reseeding on trip?.location would stomp user-picked values | ||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||
| // Set location from trip, or null if trip has no location | ||||||||||||||||||||||||||||||||||||
| setLocation(trip?.location ?? null); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Cleanup: clear location when component unmounts | ||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+71
|
||||||||||||||||||||||||||||||||||||
| // Set location from trip, or null if trip has no location | |
| setLocation(trip?.location ?? null); | |
| // Cleanup: clear location when component unmounts | |
| return () => { | |
| const nextLocation = trip?.location ?? null; | |
| // Defer the seed so the final mounted store state matches the current | |
| // trip, even if another mount-only effect in this component clears the | |
| // shared location store during the same commit. | |
| const timeoutId = setTimeout(() => { | |
| setLocation(nextLocation); | |
| }, 0); | |
| // Cleanup: cancel pending seed and clear location when component unmounts | |
| return () => { | |
| clearTimeout(timeoutId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packData.name/packData.descriptionare merged using||, which will treat empty strings as “not provided” and fall back to the source pack values. If callers ever intentionally pass an empty string (e.g., to clear description), this will be impossible.Use nullish coalescing (
??) for string fields so onlyundefined/nulltrigger the fallback.