Skip to content

Commit

Permalink
[tt] fix list progress and add global progress
Browse files Browse the repository at this point in the history
  • Loading branch information
a-type committed Oct 11, 2024
1 parent 65d50c3 commit 55547c7
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 107 deletions.
69 changes: 35 additions & 34 deletions apps/trip-tick/web/src/components/lists/AddListButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,45 @@ import { hooks } from '@/store.js';
import { Button, ButtonProps } from '@a-type/ui/components/button';
import { useMe } from '@biscuits/client';
import { useNavigate } from '@verdant-web/react-router';
import { ReactNode } from 'react';

export interface AddListButtonProps extends ButtonProps {}

export function AddListButton({
children,
className,
...rest
children,
className,
...rest
}: AddListButtonProps) {
const client = hooks.useClient();
const navigate = useNavigate();
const { data: me } = useMe();
const allLists = hooks.useAllLists();
const hasLists = allLists.length > 0;
const client = hooks.useClient();
const navigate = useNavigate();
const { data: me } = useMe();
const allLists = hooks.useAllLists();
const hasLists = allLists.length > 0;

return (
<Button
color="primary"
onClick={async () => {
if (hasLists) {
const list = await client.lists.put({
name: 'New list',
});
navigate(`/lists/${list.get('id')}`, {
skipTransition: true,
});
} else {
const listName = me ? me.me.name : `My stuff`;
const list = await client.lists.put({
name: listName,
});
navigate(`/lists/${list.get('id')}`);
}
}}
className={className}
{...rest}
>
{children || 'Add list'}
</Button>
);
return (
<Button
color="primary"
onClick={async () => {
if (hasLists) {
const list = await client.lists.put({
name: 'New list',
});
navigate(`/lists/${list.get('id')}`, {
skipTransition: true,
});
} else {
const listName = me ? me.me.name : `My stuff`;
const list = await client.lists.put({
name: listName,
});
navigate(`/lists/${list.get('id')}`, {
skipTransition: true,
});
}
}}
className={className}
{...rest}
>
{children || 'Add list'}
</Button>
);
}
25 changes: 25 additions & 0 deletions apps/trip-tick/web/src/components/trips/TripGlobalProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Progress, ProgressIndicator } from '@radix-ui/react-progress';
import { Trip } from '@trip-tick.biscuits/verdant';
import { useTripProgress } from './hooks.js';

export interface TripGlobalProgressProps {
trip: Trip;
}

export function TripGlobalProgress({ trip }: TripGlobalProgressProps) {
const { value } = useTripProgress(trip);

return (
<Progress
value={value}
className="w-full relative overflow-hidden border border-default rounded-full"
>
<ProgressIndicator
className="bg-accent w-full h-6px"
style={{
transform: `translateX(-${100 * (1 - value)}%`,
}}
/>
</Progress>
);
}
9 changes: 8 additions & 1 deletion apps/trip-tick/web/src/components/trips/TripView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { TripDateRange } from './TripDateRange.jsx';
import { ExtraItem, ListItem } from './TripItem.jsx';
import { quantityForecast } from './utils.js';
import { TripGlobalProgress } from './TripGlobalProgress.jsx';

