Skip to content

Commit

Permalink
Merge pull request #49 from compsci-adl/feat/settings
Browse files Browse the repository at this point in the history
feat: Add settings page
  • Loading branch information
rayokamoto authored Feb 17, 2024
2 parents caaae0b + 1d74535 commit e334eeb
Show file tree
Hide file tree
Showing 24 changed files with 717 additions and 181 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-icons": "^4.12.0",
"redis": "^4.6.13",
"square": "^34.0.1",
"swr": "^2.2.5",
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 16 additions & 10 deletions src/app/(account)/join/steps/StepFour.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Button from '@/components/Button';
import Field from '@/components/Field';
import { fetcher } from '@/lib/fetcher';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import useSWRMutation from 'swr/mutation';
import { useJoinUsStep, useJoinUsStudentInfo, useSetJoinUsHeading } from '../store';

export default function StepFour() {
Expand All @@ -16,19 +18,23 @@ export default function StepFour() {
const { prevStep } = useJoinUsStep();
const { studentInfo } = useJoinUsStudentInfo();

const handleSignUp = async (e: React.ChangeEvent<any>) => {
const router = useRouter();
const createMember = useSWRMutation('member', fetcher.post.mutate, {
onError: () => {
setAgreementError('Server error.');
},
onSuccess: () => {
router.push('/settings');
},
});

const handleSignUp = async () => {
setAgreementError(null);
if (!agreement) {
setAgreementError('Please agree to the terms');
return;
}
// TODO(#17): Payment
try {
const res = await fetcher.post('member', { json: studentInfo }).json();
console.log(res);
} catch {
setAgreementError('Server error.');
}
createMember.trigger(studentInfo);
};

const toggleAgreement = () => setAgreement(!agreement);
Expand All @@ -48,7 +54,7 @@ export default function StepFour() {
<Button onClick={prevStep} colour="orange">
Back
</Button>
<Button onClick={handleSignUp} colour="purple">
<Button onClick={handleSignUp} colour="purple" loading={createMember.isMutating}>
Sign up
</Button>
</div>
Expand Down
13 changes: 3 additions & 10 deletions src/app/(account)/join/steps/StepTwo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,10 @@ export default function StepTwo() {
});

const { user } = useUser();
// Fetch user's profile information on component mount
useEffect(() => {
// Check if user profile data exists
if (user && user.primaryEmailAddress && user.fullName) {
// Split the full name into first and last names
const [firstName, lastName] = user.fullName.split(' ');
// Set the first and last names in the state
form.setValue('firstName', firstName);
form.setValue('lastName', lastName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
if (!user) return;
form.setValue('firstName', String(user.firstName));
form.setValue('lastName', String(user.lastName));
}, [user]);

const { nextStep } = useJoinUsStep();
Expand Down
42 changes: 42 additions & 0 deletions src/app/(account)/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import Sidebar from './Sidebar';
import type { MembershipPayment } from './page';
import MembershipSettings from './tabs/MembershipSettings';

export const TAB_NAMES = ['Account', 'Personal Info', 'Membership', 'Notifications'] as const;
export type TabNames = (typeof TAB_NAMES)[number];

export type SettingData = { membershipPayment: MembershipPayment };
export type SettingTabProps = { settingData: SettingData };
type SettingTabComponent = ({ settingData }: SettingTabProps) => React.ReactNode;
const SETTING_TABS = {
Account: () => <div>WIP</div>,
'Personal Info': () => <div>WIP</div>,
Membership: MembershipSettings,
Notifications: () => <div>WIP</div>,
} as const satisfies Record<TabNames, SettingTabComponent>;

export default function Settings({ settingData }: { settingData: SettingData }) {
const [tab, setTab] = useState<TabNames>('Membership');
const Tab = SETTING_TABS[tab];

const { refresh } = useRouter();

return (
<>
<Sidebar
currentTab={tab}
onTabChange={(tab) => {
setTab(tab);
refresh();
}}
/>
<div className="flex w-full">
<Tab settingData={settingData} />
</div>
</>
);
}
43 changes: 43 additions & 0 deletions src/app/(account)/settings/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TAB_NAMES, type TabNames } from './Settings';

interface SidebarTabProps {
tabName: TabNames;
currentTab: TabNames;
onTabChange: (tab: TabNames) => void;
}

function SidebarTab({ tabName, currentTab, onTabChange }: SidebarTabProps) {
const selected = currentTab === tabName;
return (
<button
className={`text-left ${selected ? 'cursor-default font-bold' : 'hover:underline'}`}
onClick={() => {
if (!selected) {
onTabChange(tabName);
}
}}
>
{tabName}
</button>
);
}

interface SidebarProps {
currentTab: TabNames;
onTabChange: (tab: TabNames) => void;
}

export default function Sidebar({ currentTab, onTabChange }: SidebarProps) {
return (
<div className="flex w-48 flex-col space-y-4 border-r-2 border-grey">
{TAB_NAMES.map((tab, i) => (
<SidebarTab
currentTab={currentTab}
onTabChange={onTabChange}
tabName={tab}
key={i}
/>
))}
</div>
);
}
86 changes: 86 additions & 0 deletions src/app/(account)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import FancyRectangle from '@/components/FancyRectangle';
import Title from '@/components/Title';
import { db } from '@/db';
import { checkUserExists } from '@/db/queries';
import { memberTable } from '@/db/schema';
import { redisClient } from '@/lib/redis';
import { squareClient } from '@/lib/square';
import { currentUser } from '@clerk/nextjs';
import { eq } from 'drizzle-orm';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import Settings from './Settings';

const verifyMembershipPayment = async (clerkId: string) => {
// Get user's membership expiry date from the database
const [{ membershipExpiresAt }] = await db
.select({
membershipExpiresAt: memberTable.membershipExpiresAt,
})
.from(memberTable)
.where(eq(memberTable.clerkId, clerkId));
// If membership expiry date exists, return the existing date
if (membershipExpiresAt) {
return { paid: true as const, membershipExpiresAt };
}

const paymentId = await redisClient.hGet(`payment:membership:${clerkId}`, 'paymentId');
if (!paymentId) {
// Membership payment for the user does not exist
return { paid: false as const };
}

const resp = await squareClient.checkoutApi.retrievePaymentLink(paymentId);
const respFields = resp.result;
if (!respFields.paymentLink || respFields.paymentLink.id !== paymentId) {
// Payment has not been made
return { paid: false as const };
}

// Set expiry date to be the January 1st of the following year
const now = new Date();
const expiryDate = new Date(`${now.getFullYear() + 1}-01-01`);
await db
.update(memberTable)
.set({ membershipExpiresAt: expiryDate })
.where(eq(memberTable.id, clerkId));

// Delete key from Redis since it is no longer needed
await redisClient.del(`payment:membership:${clerkId}`);

return { paid: true as const, membershipExpiresAt: expiryDate };
};
export type MembershipPayment = Awaited<ReturnType<typeof verifyMembershipPayment>>;

export default async function SettingsPage() {
const user = await currentUser();
if (!user) return notFound();

const exists = await checkUserExists(user.id);
const membershipPayment = await verifyMembershipPayment(user.id);

return (
<div className="flex flex-col">
<div className="flex justify-center">
<Title colour="purple">Settings</Title>
</div>
<section className="mt-12 flex justify-center">
<FancyRectangle colour="purple" offset="8" filled={true}>
<div className="z-0 flex w-fit gap-8 border-4 border-black bg-white px-8 py-8 text-black md:w-[48rem] md:px-12 md:py-12">
{exists ? (
<Settings settingData={{ membershipPayment }} />
) : (
<h2 className="text-2xl">
Please finishing{' '}
<Link href="/join" className="font-bold text-purple">
signing up
</Link>{' '}
first.
</h2>
)}
</div>
</FancyRectangle>
</section>
</div>
);
}
Loading

0 comments on commit e334eeb

Please sign in to comment.