Skip to content

Commit 20db272

Browse files
committed
Update Collective profile prototype
1 parent 7ca4a8b commit 20db272

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3029
-509
lines changed

components/crowdfunding-redesign/Accounts.tsx

+448
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import type { Column, ColumnDef, Row, TableMeta } from '@tanstack/react-table';
3+
import clsx from 'clsx';
4+
import { isNil, omit } from 'lodash';
5+
import { ArrowDown10, ArrowDownZA, ArrowUp10, ArrowUpZA } from 'lucide-react';
6+
import { FormattedMessage } from 'react-intl';
7+
8+
import type {
9+
AccountMetricsFragment,
10+
Currency,
11+
OverviewMetricsQueryVariables,
12+
} from '../../lib/graphql/types/v2/graphql';
13+
import type { useQueryFilterReturnType } from '../../lib/hooks/useQueryFilter';
14+
import { getCollectivePageRoute } from '../../lib/url-helpers';
15+
16+
import { AccountHoverCard } from '../AccountHoverCard';
17+
import Avatar from '../Avatar';
18+
import type { schema } from '../dashboard/sections/overview/CollectiveOverview';
19+
import type { MetricProps } from '../dashboard/sections/overview/Metric';
20+
import { ChangeBadge, getPercentageDifference } from '../dashboard/sections/overview/Metric';
21+
import FormattedMoneyAmount from '../FormattedMoneyAmount';
22+
import Link from '../Link';
23+
import { DataTable } from '../table/DataTable';
24+
import { Badge } from '../ui/Badge';
25+
import { Button } from '../ui/Button';
26+
import { Checkbox } from '../ui/Checkbox';
27+
import { AccountsSublist } from './AccountsSublist';
28+
29+
export default function AccountsList({ data, queryFilter, loading, metric }) {
30+
const currency = data?.account?.[metric.id]?.current?.currency;
31+
32+
const meta = {
33+
queryFilter,
34+
currency: currency,
35+
isAmount: !!metric.amount,
36+
metric,
37+
};
38+
39+
// if (error) {
40+
// return <MessageBoxGraphqlError error={error} />;
41+
// }
42+
43+
return (
44+
<div className="space-y-8">
45+
<AccountsSublist label="Main account" type="COLLECTIVE" data={data} metric={metric} meta={meta} />
46+
<AccountsSublist label="Projects" type="PROJECT" data={data} metric={metric} meta={meta} />
47+
48+
<AccountsSublist label="Events" type="EVENT" data={data} metric={metric} meta={meta} />
49+
</div>
50+
);
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import React from 'react';
2+
import type { Column, ColumnDef, Row, TableMeta } from '@tanstack/react-table';
3+
import clsx from 'clsx';
4+
import { isNil, omit } from 'lodash';
5+
import { ArrowDown10, ArrowDownZA, ArrowUp10, ArrowUpZA, ChevronRight } from 'lucide-react';
6+
import { FormattedMessage } from 'react-intl';
7+
8+
import type {
9+
AccountMetricsFragment,
10+
Currency,
11+
OverviewMetricsQueryVariables,
12+
} from '../../lib/graphql/types/v2/graphql';
13+
import type { useQueryFilterReturnType } from '../../lib/hooks/useQueryFilter';
14+
import { getCollectivePageRoute } from '../../lib/url-helpers';
15+
16+
import { AccountHoverCard } from '../AccountHoverCard';
17+
import Avatar from '../Avatar';
18+
import type { schema } from '../dashboard/sections/overview/CollectiveOverview';
19+
import type { MetricProps } from '../dashboard/sections/overview/Metric';
20+
import { ChangeBadge, getPercentageDifference } from '../dashboard/sections/overview/Metric';
21+
import FormattedMoneyAmount from '../FormattedMoneyAmount';
22+
import Link from '../Link';
23+
import { DataTable } from '../table/DataTable';
24+
import { Badge } from '../ui/Badge';
25+
import { Button } from '../ui/Button';
26+
import { Checkbox } from '../ui/Checkbox';
27+
import { useRouter } from 'next/router';
28+
type AccountMetricsRow = AccountMetricsFragment & {
29+
current: number;
30+
comparison?: number;
31+
percentageDifference?: number;
32+
};
33+
34+
interface AccountMetricsMeta extends TableMeta<AccountMetricsRow> {
35+
currency: Currency;
36+
isAmount: boolean;
37+
queryFilter: useQueryFilterReturnType<typeof schema, OverviewMetricsQueryVariables>;
38+
metric: MetricProps;
39+
}
40+
41+
const SortableHeader = ({
42+
column,
43+
label,
44+
type,
45+
align,
46+
}: {
47+
column: Column<AccountMetricsRow, unknown>;
48+
label: React.ReactNode;
49+
type?: 'alphabetic' | 'numerical';
50+
align?: 'left' | 'right';
51+
}) => {
52+
const isSorted = column.getIsSorted();
53+
const isSortedDesc = isSorted === 'desc';
54+
const UpIcon = type === 'alphabetic' ? ArrowUpZA : ArrowUp10;
55+
const DownIcon = type === 'alphabetic' ? ArrowDownZA : ArrowDown10;
56+
const SortIcon = isSortedDesc || !isSorted ? UpIcon : DownIcon;
57+
return (
58+
<div className={clsx('flex items-center', align === 'right' && 'justify-end')}>
59+
<Button
60+
variant="ghost"
61+
size="xs"
62+
className={clsx('group/btn -m-2 gap-2', isSorted && 'text-foreground')}
63+
onClick={() => column.toggleSorting(!isSortedDesc)}
64+
>
65+
<SortIcon
66+
className={clsx(
67+
'h-4 w-4 transition-colors',
68+
isSorted ? 'text-muted-foreground' : 'text-transparent group-hover/btn:text-muted-foreground',
69+
)}
70+
/>
71+
<span className={clsx(align === 'left' && '-order-1')}>{label}</span>
72+
</Button>
73+
</div>
74+
);
75+
};
76+
const columns: ColumnDef<AccountMetricsRow>[] = [
77+
{
78+
id: 'name',
79+
accessorKey: 'name',
80+
header: ({ column }) => (
81+
<SortableHeader
82+
column={column}
83+
type="alphabetic"
84+
label={<FormattedMessage defaultMessage="Name" id="HAlOn1" />}
85+
align="left"
86+
/>
87+
),
88+
meta: { className: 'min-w-0 max-w-[300px]' },
89+
90+
cell: ({ row, table }) => {
91+
const account = row.original;
92+
// const { queryFilter } = table.options.meta as AccountMetricsMeta;
93+
// const selectedAccountSlug = queryFilter.values.account;
94+
return (
95+
<div className="flex items-center gap-3 text-base">
96+
<div className="flex items-center gap-1.5 overflow-hidden">
97+
<AccountHoverCard
98+
account={account}
99+
trigger={
100+
<div className="max-w-[400px] truncate">
101+
<Link
102+
href={getCollectivePageRoute(account)}
103+
className={clsx('truncate hover:underline group-hover/row:text-foreground')}
104+
>
105+
{account.name}
106+
</Link>
107+
</div>
108+
}
109+
/>
110+
111+
{account.isArchived && (
112+
<Badge size="xs" className="capitalize">
113+
Archived {account.type.toLowerCase()}
114+
</Badge>
115+
)}
116+
</div>
117+
</div>
118+
);
119+
},
120+
},
121+
{
122+
id: 'current',
123+
accessorKey: 'current',
124+
meta: { className: 'text-right' },
125+
header: ({ column, table }) => {
126+
const meta = table.options.meta as AccountMetricsMeta;
127+
return <SortableHeader align="right" column={column} label={meta.metric.label} />;
128+
},
129+
sortingFn: (rowA: Row<AccountMetricsRow>, rowB: Row<AccountMetricsRow>): number => {
130+
const a = rowA.original.current;
131+
const b = rowB.original.current;
132+
133+
const diff = a - b;
134+
135+
// sort by comparison value if current is the same
136+
if (diff === 0) {
137+
const rowAPrevious = rowA.original.comparison;
138+
const rowBPrevious = rowB.original.comparison;
139+
return rowAPrevious - rowBPrevious;
140+
}
141+
return a - b;
142+
},
143+
cell: ({ cell, table }) => {
144+
const current = cell.getValue() as number;
145+
const meta = table.options.meta as AccountMetricsMeta;
146+
147+
return (
148+
<div className="flex items-center justify-end gap-2">
149+
<span className="text-base font-medium">
150+
{meta.isAmount ? (
151+
<FormattedMoneyAmount amount={current} currency={meta.currency} precision={2} showCurrencyCode={false} />
152+
) : (
153+
current.toLocaleString()
154+
)}
155+
</span>
156+
<ChevronRight size={20} className="text-muted-foreground" />
157+
</div>
158+
);
159+
},
160+
},
161+
];
162+
163+
export function AccountsSublist({ label, type, data, metric, meta }) {
164+
const router = useRouter();
165+
const columnData: AccountMetricsRow[] = React.useMemo(() => {
166+
const nodes = data
167+
? [omit(data?.account, 'childrenAccounts'), ...(data?.account.childrenAccounts.nodes ?? [])]
168+
: [];
169+
const filteredNodes = nodes.filter(node => node.type === type);
170+
171+
return filteredNodes.map(node => {
172+
const current = node[metric.id].current.valueInCents ?? node[metric.id].current;
173+
const comparison = node[metric.id].comparison?.valueInCents ?? node[metric.id].comparison;
174+
return {
175+
...node,
176+
current: Math.abs(current),
177+
comparison: !isNil(comparison) ? Math.abs(comparison) : undefined,
178+
percentageDifference: getPercentageDifference(current, comparison),
179+
};
180+
});
181+
}, [metric.id, data]);
182+
return (
183+
<div className="">
184+
<h2 className="mb-3 px-2 text-lg font-semibold text-slate-800">{label}</h2>
185+
<div className="flex flex-col divide-y overflow-hidden rounded-xl border bg-background">
186+
{columnData
187+
.sort((a, b) => b.current - a.current)
188+
.map(account => (
189+
<Link
190+
key={account.id}
191+
className="flex items-center justify-between px-4 py-4 hover:bg-muted"
192+
href={`/preview/${router.query.collectiveSlug}/finances/${account.slug}`}
193+
>
194+
<div>{account.name}</div>
195+
<div className="flex items-center gap-2">
196+
<div className="font-medium">
197+
<FormattedMoneyAmount
198+
amount={account.current}
199+
currency={meta.currency}
200+
precision={2}
201+
showCurrencyCode={false}
202+
/>
203+
</div>
204+
<ChevronRight size={20} className="text-muted-foreground" />
205+
</div>
206+
</Link>
207+
))}
208+
</div>
209+
210+
{/* <DataTable
211+
hideHeader
212+
className="bg-background"
213+
columns={columns}
214+
data={columnData}
215+
initialSort={[{ id: 'current', desc: true }]}
216+
nbPlaceholders={nbPlaceholders}
217+
loading={loading}
218+
meta={meta}
219+
/> */}
220+
</div>
221+
);
222+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useRouter } from 'next/router';
2+
import React from 'react';
3+
import Link from '../Link';
4+
import { Slash } from 'lucide-react';
5+
6+
const getPathdata = (router, collective, account) => {
7+
switch (router.pathname) {
8+
case '/preview/[collectiveSlug]/finances/[accountSlug]':
9+
return [{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' }];
10+
case '/preview/[collectiveSlug]/transactions/[groupId]':
11+
return [
12+
{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' },
13+
{
14+
href: `/preview/${router.query.collectiveSlug}/finances/${router.query.collectiveSlug}`,
15+
label: collective?.name,
16+
},
17+
];
18+
case '/preview/[collectiveSlug]/[accountSlug]/transactions/[groupId]':
19+
return [
20+
{ href: `/preview/${router.query.collectiveSlug}/finances`, label: 'Finances' },
21+
{
22+
href: `/preview/${router.query.collectiveSlug}/finances/${router.query.accountSlug}`,
23+
label: account?.name,
24+
},
25+
];
26+
case '/preview/[collectiveSlug]/projects/[accountSlug]':
27+
return [{ href: `/preview/${router.query.collectiveSlug}/projects`, label: 'Projects' }];
28+
case '/preview/[collectiveSlug]/events/[accountSlug]':
29+
return [{ href: `/preview/${router.query.collectiveSlug}/events`, label: 'Events' }];
30+
default:
31+
return [{ href: '', label: '' }];
32+
}
33+
};
34+
35+
export function Breadcrumb({ breadcrumbs }) {
36+
return (
37+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
38+
<Slash size={20} strokeWidth={1} />
39+
40+
{breadcrumbs?.map(({ href, label }, i, a) => {
41+
if (i === a.length - 1) {
42+
return (
43+
<span key={href} className="p-1 text-foreground">
44+
{label}
45+
</span>
46+
);
47+
}
48+
return (
49+
<React.Fragment key={href}>
50+
<Link href={href} className="rounded p-1 hover:bg-muted hover:text-foreground">
51+
<span className="text-muted-foreground">{label}</span>
52+
</Link>
53+
<Slash size={20} strokeWidth={1} />
54+
</React.Fragment>
55+
);
56+
})}
57+
</div>
58+
);
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { gql, useQuery } from '@apollo/client';
3+
import { useRouter } from 'next/router';
4+
import { API_V2_CONTEXT } from '../../lib/graphql/helpers';
5+
import Link from '../Link';
6+
import Avatar from '../Avatar';
7+
8+
const collectiveHeaderQuery = gql`
9+
query CollectiveHeader($slug: String!) {
10+
account(slug: $slug) {
11+
id
12+
slug
13+
name
14+
type
15+
... on AccountWithParent {
16+
parent {
17+
id
18+
slug
19+
name
20+
}
21+
}
22+
}
23+
}
24+
`;
25+
export function CollectiveHeader() {
26+
const router = useRouter();
27+
const { data, loading } = useQuery(collectiveHeaderQuery, {
28+
variables: { slug: router.query.accountSlug },
29+
context: API_V2_CONTEXT,
30+
});
31+
return (
32+
<div className="border-b bg-background">
33+
<div className="mx-auto flex h-16 max-w-screen-xl items-center justify-between px-6">
34+
<Link href={`/preview/${data?.account.parent?.slug || data?.account.slug}`} className="flex items-center gap-2">
35+
<Avatar className="" collective={data?.account.parent || data?.account} /> <span>{data?.account.name}</span>
36+
</Link>
37+
</div>
38+
</div>
39+
);
40+
}

0 commit comments

Comments
 (0)