Skip to content
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

feature(website): update account dashboard #619

Merged
merged 2 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion seed/auth_export/accounts.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions seed/firebase-export-metadata.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"version": "12.4.4",
"version": "12.7.0",
"firestore": {
"version": "1.18.1",
"version": "1.18.2",
"path": "firestore_export",
"metadata_file": "firestore_export/firestore_export.overall_export_metadata"
},
"auth": {
"version": "12.4.4",
"version": "12.7.0",
"path": "auth_export"
},
"storage": {
"version": "12.4.4",
"version": "12.7.0",
"path": "storage_export"
}
}
Binary file not shown.
Binary file modified seed/firestore_export/all_namespaces/all_kinds/output-0
Binary file not shown.
Binary file modified seed/firestore_export/firestore_export.overall_export_metadata
Binary file not shown.
40 changes: 36 additions & 4 deletions shared/locales/en/website-me.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
{
"tabs": {
"contact-details": "Contact Info",
"contributions": "Contributions"
"sections": {
"account": {
"title": "My Account",
"personal-info": "Personal Info",
"security": "Security"
},
"contributions": {
"title": "My Contributions",
"payments": "Payments",
"subscriptions": "Subscriptions"
}
},
"contributions": {
"amount": "Amount",
"date": "Date",
"source": "Source",
"total": "Total",
"amount-currency": "{{ amount, currency }}",
"date": "Date"
"sources": {
"benevity": "Benevity",
"cash": "Cash",
"stripe": "Stripe",
"wire-transfer": "Wire Transfer"
}
},
"subscriptions": {
"amount": "Amount",
"date": "Date",
"source": "Source",
"total": "Total",
"amount-currency": "{{ amount, currency }}",
"interval": "Interval",
"interval-1": "Monthly",
"interval-3": "Quarterly",
"interval-12": "Annually",
"status": {
"active": "Active",
"canceled": "Canceled",
"paused": "Paused"
}
},
"login": {
"title": "Sign in to your account",
Expand Down
15 changes: 11 additions & 4 deletions shared/src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ export function getMonthIDs(date: Date, last_n: number) {
return months;
}

export function toDateTime(timestamp: Timestamp | Date, timezone: string = 'utc') {
return timestamp instanceof Date
? DateTime.fromJSDate(timestamp, { zone: timezone })
: DateTime.fromMillis(timestamp.toMillis(), { zone: timezone });
export function toDateTime(timestamp: Timestamp | Date | number, timezone: string = 'utc') {
if (timestamp instanceof Date) {
timestamp = timestamp as Date;
return DateTime.fromJSDate(timestamp, { zone: timezone });
} else if (Number.isInteger(timestamp)) {
timestamp = timestamp as number;
return DateTime.fromMillis(timestamp as number, { zone: timezone });
} else {
timestamp = timestamp as Timestamp;
return DateTime.fromMillis(timestamp.toMillis(), { zone: timezone });
}
}

export function toDate(dateTime: DateTime) {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const buttonVariants = cva(
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-16 rounded-md px-8',
lg: 'h-16 rounded-md px-8 font-semibold',
icon: 'h-10 w-10',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ export default function Section1Form({ translations }: Section1InputProps) {
/>
<CurrencySelector className="h-16 sm:flex-1" currencies={websiteCurrencies} fontSize="lg" />
</div>
<Button size="lg" type="submit" variant="default">
<Typography size="lg" weight="semibold" color="primary-foreground">
{translations.submit}
</Typography>
<Button size="lg" type="submit" variant="default" className="text-lg">
{translations.submit}
</Button>
</form>
</Form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { DefaultPageProps } from '@/app/[lang]/[region]';
import { CreateSubscriptionData } from '@/app/api/stripe/checkout/new/route';
import { CreateSubscriptionData } from '@/app/api/stripe/checkout/new-payment/route';
import { CheckCircleIcon } from '@heroicons/react/24/outline';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Expand All @@ -19,6 +19,7 @@ import {
import classNames from 'classnames';
import { useRouter } from 'next/navigation';
import { UseFormReturn, useForm } from 'react-hook-form';
import { useUser } from 'reactfire';
import Stripe from 'stripe';
import * as z from 'zod';

Expand Down Expand Up @@ -67,6 +68,7 @@ function RadioGroupFormItem({ active, title, value, form, description }: RadioGr

export default function Page({ params, searchParams }: DefaultPageProps) {
const router = useRouter();
const { data: authUser } = useUser();

const formSchema = z.object({
amount: z.coerce.number(),
Expand All @@ -80,12 +82,15 @@ export default function Page({ params, searchParams }: DefaultPageProps) {
});

const onSubmit = async (values: FormSchema) => {
const authToken = await authUser?.getIdToken(true);
const data: CreateSubscriptionData = {
amount: values.amount * 100, // The amount is in cents, so we need to multiply by 100 to get the correct amount.
intervalCount: Number(values.intervalCount),
successUrl: `${window.location.origin}/${params.lang}/${params.region}/donate/success?stripeCheckoutSessionId={CHECKOUT_SESSION_ID}`,
recurring: true,
firebaseAuthToken: authToken,
};
const response = await fetch('/api/stripe/checkout/new', {
const response = await fetch('/api/stripe/checkout/new-payment', {
method: 'POST',
body: JSON.stringify(data),
});
Expand Down
32 changes: 18 additions & 14 deletions website/src/app/[lang]/[region]/(website)/login/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client';

import { DefaultPageProps } from '@/app/[lang]/[region]';
import { DefaultParams } from '@/app/[lang]/[region]';
import { zodResolver } from '@hookform/resolvers/zod';
import { SiGoogle } from '@icons-pack/react-simple-icons';
import { Button, Form, FormControl, FormField, FormItem, FormMessage, Input, Typography } from '@socialincome/ui';
import { FirebaseError } from 'firebase/app';
import { browserSessionPersistence, signInWithEmailAndPassword } from 'firebase/auth';
import { useRouter } from 'next/navigation';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useAuth } from 'reactfire';
Expand All @@ -26,9 +27,9 @@ type LoginFormProps = {
unknownUser: string;
wrongPassword: string;
};
} & DefaultPageProps;
} & DefaultParams;

export default function LoginForm({ params, translations }: LoginFormProps) {
export default function LoginForm({ lang, region, translations }: LoginFormProps) {
const router = useRouter();
const auth = useAuth();

Expand All @@ -43,17 +44,20 @@ export default function LoginForm({ params, translations }: LoginFormProps) {
defaultValues: { email: '', password: '' },
});

const onSubmit = async (values: FormSchema) => {
await auth.setPersistence(browserSessionPersistence);
await signInWithEmailAndPassword(auth, values.email, values.password)
.then(() => {
router.push(`/${params.lang}/${params.region}/me`);
})
.catch((error: FirebaseError) => {
error.code === 'auth/wrong-password' && toast.error(translations.wrongPassword);
error.code === 'auth/user-not-found' && toast.error(translations.unknownUser);
});
};
const onSubmit = useCallback(
async (values: FormSchema) => {
await auth.setPersistence(browserSessionPersistence);
await signInWithEmailAndPassword(auth, values.email, values.password)
.then(() => {
router.push(`/${lang}/${region}/me`);
})
.catch((error: FirebaseError) => {
error.code === 'auth/wrong-password' && toast.error(translations.wrongPassword);
error.code === 'auth/user-not-found' && toast.error(translations.unknownUser);
});
},
[auth, lang, region, router, translations.wrongPassword, translations.unknownUser],
);

return (
<div className="mx-auto flex max-w-xl flex-col space-y-8">
Expand Down
7 changes: 4 additions & 3 deletions website/src/app/[lang]/[region]/(website)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { DefaultPageProps } from '@/app/[lang]/[region]';
import LoginForm from '@/app/[lang]/[region]/(website)/login/login-form';
import { Translator } from '@socialincome/shared/src/utils/i18n';

export default async function Page(props: DefaultPageProps) {
const translator = await Translator.getInstance({ language: props.params.lang, namespaces: 'website-me' });
export default async function Page({ params }: DefaultPageProps) {
const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-me'] });

return (
<LoginForm
lang={params.lang}
region={params.region}
translations={{
title: translator.t('login.title'),
email: translator.t('login.email'),
Expand All @@ -18,7 +20,6 @@ export default async function Page(props: DefaultPageProps) {
unknownUser: translator.t('login.unknown-user'),
wrongPassword: translator.t('login.wrong-password'),
}}
{...props}
/>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,51 @@
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 { 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: {
date: string;
amount: string;
source: string;
};
} & DefaultParams;

export function ContributionsTable({ lang, translations }: ContributionsTableProps) {
const firestore = useFirestore();
const { user } = useContext(UserContext);
const translator = useTranslator(lang, 'website-me');
const { user } = useContext(UserContext);
const { data: contributions } = useQuery({
queryKey: [user, firestore],
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
});
console.log(user?.id, firestore, contributions?.size);

return (
<Table>
<TableHeader>
<TableRow>
<TableHead>{translations.date}</TableHead>
<TableHead>{translations.source}</TableHead>
<TableHead className="text-right">{translations.amount}</TableHead>
</TableRow>
</TableHeader>
Expand All @@ -54,6 +58,7 @@ export function ContributionsTable({ lang, translations }: ContributionsTablePro
<TableCell>
<Typography>{toDateTime(contribution.get('created')).toFormat('DD', { locale: lang })}</Typography>
</TableCell>
<TableCell>{translator?.t(`contributions.sources.${contribution.get('source')}`)}</TableCell>
<TableCell className="text-right">
<Typography>
{translator?.t('contributions.amount-currency', {
Expand All @@ -68,6 +73,23 @@ export function ContributionsTable({ lang, translations }: ContributionsTablePro
</TableRow>
);
})}
<TableRow>
<TableCell>
<Typography weight="semibold">{translator?.t('contributions.total')}</Typography>
</TableCell>
<TableCell />
<TableCell>
<Typography className="text-right" weight="semibold">
{translator?.t('contributions.amount-currency', {
context: {
amount: _.sum(contributions?.docs.map((contribution) => contribution.get('amount'))),
currency: contributions?.docs[0].get('currency'),
locale: lang,
},
})}
</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { DefaultPageProps } from '@/app/[lang]/[region]';
import { ContributionsTable } from '@/app/[lang]/[region]/(website)/me/contributions/contributions-table';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { BillingPortalButton } from './billing-portal-button';

export default async function Page({ params }: DefaultPageProps) {
const translator = await Translator.getInstance({ language: params.lang, namespaces: ['website-me'] });

return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="flex max-w-lg flex-col space-y-4">
<ContributionsTable
translations={{ date: translator.t('contributions.date'), amount: translator.t('contributions.amount') }}
{...params}
/>
</div>
<BillingPortalButton />
<div className="grid grid-cols-1 gap-4">
<ContributionsTable
translations={{
date: translator.t('contributions.date'),
amount: translator.t('contributions.amount'),
source: translator.t('contributions.source'),
}}
{...params}
/>
</div>
);
}
Loading
Loading