Skip to content

Commit

Permalink
refactor(website): simplify api interaction (#775)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Mar 17, 2024
1 parent 2c73f0a commit 1c42962
Show file tree
Hide file tree
Showing 27 changed files with 623 additions and 511 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
'use client';

import { DefaultParams } from '@/app/[lang]/[region]';
import { UserContext } from '@/app/[lang]/[region]/(website)/me/user-context-provider';
import { useContributions } from '@/app/[lang]/[region]/(website)/me/hooks';
import { SpinnerIcon } from '@/components/logos/spinner-icon';
import { useTranslator } from '@/hooks/useTranslator';
import { orderBy } from '@firebase/firestore';
import { CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '@socialincome/shared/src/types/contribution';
import { USER_FIRESTORE_PATH } from '@socialincome/shared/src/types/user';
import { toDateTime } from '@socialincome/shared/src/utils/date';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@socialincome/ui';
import { useQuery } from '@tanstack/react-query';
import { collection, getDocs, query, where } from 'firebase/firestore';
import _ from 'lodash';
import { useContext } from 'react';
import { useFirestore } from 'reactfire';

type ContributionsTableProps = {
translations: {
Expand All @@ -23,24 +17,12 @@ type ContributionsTableProps = {
} & DefaultParams;

export function ContributionsTable({ lang, translations }: ContributionsTableProps) {
const firestore = useFirestore();
const translator = useTranslator(lang, 'website-me');
const { user } = useContext(UserContext);
const { data: contributions } = useQuery({
queryKey: ['ContributionsTable', user, firestore],
queryFn: async () => {
if (user && firestore) {
return await getDocs(
query(
collection(firestore, USER_FIRESTORE_PATH, user.id, CONTRIBUTION_FIRESTORE_PATH),
where('status', '==', StatusKey.SUCCEEDED),
orderBy('created', 'desc'),
),
);
} else return null;
},
staleTime: 1000 * 60 * 60, // 1 hour
});
const { contributions, loading } = useContributions();

if (loading) {
return <SpinnerIcon />;
}

return (
<Table>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DefaultPageProps } from '@/app/[lang]/[region]';
import { ContributionsTable } from '@/app/[lang]/[region]/(website)/me/payments/contributions-table';
import { ContributionsTable } from '@/app/[lang]/[region]/(website)/me/contributions/contributions-table';
import { PlusCircleIcon } from '@heroicons/react/24/outline';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { Button } from '@socialincome/ui';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
'use client';

import { DefaultParams } from '@/app/[lang]/[region]';
import { UserContext } from '@/app/[lang]/[region]/(website)/me/user-context-provider';
import { useTranslator } from '@/hooks/useTranslator';
import { orderBy } from '@firebase/firestore';
import { DONATION_CERTIFICATE_FIRESTORE_PATH } from '@socialincome/shared/src/types/donation-certificate';
import { USER_FIRESTORE_PATH } from '@socialincome/shared/src/types/user';
import { useDonationCertificates } from '@/app/[lang]/[region]/(website)/me/hooks';
import { SpinnerIcon } from '@/components/logos/spinner-icon';
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@socialincome/ui';
import { useQuery } from '@tanstack/react-query';
import { collection, getDocs, query } from 'firebase/firestore';
import Link from 'next/link';
import { useContext } from 'react';
import { useFirestore } from 'reactfire';

type ContributionsTableProps = {
translations: {
Expand All @@ -21,24 +14,12 @@ type ContributionsTableProps = {
};
} & DefaultParams;

export function DonationCertificatesTable({ lang, translations }: ContributionsTableProps) {
const firestore = useFirestore();
const translator = useTranslator(lang, 'website-me');
const { user } = useContext(UserContext);
const { data: donationCertificates } = useQuery({
queryKey: ['DonationCertificatesTable', user, firestore],
queryFn: async () => {
if (user && firestore) {
return await getDocs(
query(
collection(firestore, USER_FIRESTORE_PATH, user.id, DONATION_CERTIFICATE_FIRESTORE_PATH),
orderBy('year', 'desc'),
),
);
} else return null;
},
staleTime: 1000 * 60 * 60, // 1 hour
});
export function DonationCertificatesTable({ translations }: ContributionsTableProps) {
const { donationCertificates, loading } = useDonationCertificates();

if (loading) {
return <SpinnerIcon />;
}

if (donationCertificates?.size === 0) {
return <Typography dangerouslySetInnerHTML={{ __html: translations.noCertificatesYet }} />;
Expand Down
181 changes: 181 additions & 0 deletions website/src/app/[lang]/[region]/(website)/me/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { UserContext } from '@/components/providers/user-context-provider';
import { useApi } from '@/hooks/useApi';
import { orderBy } from '@firebase/firestore';
import { Status } from '@mailchimp/mailchimp_marketing';
import { CONTRIBUTION_FIRESTORE_PATH, StatusKey } from '@socialincome/shared/src/types/contribution';
import { DONATION_CERTIFICATE_FIRESTORE_PATH } from '@socialincome/shared/src/types/donation-certificate';
import { EMPLOYERS_FIRESTORE_PATH, Employer } from '@socialincome/shared/src/types/employers';
import { USER_FIRESTORE_PATH } from '@socialincome/shared/src/types/user';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Timestamp, addDoc, collection, deleteDoc, doc, getDocs, query, updateDoc, where } from 'firebase/firestore';
import { useContext } from 'react';
import { useFirestore } from 'reactfire';
import Stripe from 'stripe';

export const useUser = () => {
const user = useContext(UserContext);
if (!user) {
throw new Error('useUserContext must be used within a UserContextProvider');
}
return user;
};

export const useContributions = () => {
const firestore = useFirestore();
const user = useUser();
const {
data: contributions,
isLoading,
isRefetching,
error,
} = useQuery({
queryKey: ['me/contributions'],
queryFn: async () => {
return await getDocs(
query(
collection(firestore, USER_FIRESTORE_PATH, user.id, CONTRIBUTION_FIRESTORE_PATH),
where('status', '==', StatusKey.SUCCEEDED),
orderBy('created', 'desc'),
),
);
},
staleTime: 1000 * 60 * 60, // 1 hour
});
return { contributions, loading: isLoading || isRefetching, error };
};

export const useSubscriptions = () => {
const api = useApi();
const {
data: subscriptions,
isLoading,
isRefetching,
error,
} = useQuery({
queryKey: ['me/subscriptions'],
queryFn: async () => {
const response = await api.get('/api/stripe/subscriptions');
return (await response.json()) as Stripe.Subscription[];
},
staleTime: 1000 * 60 * 60, // 1 hour
});
return { subscriptions, loading: isLoading || isRefetching, error };
};

export const useDonationCertificates = () => {
const firestore = useFirestore();
const user = useUser();
const {
data: donationCertificates,
isLoading,
isRefetching,
error,
} = useQuery({
queryKey: ['me/donation-certificates'],
queryFn: async () => {
return await getDocs(
query(
collection(firestore, USER_FIRESTORE_PATH, user.id, DONATION_CERTIFICATE_FIRESTORE_PATH),
orderBy('year', 'desc'),
),
);
},
staleTime: 1000 * 60 * 60, // 1 hour
});
return { donationCertificates, loading: isLoading || isRefetching, error };
};

// Mailchimp
export const useMailchimpSubscription = () => {
const api = useApi();
const {
data: status,
isLoading,
isRefetching,
error,
} = useQuery<string | null>({
queryKey: ['me/mailchimp'],
queryFn: async () => {
const response = await api.get('/api/mailchimp/subscription');
return (await response.json()).status;
},
staleTime: 1000 * 60 * 60, // 1 hour
});
return { status, loading: isLoading || isRefetching, error };
};

export const useUpsertMailchimpSubscription = () => {
const api = useApi();
const queryClient = useQueryClient();

return async (status: Status) => {
const response = await api.post('/api/mailchimp/subscription', { status });
queryClient.invalidateQueries({ queryKey: ['me/mailchimp'] });
return response;
};
};

export type EmployerWithId = {
id: string;
} & Employer;

export const useEmployers = () => {
const firestore = useFirestore();
const user = useUser();

const {
data: employers,
isLoading,
isRefetching,
error,
} = useQuery({
queryKey: ['me/employers'],
queryFn: async () => {
const data = await getDocs(
query(collection(firestore, USER_FIRESTORE_PATH, user.id, 'employers'), orderBy('created', 'desc')),
);
return data!.docs.map((e) => ({ id: e.id, ...e.data() }) as EmployerWithId);
},
staleTime: 1000 * 60 * 60, // 1 hour
});
return { employers, loading: isLoading || isRefetching, error };
};

export const useArchiveEmployer = () => {
const firestore = useFirestore();
const user = useUser();
const queryClient = useQueryClient();

return async (employer_id: string) => {
const employerRef = doc(firestore, USER_FIRESTORE_PATH, user!.id, 'employers', employer_id);
await updateDoc(employerRef, { is_current: false });
queryClient.invalidateQueries({ queryKey: ['me/employers'] });
};
};

export const useDeleteEmployer = () => {
const firestore = useFirestore();
const user = useUser();
const queryClient = useQueryClient();

return async (employer_id: string) => {
const employerRef = doc(firestore, USER_FIRESTORE_PATH, user!.id, 'employers', employer_id);
await deleteDoc(employerRef);
queryClient.invalidateQueries({ queryKey: ['me/employers'] });
};
};

export const useAddEmployer = () => {
const firestore = useFirestore();
const user = useUser();
const queryClient = useQueryClient();

return async (employer_name: string) => {
await addDoc(collection(firestore, USER_FIRESTORE_PATH, user.id, EMPLOYERS_FIRESTORE_PATH), {
employer_name: employer_name,
is_current: true,
created: Timestamp.now(),
});
queryClient.invalidateQueries({ queryKey: ['me/employers'] });
};
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { DefaultParams } from '@/app/[lang]/[region]';
import { UserContext } from '@/app/[lang]/[region]/(website)/me/user-context-provider';
import { UserContext } from '@/components/providers/user-context-provider';
import {
ArrowPathIcon,
BriefcaseIcon,
Expand Down Expand Up @@ -59,14 +59,14 @@ type LayoutClientProps = {

export function LayoutClient({ params, translations, children }: PropsWithChildren<LayoutClientProps>) {
const pathname = usePathname();
const { user } = useContext(UserContext);
const user = useContext(UserContext);
const [isOpen, setIsOpen] = useState(false);

const navigationMenu = (
<ul className="pr-4">
<NavigationSectionTitle>{translations.contributionsTitle}</NavigationSectionTitle>
<NavigationLink
href={`/${params.lang}/${params.region}/me/payments`}
href={`/${params.lang}/${params.region}/me/contributions`}
Icon={CurrencyDollarIcon}
onClick={() => setIsOpen(false)}
>
Expand Down Expand Up @@ -122,7 +122,7 @@ export function LayoutClient({ params, translations, children }: PropsWithChildr
case `/${params.lang}/${params.region}/me/security`:
title = translations.security;
break;
case `/${params.lang}/${params.region}/me/payments`:
case `/${params.lang}/${params.region}/me/contributions`:
title = translations.payments;
break;
case `/${params.lang}/${params.region}/me/subscriptions`:
Expand Down
39 changes: 21 additions & 18 deletions website/src/app/[lang]/[region]/(website)/me/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DefaultLayoutProps } from '@/app/[lang]/[region]';
import { LayoutClient } from '@/app/[lang]/[region]/(website)/me/layout-client';
import { UserContextProvider } from '@/app/[lang]/[region]/(website)/me/user-context-provider';
import { ApiProvider } from '@/components/providers/api-provider';
import { UserContextProvider } from '@/components/providers/user-context-provider';
import { getMetadata } from '@/metadata';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { BaseContainer } from '@socialincome/ui';
Expand All @@ -16,23 +17,25 @@ export default async function Layout({ children, params }: PropsWithChildren<Def
return (
<BaseContainer className="pb-8">
<UserContextProvider>
<LayoutClient
params={params}
translations={{
accountTitle: translator.t('sections.account.title'),
personalInfo: translator.t('sections.account.personal-info'),
security: translator.t('sections.account.security'),
contributionsTitle: translator.t('sections.contributions.title'),
payments: translator.t('sections.contributions.payments'),
subscriptions: translator.t('sections.contributions.subscriptions'),
donationCertificatesShort: translator.t('sections.contributions.donation-certificates-short'),
donationCertificatesLong: translator.t('sections.contributions.donation-certificates-long'),
employerTitle: translator.t('sections.employer.title'),
work: translator.t('sections.employer.work'),
}}
>
{children}
</LayoutClient>
<ApiProvider>
<LayoutClient
params={params}
translations={{
accountTitle: translator.t('sections.account.title'),
personalInfo: translator.t('sections.account.personal-info'),
security: translator.t('sections.account.security'),
contributionsTitle: translator.t('sections.contributions.title'),
payments: translator.t('sections.contributions.payments'),
subscriptions: translator.t('sections.contributions.subscriptions'),
donationCertificatesShort: translator.t('sections.contributions.donation-certificates-short'),
donationCertificatesLong: translator.t('sections.contributions.donation-certificates-long'),
employerTitle: translator.t('sections.employer.title'),
work: translator.t('sections.employer.work'),
}}
>
{children}
</LayoutClient>
</ApiProvider>
</UserContextProvider>
</BaseContainer>
);
Expand Down
2 changes: 1 addition & 1 deletion website/src/app/[lang]/[region]/(website)/me/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { useEffect } from 'react';

export default function Page() {
useEffect(() => {
redirect('./me/payments');
redirect('./me/contributions');
}, []);
}
Loading

0 comments on commit 1c42962

Please sign in to comment.