export interface TripViewProps {
tripId: string;
Expand Down Expand Up @@ -108,6 +109,9 @@ function TripViewImpl({ trip }: { trip: Trip }) {
<div className="flex flex-col gap-4 w-full">
<TripViewInfo trip={trip} forecast={forecast} />
<TripViewChecklists trip={trip} forecast={forecast} />
<div className="fixed bottom-0 left-0 right-0 bg-wash p-1 w-full">
<TripGlobalProgress trip={trip} />
</div>
</div>
);
}
Expand Down Expand Up @@ -275,7 +279,10 @@ function ListTab({ trip, list }: { list: List; trip: Trip }) {
className="relative overflow-hidden flex-shrink-0"
>
<span className="text-nowrap">{list.get('name')}</span>
<Progress.Root className="w-full absolute bottom-0 left-0 overflow-hidden rounded-b-full border border-t-solid border-t-primary">
<Progress.Root
value={value}
className="w-full absolute bottom-0 left-0 overflow-hidden rounded-b-full border border-t-solid border-t-primary"
>
<Progress.Indicator
className="bg-accent w-full h-4px"
style={{
Expand Down
152 changes: 80 additions & 72 deletions apps/trip-tick/web/src/components/trips/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,87 +4,95 @@ import { Trip } from '@trip-tick.biscuits/verdant';
import differenceInDays from 'date-fns/differenceInDays';

export function useTripProgress(
trip: Trip,
{ listFilter }: { listFilter?: string[] } = {},
trip: Trip,
{ listFilter }: { listFilter?: string[] } = {},
) {
const { lists, completions, extraItems } = hooks.useWatch(trip);
const days = useTripDays(trip);
hooks.useWatch(lists, { deep: true });
hooks.useWatch(completions);
hooks.useWatch(extraItems);
const allLists = hooks.useAllLists();
const { lists, completions, extraItems } = hooks.useWatch(trip);
const days = useTripDays(trip);
hooks.useWatch(lists, { deep: true });
hooks.useWatch(completions);
hooks.useWatch(extraItems);
const allLists = hooks.useAllLists();

const mappedLists = lists
.map((id) => allLists.find((l) => l.get('id') === id))
.filter(function nonNil<T>(x: T | undefined): x is T {
return x !== undefined;
})
.filter((list) => {
if (!listFilter) {
return true;
}
return listFilter.includes(list.get('id'));
})
.map((list) => list.getSnapshot());
const mappedLists = lists
.map((id) => allLists.find((l) => l.get('id') === id))
.filter(function nonNil<T>(x: T | undefined): x is T {
return x !== undefined;
})
.filter((list) => {
if (!listFilter) {
return true;
}
return listFilter.includes(list.get('id'));
})
.map((list) => list.getSnapshot());

const extraItemCount = extraItems.values().reduce((acc, exList) => {
return exList.getAll().reduce((acc, ex) => {
return acc + ex.get('quantity');
}, acc);
}, 0);
const totalItems = mappedLists.reduce((acc, list) => {
return (
acc +
list.items.reduce((acc, item) => {
return (
acc +
getComputedQuantity({
quantity: item.quantity,
roundDown: item.roundDown,
days,
periodMultiplier: item.periodMultiplier,
additional: item.additional,
conditions: item.conditions,
period: item.period,
})
);
}, 0)
);
}, extraItemCount);
const extraItemCount = extraItems
.entries()
.filter(([listId]) => !listFilter || listFilter.includes(listId))
.reduce((acc, [_, exList]) => {
return exList.getAll().reduce((acc, ex) => {
return acc + ex.get('quantity');
}, acc);
}, 0);
const totalItems = mappedLists.reduce((acc, list) => {
return (
acc +
list.items.reduce((acc, item) => {
return (
acc +
getComputedQuantity({
quantity: item.quantity,
roundDown: item.roundDown,
days,
periodMultiplier: item.periodMultiplier,
additional: item.additional,
conditions: item.conditions,
period: item.period,
})
);
}, 0)
);
}, extraItemCount);

// starting from lists because completions may include data
// from lists that are no longer included
const completedItems = lists
.getSnapshot()
.filter((listId) => !listFilter || listFilter.includes(listId))
.reduce((acc, listId) => {
const list = allLists.find((l) => l.get('id') === listId);
if (!list) {
return acc;
}
const listItems = list.get('items');
const completedQuantities = listItems
.getSnapshot()
.reduce((acc2, item) => {
return acc2 + (completions.get(item.id!) ?? 0);
}, 0);
return acc + completedQuantities;
}, 0);
// starting from lists because completions may include data
// from lists that are no longer included
const completedItems = lists
.getSnapshot()
.filter((listId) => !listFilter || listFilter.includes(listId))
.reduce((acc, listId) => {
const list = allLists.find((l) => l.get('id') === listId);
if (!list) {
return acc;
}
const listItems = list.get('items');
let completedQuantities = listItems.getSnapshot().reduce((acc2, item) => {
return acc2 + (completions.get(item.id!) ?? 0);
}, 0);
// include extra items added to this list
const extras = extraItems.get(listId);
if (extras) {
completedQuantities += extras.getAll().reduce((acc2, ex) => {
return acc2 + (completions.get(ex.get('id')) ?? 0);
}, 0);
}
return acc + completedQuantities;
}, 0);

return {
totalItems,
completedItems,
value: completedItems / totalItems,
};
return {
totalItems,
completedItems,
value: completedItems / totalItems,
};
}

/**
* Total number of days, including the first day and the last day.
*/
export function useTripDays(trip: Trip) {
const { startsAt, endsAt } = hooks.useWatch(trip);
if (!startsAt || !endsAt) {
return 0;
}
return differenceInDays(endsAt, startsAt) + 1;
const { startsAt, endsAt } = hooks.useWatch(trip);
if (!startsAt || !endsAt) {
return 0;
}
return differenceInDays(endsAt, startsAt) + 1;
}

0 comments on commit 55547c7

Please sign in to comment.