From 7615078e62546e2281e3e92e30e436f2a4b72893 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Tue, 13 Feb 2024 22:13:10 +1030 Subject: [PATCH 01/32] feat: add basic settings menu with sidebar --- src/app/(account)/settings/Settings.tsx | 54 +++++++++++++++++++++++ src/app/(account)/settings/Sidebar.tsx | 33 ++++++++++++++ src/app/(account)/settings/SidebarTab.tsx | 17 +++++++ src/app/(account)/settings/page.tsx | 33 ++++++++++++++ src/components/UserButton.tsx | 6 +-- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/app/(account)/settings/Settings.tsx create mode 100644 src/app/(account)/settings/Sidebar.tsx create mode 100644 src/app/(account)/settings/SidebarTab.tsx create mode 100644 src/app/(account)/settings/page.tsx diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx new file mode 100644 index 00000000..8644d4f6 --- /dev/null +++ b/src/app/(account)/settings/Settings.tsx @@ -0,0 +1,54 @@ +interface SettingsProps { + selectedTab: string; +} + +export default function Settings({ selectedTab }: SettingsProps) { + const renderSettings = () => { + switch (selectedTab) { + case 'account': + return ( +
+ {/* Account settings */} +

Change Email

+

Change Password

+

Change Linked Google Account

+

Delete Account

+
+ ); + case 'personalInfo': + return ( +
+ {/* Personal info settings */} +

Update Name

+

Update Age

+

Update Gender

+

Update Degree

+

Update Student Type

+

Update Student Status

+

Update Student Number

+
+ ); + case 'membership': + return ( +
+ {/* Membership settings */} +

Membership Status

+

Pay Membership Fee

+
+ ); + case 'notifications': + return ( +
+ {/* Notifications settings */} +

Enable Email Notifications

+

Upcoming Events

+

Newsletter

+
+ ); + default: + return null; + } + }; + + return
{renderSettings()}
; +} diff --git a/src/app/(account)/settings/Sidebar.tsx b/src/app/(account)/settings/Sidebar.tsx new file mode 100644 index 00000000..d96b04b0 --- /dev/null +++ b/src/app/(account)/settings/Sidebar.tsx @@ -0,0 +1,33 @@ +import SidebarTab from './SidebarTab'; + +interface SidebarProps { + selectedTab: string; + handleTabClick: (tab: string) => void; +} + +export default function Sidebar({ selectedTab, handleTabClick }: SidebarProps) { + return ( +
+ handleTabClick('account')} + /> + handleTabClick('personalInfo')} + /> + handleTabClick('membership')} + /> + handleTabClick('notifications')} + /> +
+ ); +} diff --git a/src/app/(account)/settings/SidebarTab.tsx b/src/app/(account)/settings/SidebarTab.tsx new file mode 100644 index 00000000..eb4de48f --- /dev/null +++ b/src/app/(account)/settings/SidebarTab.tsx @@ -0,0 +1,17 @@ +interface SidebarTabProps { + tabName: string; + selected: boolean; + onClick: () => void; +} + +export default function SidebarTab({ tabName, selected, onClick }: SidebarTabProps) { + return ( + + ); +} diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx new file mode 100644 index 00000000..49e54a56 --- /dev/null +++ b/src/app/(account)/settings/page.tsx @@ -0,0 +1,33 @@ +'use client'; + +import FancyRectangle from '@/components/FancyRectangle'; +import Title from '@/components/Title'; +import { useState } from 'react'; +import Settings from './Settings'; +import Sidebar from './Sidebar'; + +export default function SettingsPage() { + const [selectedTab, setSelectedTab] = useState('account'); + + const handleTabClick = (tab: string) => { + setSelectedTab(tab); + }; + + return ( +
+
+ Settings +
+
+ +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index ead30ebb..e76f114d 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -44,9 +44,9 @@ export default function UserButton() { > CS Club Drive - {/* Link to Manage Account Page */} - - Manage Account + {/* Link to Settings Page */} + + Settings {/* Sign Out */} + +
+

Change Password

+
+
+ + + {errors.oldPassword && Old Password is required} +
+
+ + + {errors.newPassword && New Password is required} +
+
+ + + {errors.confirmPassword && Confirm Password is required} +
+ +
+
+

Change Linked Google Account

+
+

Link Status:

+
+ + ); +} diff --git a/src/app/(account)/settings/MembershipSettings.tsx b/src/app/(account)/settings/MembershipSettings.tsx new file mode 100644 index 00000000..d9a5b5ce --- /dev/null +++ b/src/app/(account)/settings/MembershipSettings.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; + +export default function MembershipSettings() { + return ( +
+
+

+ Membership Status: Payment Required +

+
+ {/* TODO: Check Membership status and display relevant message */} +

+ Finalise your membership by completing the required payment either online below, + at a club event, or contact one of the{' '} + + committee members + + . +

+
+

Pay Membership Fee

+
+ ); +} diff --git a/src/app/(account)/settings/NotificationsSettings.tsx b/src/app/(account)/settings/NotificationsSettings.tsx new file mode 100644 index 00000000..7b44f520 --- /dev/null +++ b/src/app/(account)/settings/NotificationsSettings.tsx @@ -0,0 +1,9 @@ +export default function NotificationsSettings() { + return ( +
+

Enable Email Notifications

+

Upcoming Events

+

Newsletter

+
+ ); +} diff --git a/src/app/(account)/settings/PersonalInfoSettings.tsx b/src/app/(account)/settings/PersonalInfoSettings.tsx new file mode 100644 index 00000000..73dc3542 --- /dev/null +++ b/src/app/(account)/settings/PersonalInfoSettings.tsx @@ -0,0 +1,31 @@ +import Button from '@/components/Button'; +import { useUser } from '@clerk/nextjs'; + +interface PersonalInfoSettingsProps { + register: any; + handleSubmit: any; +} + +export default function PersonalInfoSettings({ + register, + handleSubmit, +}: PersonalInfoSettingsProps) { + const { user } = useUser(); + + return ( +
+

Update Name

+ +

Update Age

+

Update Gender

+ +
+ ); +} diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index 8644d4f6..f7555108 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,50 +1,58 @@ +import { useUser } from '@clerk/nextjs'; +import { useForm } from 'react-hook-form'; +import AccountSettings from './AccountSettings'; +import MembershipSettings from './MembershipSettings'; +import NotificationsSettings from './NotificationsSettings'; +import PersonalInfoSettings from './PersonalInfoSettings'; + interface SettingsProps { selectedTab: string; + setSelectedTab: (tab: string) => void; } -export default function Settings({ selectedTab }: SettingsProps) { +export default function Settings({ selectedTab, setSelectedTab }: SettingsProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + const { user } = useUser(); + + const handleGoToMembership = () => { + setSelectedTab('membership'); + }; + + const onSubmit = (data: any) => { + // TODO: Send data to Clerk via API + console.log(data); + }; + const renderSettings = () => { switch (selectedTab) { case 'account': return ( -
- {/* Account settings */} -

Change Email

-

Change Password

-

Change Linked Google Account

-

Delete Account

-
+ ); case 'personalInfo': return ( -
- {/* Personal info settings */} -

Update Name

-

Update Age

-

Update Gender

-

Update Degree

-

Update Student Type

-

Update Student Status

-

Update Student Number

-
+ ); case 'membership': - return ( -
- {/* Membership settings */} -

Membership Status

-

Pay Membership Fee

-
- ); + return ; case 'notifications': - return ( -
- {/* Notifications settings */} -

Enable Email Notifications

-

Upcoming Events

-

Newsletter

-
- ); + return ; default: return null; } diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 49e54a56..e7c5fd35 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -20,11 +20,9 @@ export default function SettingsPage() {
-
+
-
- -
+
From 39d2bf0ebb18c77f40f387e8c7159434d9e27c70 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 13:42:28 +1030 Subject: [PATCH 03/32] feat: add online payment to settings --- .../(account)/settings/MembershipSettings.tsx | 95 ++++++++++++++++--- src/app/api/payment/route.ts | 2 +- .../api/verify-membership-payment/route.ts | 4 +- 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/app/(account)/settings/MembershipSettings.tsx b/src/app/(account)/settings/MembershipSettings.tsx index d9a5b5ce..6d76410b 100644 --- a/src/app/(account)/settings/MembershipSettings.tsx +++ b/src/app/(account)/settings/MembershipSettings.tsx @@ -1,26 +1,95 @@ +import Button from '@/components/Button'; +import { useMount } from '@/hooks/use-mount'; +import { useUser } from '@clerk/nextjs'; import Link from 'next/link'; -import { useRouter } from 'next/router'; import { useState } from 'react'; -export default function MembershipSettings() { +export default function MembershipSettings({}) { + const [membershipStatus, setMembershipStatus] = useState('Payment Required'); + const { user } = useUser(); + + useMount(() => { + const verifyMembershipPayment = async () => { + console.log('Verifying membership payment'); + try { + const response = await fetch('/api/verify-membership-payment', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + redirectUrl: window.location.href, + }), + }); + + if (response.ok) { + setMembershipStatus('Paid'); + } + } catch (error) { + console.error('Error verifying membership payment:', error); + } + }; + + verifyMembershipPayment(); + }); + + const handlePayment = async () => { + try { + if (!user || !user.id) { + console.error('User not authenticated or ID not available'); + return; + } + + const response = await fetch('/api/payment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + product: 'membership', + customerId: user.id, + redirectUrl: window.location.href, + }), + }); + + if (response.ok) { + const paymentLink = await response.json(); + window.location.href = paymentLink.url; + } else { + console.error('Failed to create payment link'); + } + } catch (error) { + console.error('Error creating payment link:', error); + } + }; + return (

- Membership Status: Payment Required + Membership Status:{' '} + + {membershipStatus} +

- {/* TODO: Check Membership status and display relevant message */} -

- Finalise your membership by completing the required payment either online below, - at a club event, or contact one of the{' '} - - committee members - - . -

+ {membershipStatus === 'Payment Required' && ( +

+ Finalise your membership by completing the required payment either online + below, at a club event, or contact one of the{' '} + + committee members + + . +

+ )} + {membershipStatus === 'Paid' &&

You are a CS Club member!

}
-

Pay Membership Fee

+

Pay Membership Fee

+
+
); } diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index 9e76330b..2f3f2c02 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -77,7 +77,7 @@ export async function POST(request: Request) { }) .from(members) .where(eq(members.clerkId, user.id)); - const paymentId = resp.result.paymentLink?.orderId ?? ''; + const paymentId = resp.result.paymentLink?.id ?? ''; const createdAt = resp.result.paymentLink?.createdAt ?? ''; await redisClient.hSet(`payment:membership:${userId}`, { paymentId: paymentId, diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts index 017585a9..8dc975be 100644 --- a/src/app/api/verify-membership-payment/route.ts +++ b/src/app/api/verify-membership-payment/route.ts @@ -50,9 +50,9 @@ export async function PUT(request: Request) { return new Response('Membership payment for the user does not exist', { status: 404 }); } - const resp = await squareClient.paymentsApi.getPayment(paymentId); + const resp = await squareClient.checkoutApi.retrievePaymentLink(paymentId); const respFields = resp.result; - if (respFields.payment?.status !== 'COMPLETED') { + if (!respFields.paymentLink || respFields.paymentLink.id !== paymentId) { return new Response('Payment has not been made', { status: 404 }); } From 1591806b80350747f319c7d0ef957a671e626ada Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 15:08:52 +1030 Subject: [PATCH 04/32] feat: implement member payment verification --- .../(account)/settings/AccountSettings.tsx | 74 ++++++++++++---- .../(account)/settings/MembershipSettings.tsx | 87 +++++++++++++------ src/app/(account)/settings/Settings.tsx | 15 +++- .../api/verify-membership-payment/route.ts | 13 +++ src/lib/formatDate.ts | 8 ++ 5 files changed, 151 insertions(+), 46 deletions(-) create mode 100644 src/lib/formatDate.ts diff --git a/src/app/(account)/settings/AccountSettings.tsx b/src/app/(account)/settings/AccountSettings.tsx index 8109e8f5..79962164 100644 --- a/src/app/(account)/settings/AccountSettings.tsx +++ b/src/app/(account)/settings/AccountSettings.tsx @@ -1,4 +1,6 @@ import Button from '@/components/Button'; +import { useMount } from '@/hooks/use-mount'; +import { formatDate } from '@/lib/formatDate'; interface AccountSettingsProps { user: any; @@ -7,6 +9,9 @@ interface AccountSettingsProps { errors: any; onSubmit: (data: any) => void; handleGoToMembership: () => void; + membershipStatus: string; + setMembershipStatus: (status: string) => void; + setMembershipExpirationDate: (date: string) => void; } export default function AccountSettings({ @@ -16,27 +21,62 @@ export default function AccountSettings({ errors, onSubmit, handleGoToMembership, + membershipStatus, + setMembershipStatus, + setMembershipExpirationDate, }: AccountSettingsProps) { + useMount(() => { + const verifyMembershipPayment = async () => { + console.log('Verifying membership payment'); + try { + const response = await fetch('/api/verify-membership-payment', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + redirectUrl: window.location.href, + }), + }); + + if (response.ok) { + const data = await response.json(); + setMembershipStatus('Paid'); + const expirationDate = formatDate(data.membershipExpiresAt); + setMembershipExpirationDate(expirationDate); + } else { + setMembershipStatus('Payment Required'); + } + } catch (error) { + console.error('Error verifying membership payment:', error); + } + }; + + verifyMembershipPayment(); + }); + return (
-
-

- Membership Status: Payment Required -

+ {membershipStatus === 'Payment Required' && ( +
+

+ Membership Status: Payment Required +

-
- {/* TODO: Check Membership status and display relevant message */} -

- Finalise your membership by{' '} - - clicking here - {' '} - and completing the required payment. -

-
+
+ {/* TODO: Check Membership status and display relevant message */} +

+ Finalise your membership by{' '} + + clicking here + {' '} + and completing the required payment. +

+
+ )}

Change Email

diff --git a/src/app/(account)/settings/MembershipSettings.tsx b/src/app/(account)/settings/MembershipSettings.tsx index 6d76410b..2adf7d36 100644 --- a/src/app/(account)/settings/MembershipSettings.tsx +++ b/src/app/(account)/settings/MembershipSettings.tsx @@ -1,11 +1,22 @@ import Button from '@/components/Button'; import { useMount } from '@/hooks/use-mount'; +import { formatDate } from '@/lib/formatDate'; import { useUser } from '@clerk/nextjs'; import Link from 'next/link'; -import { useState } from 'react'; -export default function MembershipSettings({}) { - const [membershipStatus, setMembershipStatus] = useState('Payment Required'); +interface MembershipSettingsProps { + membershipStatus: string; + setMembershipStatus: (status: string) => void; + membershipExpirationDate: string; + setMembershipExpirationDate: (date: string) => void; +} + +export default function MembershipSettings({ + membershipStatus, + setMembershipStatus, + membershipExpirationDate, + setMembershipExpirationDate, +}: MembershipSettingsProps) { const { user } = useUser(); useMount(() => { @@ -23,7 +34,12 @@ export default function MembershipSettings({}) { }); if (response.ok) { + const data = await response.json(); setMembershipStatus('Paid'); + const expirationDate = formatDate(data.membershipExpiresAt); + setMembershipExpirationDate(expirationDate); + } else { + setMembershipStatus('Payment Required'); } } catch (error) { console.error('Error verifying membership payment:', error); @@ -65,31 +81,46 @@ export default function MembershipSettings({}) { return (
-
-

- Membership Status:{' '} - - {membershipStatus} - -

-
- {membershipStatus === 'Payment Required' && ( -

- Finalise your membership by completing the required payment either online - below, at a club event, or contact one of the{' '} - - committee members - - . -

- )} - {membershipStatus === 'Paid' &&

You are a CS Club member!

} -
-

Pay Membership Fee

-
- + {membershipStatus !== null && ( +
+

+ Membership Status:{' '} + + {membershipStatus} + +

+
+ {membershipStatus === 'Payment Required' && ( + <> +

+ Finalise your membership by completing the required payment either + online below, at a club event, or contact one of the{' '} + + committee members + + . +

+

Pay Membership Fee

+
+ + + )} + {membershipStatus === 'Paid' && ( +

+ You are a CS Club member! Your membership expires on{' '} + {membershipExpirationDate}. +

+ )} +
+ )}
); } diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index f7555108..e19072de 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,4 +1,5 @@ import { useUser } from '@clerk/nextjs'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import AccountSettings from './AccountSettings'; import MembershipSettings from './MembershipSettings'; @@ -17,6 +18,8 @@ export default function Settings({ selectedTab, setSelectedTab }: SettingsProps) formState: { errors }, } = useForm(); const { user } = useUser(); + const [membershipStatus, setMembershipStatus] = useState('Checking...'); + const [membershipExpirationDate, setMembershipExpirationDate] = useState(''); const handleGoToMembership = () => { setSelectedTab('membership'); @@ -38,6 +41,9 @@ export default function Settings({ selectedTab, setSelectedTab }: SettingsProps) errors={errors} onSubmit={onSubmit} handleGoToMembership={handleGoToMembership} + membershipStatus={membershipStatus} + setMembershipStatus={setMembershipStatus} + setMembershipExpirationDate={setMembershipExpirationDate} /> ); case 'personalInfo': @@ -50,7 +56,14 @@ export default function Settings({ selectedTab, setSelectedTab }: SettingsProps) /> ); case 'membership': - return ; + return ( + + ); case 'notifications': return ; default: diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts index 8dc975be..c79311c4 100644 --- a/src/app/api/verify-membership-payment/route.ts +++ b/src/app/api/verify-membership-payment/route.ts @@ -38,6 +38,19 @@ export async function PUT(request: Request) { } try { + // Get user's membership expiry date from the database + const [{ membershipExpiresAt }] = await db + .select({ + membershipExpiresAt: members.membershipExpiresAt, + }) + .from(members) + .where(eq(members.clerkId, user.id)); + + // If membership expiry date exists, return the existing date + if (membershipExpiresAt) { + return new Response(JSON.stringify({ membershipExpiresAt }), { status: 200 }); + } + // Get payment ID from Redis cache const [{ id: userId }] = await db .select({ diff --git a/src/lib/formatDate.ts b/src/lib/formatDate.ts new file mode 100644 index 00000000..63b6c5b3 --- /dev/null +++ b/src/lib/formatDate.ts @@ -0,0 +1,8 @@ +// Function to format the date to DD/MM/YY format +export const formatDate = (dateString: string): string => { + const date = new Date(dateString); + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString().slice(2); + return `${day}/${month}/${year}`; +}; From 869c7ba713a7ee81684d2e55b7c2aeb9e448858d Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 16:40:14 +1030 Subject: [PATCH 05/32] feat: force user to finish sign up before accessing settings --- src/app/(account)/settings/page.tsx | 50 ++++++++++++++++++++++++- src/app/api/check-user-exists/route.ts | 29 ++++++++++++++ src/components/UserButton.tsx | 52 +++++++++++++++++++------- 3 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 src/app/api/check-user-exists/route.ts diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index e7c5fd35..5adad9f3 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -2,12 +2,36 @@ import FancyRectangle from '@/components/FancyRectangle'; import Title from '@/components/Title'; +import { useMount } from '@/hooks/use-mount'; +import Link from 'next/link'; import { useState } from 'react'; import Settings from './Settings'; import Sidebar from './Sidebar'; export default function SettingsPage() { const [selectedTab, setSelectedTab] = useState('account'); + const [userFound, setUserFound] = useState(''); + + useMount(() => { + const checkUserExists = async () => { + try { + const response = await fetch('/api/check-user-exists'); + if (response.ok) { + const data = await response.json(); + const userExists = data.exists; + setUserFound('true'); + console.log('User exists:', userExists); + } else { + setUserFound('false'); + console.error('Failed to fetch user existence status:', response.status); + } + } catch (error) { + console.error('Error checking user existence:', error); + } + }; + + checkUserExists(); + }); const handleTabClick = (tab: string) => { setSelectedTab(tab); @@ -21,8 +45,30 @@ export default function SettingsPage() {
- - + {userFound == 'true' && ( + <> + + + + )} + {userFound == 'false' && ( +

+ Please finishing{' '} + + signing up + {' '} + first. +

+ )}
diff --git a/src/app/api/check-user-exists/route.ts b/src/app/api/check-user-exists/route.ts new file mode 100644 index 00000000..667b9a1c --- /dev/null +++ b/src/app/api/check-user-exists/route.ts @@ -0,0 +1,29 @@ +import { db } from '@/db'; +import { members } from '@/db/schema'; +import { currentUser } from '@clerk/nextjs'; +import { eq } from 'drizzle-orm'; + +export async function GET(request: Request) { + const user = await currentUser(); + if (!user) { + return new Response(null, { status: 401 }); + } + + try { + const existingUser = await db + .select({ + id: members.id, + }) + .from(members) + .where(eq(members.clerkId, user.id)); + + if (existingUser.length > 0) { + return new Response(JSON.stringify({ exists: true }), { status: 200 }); + } else { + return new Response(JSON.stringify({ exists: false }), { status: 404 }); + } + } catch (error) { + console.error('Error checking user existence:', error); + return new Response(null, { status: 500 }); + } +} diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index e76f114d..b7ed6093 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -1,4 +1,5 @@ import Button from '@/components/Button'; +import { useMount } from '@/hooks/use-mount'; import { useClerk, useUser } from '@clerk/clerk-react'; import Image from 'next/image'; import Link from 'next/link'; @@ -8,9 +9,30 @@ import FancyRectangle from './FancyRectangle'; export default function UserButton() { const { user } = useUser(); const [isPopupOpen, setPopupOpen] = useState(false); + const [userFound, setUserFound] = useState(false); const popupRef = useRef(null); const { signOut } = useClerk(); + useMount(() => { + const checkUserExists = async () => { + try { + const response = await fetch('/api/check-user-exists'); + if (response.ok) { + const data = await response.json(); + const userExists = data.exists; + setUserFound(userExists); + console.log('User exists:', userExists); + } else { + console.error('Failed to fetch user existence status:', response.status); + } + } catch (error) { + console.error('Error checking user existence:', error); + } + }; + + checkUserExists(); + }); + const handleButtonClick = () => { setPopupOpen(!isPopupOpen); }; @@ -35,19 +57,23 @@ export default function UserButton() { ref={popupRef} className="absolute right-0 top-10 z-10 flex w-52 flex-col gap-y-4 border-4 border-black bg-white p-4 text-xl md:w-44 md:text-base" > - {/* TODO(#16): Link to CS Club Drive */} - - CS Club Drive - - {/* Link to Settings Page */} - - Settings - + {/* Only show options if finished sign up */} + {userFound && ( + <> + {/* TODO(#16): Link to CS Club Drive */} + + CS Club Drive + + + Settings + + + )} {/* Sign Out */} diff --git a/src/app/api/get-user-info/route.ts b/src/app/api/get-user-info/route.ts new file mode 100644 index 00000000..c8f4106b --- /dev/null +++ b/src/app/api/get-user-info/route.ts @@ -0,0 +1,33 @@ +import { db } from '@/db'; +import { members } from '@/db/schema'; +import { currentUser } from '@clerk/nextjs'; +import { eq } from 'drizzle-orm'; + +export async function GET(request: Request) { + const user = await currentUser(); + if (!user) { + return new Response(null, { status: 401 }); + } + + try { + const userData = await db + .select({ + ageBracket: members.ageBracket, + gender: members.gender, + studentType: members.studentType, + studentStatus: members.studentStatus, + studentId: members.studentId, + }) + .from(members) + .where(eq(members.clerkId, user.id)); + + if (!userData) { + return new Response(JSON.stringify({}), { status: 404 }); + } + + return new Response(JSON.stringify(userData), { status: 200 }); + } catch (error) { + console.error('Error fetching user data:', error); + return new Response(null, { status: 500 }); + } +} diff --git a/src/components/Field.tsx b/src/components/Field.tsx index 63ea7991..4e8216b6 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -10,6 +10,7 @@ export interface FieldProps { type?: 'text' | 'password' | 'select' | 'checkbox'; options?: readonly string[] | string[]; placeholder?: string; + defaultValue?: string; } const Field = ({ @@ -20,6 +21,7 @@ const Field = ({ type = 'text', options = [], placeholder, + defaultValue, }: FieldProps) => { const [showPassword, setShowPassword] = useState(false); @@ -41,12 +43,11 @@ const Field = ({ onChange={(e) => onChange(e.target.value)} id={label.toLowerCase()} name={label.toLowerCase()} - value={value} className="mt-1 w-full border border-gray-300 px-3 py-2 text-grey" > {placeholder && } {options.map((option, index) => ( - ))} @@ -69,8 +70,8 @@ const Field = ({ onChange={(e) => onChange(e.target.value)} id={label.toLowerCase()} name={label.toLowerCase()} - value={value} type={showPassword ? 'text' : type} + defaultValue={defaultValue} className="mt-1 w-full rounded-none border border-gray-300 px-3 py-2 text-grey" /> {type === 'password' && ( From c42e0b55ee84fb76a54fc821b6e6b7d1f013c99c Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 19:08:00 +1030 Subject: [PATCH 07/32] feat: fill name form db in settings --- src/app/api/get-user-info/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/api/get-user-info/route.ts b/src/app/api/get-user-info/route.ts index c8f4106b..28fe335c 100644 --- a/src/app/api/get-user-info/route.ts +++ b/src/app/api/get-user-info/route.ts @@ -12,6 +12,8 @@ export async function GET(request: Request) { try { const userData = await db .select({ + firstName: members.firstName, + lastName: members.lastName, ageBracket: members.ageBracket, gender: members.gender, studentType: members.studentType, From 61253bc8957c933eb3d5c278fe228280638032f0 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 19:34:25 +1030 Subject: [PATCH 08/32] feat: add notification settings --- .../(account)/settings/AccountSettings.tsx | 3 +- .../(account)/settings/MembershipSettings.tsx | 6 +- .../settings/NotificationsSettings.tsx | 65 +++++++++++++++++-- .../settings/PersonalInfoSettings.tsx | 4 ++ src/app/(account)/settings/Settings.tsx | 2 +- src/constants/colours.ts | 4 +- 6 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/app/(account)/settings/AccountSettings.tsx b/src/app/(account)/settings/AccountSettings.tsx index 79962164..f7fc3789 100644 --- a/src/app/(account)/settings/AccountSettings.tsx +++ b/src/app/(account)/settings/AccountSettings.tsx @@ -63,8 +63,7 @@ export default function AccountSettings({ Membership Status: Payment Required -
- {/* TODO: Check Membership status and display relevant message */} +

Finalise your membership by{' '} {membershipStatus !== null && (

-

+

Membership Status:{' '}

-
+
{membershipStatus === 'Payment Required' && ( <>

@@ -107,7 +107,7 @@ export default function MembershipSettings({ .

Pay Membership Fee

-
+
diff --git a/src/app/(account)/settings/NotificationsSettings.tsx b/src/app/(account)/settings/NotificationsSettings.tsx index 7b44f520..df902897 100644 --- a/src/app/(account)/settings/NotificationsSettings.tsx +++ b/src/app/(account)/settings/NotificationsSettings.tsx @@ -1,9 +1,66 @@ +import Button from '@/components/Button'; +import { useState } from 'react'; + export default function NotificationsSettings() { + const [emailNotifications, setEmailNotifications] = useState(false); + const [upcomingEvents, setUpcomingEvents] = useState(true); + const [newsletter, setNewsletter] = useState(true); + + const toggleEmailNotifications = () => { + setEmailNotifications((prev) => !prev); + }; + return ( -
-

Enable Email Notifications

-

Upcoming Events

-

Newsletter

+
+
+

Change Email Notification Settings

+
+
+
+

Enable Email Notifications

+
+ +
+
+
+

+ Upcoming Events +

+
+ +
+
+
+

+ Newsletter +

+
+ +
+
); } diff --git a/src/app/(account)/settings/PersonalInfoSettings.tsx b/src/app/(account)/settings/PersonalInfoSettings.tsx index ef2cee8d..8d948239 100644 --- a/src/app/(account)/settings/PersonalInfoSettings.tsx +++ b/src/app/(account)/settings/PersonalInfoSettings.tsx @@ -62,6 +62,10 @@ export default function PersonalInfoSettings() { return ( +
+

Change Personal Info

+
+
{renderSettings()}
; + return
{renderSettings()}
; } diff --git a/src/constants/colours.ts b/src/constants/colours.ts index 298e7b5f..8738473a 100644 --- a/src/constants/colours.ts +++ b/src/constants/colours.ts @@ -1,8 +1,9 @@ -export type Colour = 'black' | 'grey' | 'white' | 'yellow' | 'orange' | 'purple'; +export type Colour = 'black' | 'grey' | 'lightGrey' | 'white' | 'yellow' | 'orange' | 'purple'; export const BG_COLOURS = { black: 'bg-black', grey: 'bg-grey', + lightGrey: 'bg-gray-500', white: 'bg-white', yellow: 'bg-yellow', orange: 'bg-orange', @@ -12,6 +13,7 @@ export const BG_COLOURS = { export const BORDER_COLOURS = { black: 'border-black border-2', grey: 'border-grey border-2', + lightGrey: 'border-gray-500 border-2', white: 'border-white border-2', yellow: 'border-yellow border-2', orange: 'border-orange border-2', From bf8a87b22e37b46cc350398783e4e7e58e809c94 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Thu, 15 Feb 2024 20:09:00 +1030 Subject: [PATCH 09/32] chore: update input field styles for consistency --- src/app/(account)/settings/AccountSettings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/(account)/settings/AccountSettings.tsx b/src/app/(account)/settings/AccountSettings.tsx index f7fc3789..08e8d70a 100644 --- a/src/app/(account)/settings/AccountSettings.tsx +++ b/src/app/(account)/settings/AccountSettings.tsx @@ -84,7 +84,7 @@ export default function AccountSettings({ type="email" defaultValue={user?.primaryEmailAddress?.toString()} {...register('email')} - className="border-2 border-black p-2" + className="border border-gray-300 p-2" /> {errors.email && Email is required}
@@ -116,7 +116,7 @@ export default function AccountSettings({ type="password" placeholder={'New Password'} {...register('newPassword')} - className="border-2 border-black p-2" + className="border border-gray-300 p-2" /> {errors.newPassword && New Password is required}
@@ -129,7 +129,7 @@ export default function AccountSettings({ type="password" placeholder={'Confirm Password'} {...register('confirmPassword')} - className="border-2 border-black p-2" + className="border border-gray-300 p-2" /> {errors.confirmPassword && Confirm Password is required} From 723c4e272823655fcea9e8b16d434bc5a62ba7e7 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Fri, 16 Feb 2024 10:16:15 +1030 Subject: [PATCH 10/32] feat: fill name in settings --- src/app/(account)/settings/PersonalInfoSettings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/(account)/settings/PersonalInfoSettings.tsx b/src/app/(account)/settings/PersonalInfoSettings.tsx index 8d948239..f1b0c11b 100644 --- a/src/app/(account)/settings/PersonalInfoSettings.tsx +++ b/src/app/(account)/settings/PersonalInfoSettings.tsx @@ -2,6 +2,7 @@ import Button from '@/components/Button'; import ControlledField from '@/components/ControlledField'; import { AGE_BRACKETS, GENDERS, STUDENT_TYPES, STUDENT_STATUSES } from '@/constants/student-info'; import { useMount } from '@/hooks/use-mount'; +import { useUser } from '@clerk/nextjs'; import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -23,9 +24,10 @@ const personalInfoSchema = z.object({ }); export default function PersonalInfoSettings() { + const { user } = useUser(); const [formData, setFormData] = useState({ - firstName: '', - lastName: '', + firstName: user?.firstName || '', + lastName: user?.lastName || '', ageBracket: '', gender: '', studentType: '', @@ -62,10 +64,8 @@ export default function PersonalInfoSettings() { return ( -
-

Change Personal Info

-
-
+

Change Personal Info

+
Date: Fri, 16 Feb 2024 11:25:29 +1030 Subject: [PATCH 11/32] fix(field/select): remove unnecessary `selected` --- src/components/Field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Field.tsx b/src/components/Field.tsx index 4e8216b6..c73c66b6 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -47,7 +47,7 @@ const Field = ({ > {placeholder && } {options.map((option, index) => ( - ))} From 30d2f438cdcfebcd33737372135d8c5964d5c761 Mon Sep 17 00:00:00 2001 From: phoenixpereira Date: Fri, 16 Feb 2024 14:11:29 +1030 Subject: [PATCH 12/32] chore: disable unfinished settings tabs --- src/app/(account)/settings/Sidebar.tsx | 15 +++++++++------ src/app/(account)/settings/page.tsx | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/(account)/settings/Sidebar.tsx b/src/app/(account)/settings/Sidebar.tsx index d96b04b0..7cdfef55 100644 --- a/src/app/(account)/settings/Sidebar.tsx +++ b/src/app/(account)/settings/Sidebar.tsx @@ -8,26 +8,29 @@ interface SidebarProps { export default function Sidebar({ selectedTab, handleTabClick }: SidebarProps) { return (
- handleTabClick('account')} - /> - */} + {/* TODO: Implement personal info settings */} + {/* handleTabClick('personalInfo')} - /> + /> */} handleTabClick('membership')} /> - handleTabClick('notifications')} - /> + /> */}
); } diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 5adad9f3..77a22ed7 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -9,7 +9,7 @@ import Settings from './Settings'; import Sidebar from './Sidebar'; export default function SettingsPage() { - const [selectedTab, setSelectedTab] = useState('account'); + const [selectedTab, setSelectedTab] = useState('membership'); const [userFound, setUserFound] = useState(''); useMount(() => { From 8922437a921790be88b80b8019f38f448f233174 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Fri, 16 Feb 2024 14:27:54 +1030 Subject: [PATCH 13/32] refactor: rename `formatDate` filename to dash-case and move to utils --- src/app/(account)/settings/AccountSettings.tsx | 2 +- src/app/(account)/settings/MembershipSettings.tsx | 2 +- src/{lib/formatDate.ts => utils/format-date.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{lib/formatDate.ts => utils/format-date.ts} (100%) diff --git a/src/app/(account)/settings/AccountSettings.tsx b/src/app/(account)/settings/AccountSettings.tsx index 08e8d70a..0c1a2c59 100644 --- a/src/app/(account)/settings/AccountSettings.tsx +++ b/src/app/(account)/settings/AccountSettings.tsx @@ -1,6 +1,6 @@ import Button from '@/components/Button'; import { useMount } from '@/hooks/use-mount'; -import { formatDate } from '@/lib/formatDate'; +import { formatDate } from '@/utils/format-date'; interface AccountSettingsProps { user: any; diff --git a/src/app/(account)/settings/MembershipSettings.tsx b/src/app/(account)/settings/MembershipSettings.tsx index 8b258d6a..24a895ef 100644 --- a/src/app/(account)/settings/MembershipSettings.tsx +++ b/src/app/(account)/settings/MembershipSettings.tsx @@ -1,6 +1,6 @@ import Button from '@/components/Button'; import { useMount } from '@/hooks/use-mount'; -import { formatDate } from '@/lib/formatDate'; +import { formatDate } from '@/utils/format-date'; import { useUser } from '@clerk/nextjs'; import Link from 'next/link'; diff --git a/src/lib/formatDate.ts b/src/utils/format-date.ts similarity index 100% rename from src/lib/formatDate.ts rename to src/utils/format-date.ts From c50f13862b7c7611a6c8f03fd983c19a4c013bd0 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Fri, 16 Feb 2024 17:25:32 +1030 Subject: [PATCH 14/32] fix: form field isn't controlled --- src/app/(account)/join/steps/StepTwo.tsx | 13 +++---------- src/components/Field.tsx | 4 +--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/app/(account)/join/steps/StepTwo.tsx b/src/app/(account)/join/steps/StepTwo.tsx index 1ad9077f..c99a1aae 100644 --- a/src/app/(account)/join/steps/StepTwo.tsx +++ b/src/app/(account)/join/steps/StepTwo.tsx @@ -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(); diff --git a/src/components/Field.tsx b/src/components/Field.tsx index c73c66b6..48041c45 100644 --- a/src/components/Field.tsx +++ b/src/components/Field.tsx @@ -10,7 +10,6 @@ export interface FieldProps { type?: 'text' | 'password' | 'select' | 'checkbox'; options?: readonly string[] | string[]; placeholder?: string; - defaultValue?: string; } const Field = ({ @@ -21,7 +20,6 @@ const Field = ({ type = 'text', options = [], placeholder, - defaultValue, }: FieldProps) => { const [showPassword, setShowPassword] = useState(false); @@ -71,7 +69,7 @@ const Field = ({ id={label.toLowerCase()} name={label.toLowerCase()} type={showPassword ? 'text' : type} - defaultValue={defaultValue} + value={value} className="mt-1 w-full rounded-none border border-gray-300 px-3 py-2 text-grey" /> {type === 'password' && ( From 62834abfac0c7e7d808215ee4e2704be8233bb90 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Fri, 16 Feb 2024 18:02:07 +1030 Subject: [PATCH 15/32] feat(join): redirect to setting page after join --- package.json | 1 + pnpm-lock.yaml | 13 ++++++++++++ src/app/(account)/join/steps/StepFour.tsx | 26 ++++++++++++++--------- src/app/api/member/route.ts | 6 +++--- src/components/Button.tsx | 19 ++++++----------- src/db/schema.ts | 6 +++--- src/lib/fetcher.ts | 9 +++++++- 7 files changed, 51 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 28de61d0..85a37bea 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8013f694..2aacd516 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: square: specifier: ^34.0.1 version: 34.0.1 + swr: + specifier: ^2.2.5 + version: 2.2.5(react@18.2.0) zod: specifier: ^3.22.4 version: 3.22.4 @@ -5778,6 +5781,16 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /swr@2.2.5(react@18.2.0): + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /synckit@0.8.8: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/app/(account)/join/steps/StepFour.tsx b/src/app/(account)/join/steps/StepFour.tsx index 0bf72554..dab55abe 100644 --- a/src/app/(account)/join/steps/StepFour.tsx +++ b/src/app/(account)/join/steps/StepFour.tsx @@ -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() { @@ -16,19 +18,23 @@ export default function StepFour() { const { prevStep } = useJoinUsStep(); const { studentInfo } = useJoinUsStudentInfo(); - const handleSignUp = async (e: React.ChangeEvent) => { + const router = useRouter(); + const createMember = useSWRMutation('member', fetcher.post, { + 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); @@ -48,7 +54,7 @@ export default function StepFour() { - diff --git a/src/app/api/member/route.ts b/src/app/api/member/route.ts index 3feb3939..3ccf4e32 100644 --- a/src/app/api/member/route.ts +++ b/src/app/api/member/route.ts @@ -1,12 +1,12 @@ import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { currentUser } from '@clerk/nextjs'; import { createInsertSchema } from 'drizzle-zod'; import { z } from 'zod'; export async function POST(request: Request) { const req = await request.json(); - const schema = createInsertSchema(members, { + const schema = createInsertSchema(memberTable, { clerkId: z.undefined(), email: z.undefined(), }); @@ -21,7 +21,7 @@ export async function POST(request: Request) { return new Response(JSON.stringify(reqBody.error.format()), { status: 400 }); } - await db.insert(members).values({ + await db.insert(memberTable).values({ clerkId: user.id, email: user.emailAddresses[0].emailAddress, ...reqBody.data, diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7a047bf7..73c1f249 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,29 +7,24 @@ interface ButtonProps { children: React.ReactNode; colour: Colour; href?: string; - onClick?: (e: React.MouseEvent) => void | Promise; + onClick?: () => void; type?: 'button' | 'submit' | 'reset'; width?: string; + loading?: boolean; } -const Button = ({ children, colour, href, onClick, width, type }: ButtonProps) => { +const Button = ({ children, colour, href, onClick, width, type, loading }: ButtonProps) => { const isAnchor = !!href; const Component = isAnchor ? 'a' : 'button'; - const buttonStyles = `whitespace-nowrap py-4 px-12 md:py-1 md:px-2 lg:py-2 lg:px-6 border-2 border-black font-bold hover:bg-yellow transition-colors duration-300 ${BG_COLOURS[colour]} text-lg md:text-base`; - const buttonClasses = width ? `${buttonStyles} ${width}` : buttonStyles; - return ( - + | React.MouseEvent - ) => void | Promise - } + onClick={onClick} type={isAnchor ? undefined : type} - className={buttonClasses} + className={`${width} ${BG_COLOURS[colour]} ${isAnchor ? 'hover:bg-yellow' : 'hover:enabled:bg-yellow'} whitespace-nowrap border-2 border-black px-12 py-4 text-lg font-bold transition-colors duration-300 disabled:cursor-wait disabled:grayscale md:px-2 md:py-1 md:text-base lg:px-6 lg:py-2`} + disabled={loading} > {children} diff --git a/src/db/schema.ts b/src/db/schema.ts index 70f802ea..55f32ead 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -9,7 +9,7 @@ import { sql } from 'drizzle-orm'; import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { nanoid } from 'nanoid'; -export const members = sqliteTable('members', { +export const memberTable = sqliteTable('members', { id: text('id') .$defaultFn(() => nanoid()) .primaryKey(), @@ -23,8 +23,8 @@ export const members = sqliteTable('members', { studentId: text('student_id'), gender: text('gender', { enum: GENDERS }).notNull(), ageBracket: text('age_bracket', { enum: AGE_BRACKETS }).notNull(), - degree: text('degree', { enum: DEGREES }), - studentType: text('student_type', { enum: STUDENT_TYPES }), + degree: text('degree', { enum: [...DEGREES, ''] }), + studentType: text('student_type', { enum: [...STUDENT_TYPES, ''] }), emailPreferences: text('email_preferences', { mode: 'json' }), diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index 5b403084..26dd0222 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -1,3 +1,10 @@ import ky from 'ky'; -export const fetcher = ky.create({ prefixUrl: '/api' }); +const kyInstance = ky.create({ prefixUrl: '/api' }); + +export const fetcher = { + get: async (args: Parameters) => + (await kyInstance.get(...args).json()) as any, + post: async (url: string, { arg }: { arg: unknown }) => + (await kyInstance.post(url, { json: arg }).json()) as any, +}; From fc5c6c55ab371e5326bfb19d87f4c9dbf782796e Mon Sep 17 00:00:00 2001 From: jsun969 Date: Fri, 16 Feb 2024 18:14:45 +1030 Subject: [PATCH 16/32] fix(header): fetch data more fluently & hide join button when user exists --- src/app/api/check-user-exists/route.ts | 26 +++------- src/components/Header.tsx | 68 +++++++++++++++----------- src/components/UserButton.tsx | 34 ++----------- src/lib/fetcher.ts | 2 +- 4 files changed, 51 insertions(+), 79 deletions(-) diff --git a/src/app/api/check-user-exists/route.ts b/src/app/api/check-user-exists/route.ts index 667b9a1c..261806bf 100644 --- a/src/app/api/check-user-exists/route.ts +++ b/src/app/api/check-user-exists/route.ts @@ -1,29 +1,17 @@ import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { currentUser } from '@clerk/nextjs'; import { eq } from 'drizzle-orm'; -export async function GET(request: Request) { +export async function GET() { const user = await currentUser(); if (!user) { return new Response(null, { status: 401 }); } - try { - const existingUser = await db - .select({ - id: members.id, - }) - .from(members) - .where(eq(members.clerkId, user.id)); - - if (existingUser.length > 0) { - return new Response(JSON.stringify({ exists: true }), { status: 200 }); - } else { - return new Response(JSON.stringify({ exists: false }), { status: 404 }); - } - } catch (error) { - console.error('Error checking user existence:', error); - return new Response(null, { status: 500 }); - } + const existingUser = await db + .select({ id: memberTable.id }) + .from(memberTable) + .where(eq(memberTable.clerkId, user.id)); + return Response.json({ exists: existingUser.length > 0 }); } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9af155dc..a0d132a9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -3,24 +3,31 @@ import FancyRectangle from '@/components/FancyRectangle'; import { BREAKPOINTS } from '@/constants/breakpoints'; import { useMount } from '@/hooks/use-mount'; +import { fetcher } from '@/lib/fetcher'; import { useUser } from '@clerk/nextjs'; import Image from 'next/image'; import Link from 'next/link'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { IoMdClose, IoMdMenu } from 'react-icons/io'; +import useSWR from 'swr'; import Button from './Button'; import UserButton from './UserButton'; export default function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); - const headerRef = useRef(null); - const [isScrolled, setIsScrolled] = useState(false); - const { isSignedIn } = useUser(); - const toggleMenu = () => { setIsMenuOpen(!isMenuOpen); }; + const closeMenu = () => { + setIsMenuOpen(false); + }; + + const clerkUser = useUser(); + const checkUserExists = useSWR<{ exists: boolean }>('check-user-exists', fetcher.get, { + isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, + }); + const [isScrolled, setIsScrolled] = useState(false); useMount(() => { window.addEventListener('scroll', handleScroll); window.addEventListener('resize', handleResize); @@ -30,22 +37,17 @@ export default function Header() { window.removeEventListener('resize', handleResize); }; }); - - const closeMenu = () => { - setIsMenuOpen(false); - }; - const handleResize = () => { if (window.innerWidth >= BREAKPOINTS.md) { setIsMenuOpen(false); } }; - const handleScroll = () => { const scrollPosition = window.scrollY; setIsScrolled(scrollPosition > 0); }; + const isLoading = !clerkUser.isLoaded || checkUserExists.isLoading; return (
- {isSignedIn ? ( - <> - - - - ) : ( - <> - - - - )} + {!isLoading && + (clerkUser.isSignedIn ? ( + <> + {!checkUserExists.data?.exists && ( + + )} + + + ) : ( + <> + + + + ))}
diff --git a/src/components/UserButton.tsx b/src/components/UserButton.tsx index b7ed6093..0614d1fc 100644 --- a/src/components/UserButton.tsx +++ b/src/components/UserButton.tsx @@ -1,38 +1,15 @@ import Button from '@/components/Button'; -import { useMount } from '@/hooks/use-mount'; import { useClerk, useUser } from '@clerk/clerk-react'; import Image from 'next/image'; import Link from 'next/link'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import FancyRectangle from './FancyRectangle'; -export default function UserButton() { +export default function UserButton({ userExists }: { userExists: boolean }) { const { user } = useUser(); const [isPopupOpen, setPopupOpen] = useState(false); - const [userFound, setUserFound] = useState(false); - const popupRef = useRef(null); const { signOut } = useClerk(); - useMount(() => { - const checkUserExists = async () => { - try { - const response = await fetch('/api/check-user-exists'); - if (response.ok) { - const data = await response.json(); - const userExists = data.exists; - setUserFound(userExists); - console.log('User exists:', userExists); - } else { - console.error('Failed to fetch user existence status:', response.status); - } - } catch (error) { - console.error('Error checking user existence:', error); - } - }; - - checkUserExists(); - }); - const handleButtonClick = () => { setPopupOpen(!isPopupOpen); }; @@ -53,12 +30,9 @@ export default function UserButton() { {/* Popup menu */} {isPopupOpen && ( -
+
{/* Only show options if finished sign up */} - {userFound && ( + {userExists && ( <> {/* TODO(#16): Link to CS Club Drive */} ) => + get: async (...args: Parameters) => (await kyInstance.get(...args).json()) as any, post: async (url: string, { arg }: { arg: unknown }) => (await kyInstance.post(url, { json: arg }).json()) as any, From 4b0a3d0b36a7f9021b5f724b1eb5b2c504f7acc3 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 00:11:54 +1030 Subject: [PATCH 17/32] refactor(settings): move setting tabs to `tabs` folder --- .../settings/{ => tabs}/AccountSettings.tsx | 0 .../{ => tabs}/MembershipSettings.tsx | 0 .../{ => tabs}/NotificationsSettings.tsx | 0 .../{ => tabs}/PersonalInfoSettings.tsx | 30 +++---------------- 4 files changed, 4 insertions(+), 26 deletions(-) rename src/app/(account)/settings/{ => tabs}/AccountSettings.tsx (100%) rename src/app/(account)/settings/{ => tabs}/MembershipSettings.tsx (100%) rename src/app/(account)/settings/{ => tabs}/NotificationsSettings.tsx (100%) rename src/app/(account)/settings/{ => tabs}/PersonalInfoSettings.tsx (77%) diff --git a/src/app/(account)/settings/AccountSettings.tsx b/src/app/(account)/settings/tabs/AccountSettings.tsx similarity index 100% rename from src/app/(account)/settings/AccountSettings.tsx rename to src/app/(account)/settings/tabs/AccountSettings.tsx diff --git a/src/app/(account)/settings/MembershipSettings.tsx b/src/app/(account)/settings/tabs/MembershipSettings.tsx similarity index 100% rename from src/app/(account)/settings/MembershipSettings.tsx rename to src/app/(account)/settings/tabs/MembershipSettings.tsx diff --git a/src/app/(account)/settings/NotificationsSettings.tsx b/src/app/(account)/settings/tabs/NotificationsSettings.tsx similarity index 100% rename from src/app/(account)/settings/NotificationsSettings.tsx rename to src/app/(account)/settings/tabs/NotificationsSettings.tsx diff --git a/src/app/(account)/settings/PersonalInfoSettings.tsx b/src/app/(account)/settings/tabs/PersonalInfoSettings.tsx similarity index 77% rename from src/app/(account)/settings/PersonalInfoSettings.tsx rename to src/app/(account)/settings/tabs/PersonalInfoSettings.tsx index f1b0c11b..3b3e52ff 100644 --- a/src/app/(account)/settings/PersonalInfoSettings.tsx +++ b/src/app/(account)/settings/tabs/PersonalInfoSettings.tsx @@ -1,6 +1,6 @@ import Button from '@/components/Button'; import ControlledField from '@/components/ControlledField'; -import { AGE_BRACKETS, GENDERS, STUDENT_TYPES, STUDENT_STATUSES } from '@/constants/student-info'; +import { AGE_BRACKETS, GENDERS, STUDENT_STATUSES, STUDENT_TYPES } from '@/constants/student-info'; import { useMount } from '@/hooks/use-mount'; import { useUser } from '@clerk/nextjs'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -66,27 +66,14 @@ export default function PersonalInfoSettings() {

Change Personal Info

- - + + - + From 82f32544b9540eca0b33d04b7ac39777d10b4132 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 02:10:14 +1030 Subject: [PATCH 18/32] refactor(lib/fetcher): add `query` and `mutate` fetch type --- src/app/(account)/join/steps/StepFour.tsx | 2 +- src/components/Header.tsx | 2 +- src/lib/fetcher.ts | 25 ++++++++++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/app/(account)/join/steps/StepFour.tsx b/src/app/(account)/join/steps/StepFour.tsx index dab55abe..6fbcebd5 100644 --- a/src/app/(account)/join/steps/StepFour.tsx +++ b/src/app/(account)/join/steps/StepFour.tsx @@ -19,7 +19,7 @@ export default function StepFour() { const { studentInfo } = useJoinUsStudentInfo(); const router = useRouter(); - const createMember = useSWRMutation('member', fetcher.post, { + const createMember = useSWRMutation('member', fetcher.post.mutate, { onError: () => { setAgreementError('Server error.'); }, diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a0d132a9..02ea0109 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -23,7 +23,7 @@ export default function Header() { }; const clerkUser = useUser(); - const checkUserExists = useSWR<{ exists: boolean }>('check-user-exists', fetcher.get, { + const checkUserExists = useSWR<{ exists: boolean }>('check-user-exists', fetcher.get.query, { isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, }); diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index 26513148..c07d4c89 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -2,9 +2,24 @@ import ky from 'ky'; const kyInstance = ky.create({ prefixUrl: '/api' }); -export const fetcher = { - get: async (...args: Parameters) => - (await kyInstance.get(...args).json()) as any, - post: async (url: string, { arg }: { arg: unknown }) => - (await kyInstance.post(url, { json: arg }).json()) as any, +const METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const; +type Methods = (typeof METHODS)[number]; + +type Fetcher = { + [Method in Methods]: { + query: (args: Parameters<(typeof kyInstance)[Method]>) => Promise; + mutate: (url: string, { arg }: { arg: unknown }) => Promise; + }; }; +export const fetcher = METHODS.reduce( + (acc, method) => ({ + ...acc, + [method]: { + // @ts-expect-error - args type is unnecessary + query: async (args: any[]) => (await kyInstance[method](...args).json()) as any, + mutate: async (url: string, { arg }: { arg: unknown }) => + (await kyInstance[method](url, { json: arg }).json()) as any, + }, + }), + {} as Fetcher +); From 2a2de505000600df381d2b99d7e9763802d421c6 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 02:11:12 +1030 Subject: [PATCH 19/32] feat(settings): enable ssr --- src/app/(account)/settings/Settings.tsx | 89 +++++------------------ src/app/(account)/settings/Sidebar.tsx | 57 ++++++++------- src/app/(account)/settings/SidebarTab.tsx | 17 ----- src/app/(account)/settings/page.tsx | 59 +++------------ src/app/api/check-user-exists/route.ts | 11 +-- src/db/queries.ts | 11 +++ 6 files changed, 73 insertions(+), 171 deletions(-) delete mode 100644 src/app/(account)/settings/SidebarTab.tsx create mode 100644 src/db/queries.ts diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index d0d1b615..3e8c65af 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,75 +1,26 @@ -import { useUser } from '@clerk/nextjs'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import AccountSettings from './AccountSettings'; -import MembershipSettings from './MembershipSettings'; -import NotificationsSettings from './NotificationsSettings'; -import PersonalInfoSettings from './PersonalInfoSettings'; - -interface SettingsProps { - selectedTab: string; - setSelectedTab: (tab: string) => void; -} +'use client'; -export default function Settings({ selectedTab, setSelectedTab }: SettingsProps) { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm(); - const { user } = useUser(); - const [membershipStatus, setMembershipStatus] = useState('Checking...'); - const [membershipExpirationDate, setMembershipExpirationDate] = useState(''); +import { useState } from 'react'; +import Sidebar from './Sidebar'; +import MembershipSettings from './tabs/MembershipSettings'; - const handleGoToMembership = () => { - setSelectedTab('membership'); - }; +export const TAB_NAMES = ['account', 'personalInfo', 'membership', 'notifications'] as const; +export type TabNames = (typeof TAB_NAMES)[number]; - const onSubmit = (data: any) => { - // TODO: Send data to Clerk via API - console.log(data); - }; +const SETTING_TABS = { + account: <>, + personalInfo: <>, + membership: , + notifications: <>, +} as const satisfies Record; - const renderSettings = () => { - switch (selectedTab) { - case 'account': - return ( - - ); - case 'personalInfo': - return ( - - ); - case 'membership': - return ( - - ); - case 'notifications': - return ; - default: - return null; - } - }; +export default function Settings() { + const [tab, setTab] = useState('membership'); - return
{renderSettings()}
; + return ( + <> + setTab(tab)} /> +
{SETTING_TABS[tab]}
+ + ); } diff --git a/src/app/(account)/settings/Sidebar.tsx b/src/app/(account)/settings/Sidebar.tsx index 7cdfef55..c608de04 100644 --- a/src/app/(account)/settings/Sidebar.tsx +++ b/src/app/(account)/settings/Sidebar.tsx @@ -1,36 +1,39 @@ -import SidebarTab from './SidebarTab'; +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 ( + + ); +} interface SidebarProps { - selectedTab: string; - handleTabClick: (tab: string) => void; + currentTab: TabNames; + onTabChange: (tab: TabNames) => void; } -export default function Sidebar({ selectedTab, handleTabClick }: SidebarProps) { +export default function Sidebar({ currentTab, onTabChange }: SidebarProps) { return (
- {/* TODO: Implement account settings */} - {/* handleTabClick('account')} - /> */} - {/* TODO: Implement personal info settings */} - {/* handleTabClick('personalInfo')} - /> */} - handleTabClick('membership')} - /> - {/* TODO: Implement email notification settings */} - {/* handleTabClick('notifications')} - /> */} + {TAB_NAMES.map((tab, i) => ( + + ))}
); } diff --git a/src/app/(account)/settings/SidebarTab.tsx b/src/app/(account)/settings/SidebarTab.tsx deleted file mode 100644 index eb4de48f..00000000 --- a/src/app/(account)/settings/SidebarTab.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface SidebarTabProps { - tabName: string; - selected: boolean; - onClick: () => void; -} - -export default function SidebarTab({ tabName, selected, onClick }: SidebarTabProps) { - return ( - - ); -} diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 77a22ed7..71b3ca1a 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -1,41 +1,13 @@ -'use client'; - import FancyRectangle from '@/components/FancyRectangle'; import Title from '@/components/Title'; -import { useMount } from '@/hooks/use-mount'; +import { checkUserExists } from '@/db/queries'; +import { currentUser } from '@clerk/nextjs'; import Link from 'next/link'; -import { useState } from 'react'; import Settings from './Settings'; -import Sidebar from './Sidebar'; - -export default function SettingsPage() { - const [selectedTab, setSelectedTab] = useState('membership'); - const [userFound, setUserFound] = useState(''); - - useMount(() => { - const checkUserExists = async () => { - try { - const response = await fetch('/api/check-user-exists'); - if (response.ok) { - const data = await response.json(); - const userExists = data.exists; - setUserFound('true'); - console.log('User exists:', userExists); - } else { - setUserFound('false'); - console.error('Failed to fetch user existence status:', response.status); - } - } catch (error) { - console.error('Error checking user existence:', error); - } - }; - checkUserExists(); - }); - - const handleTabClick = (tab: string) => { - setSelectedTab(tab); - }; +export default async function SettingsPage() { + const user = await currentUser(); + const exists = user !== null && (await checkUserExists(user.id)); return (
@@ -45,25 +17,12 @@ export default function SettingsPage() {
- {userFound == 'true' && ( - <> - - - - )} - {userFound == 'false' && ( + {exists ? ( + + ) : (

Please finishing{' '} - + signing up {' '} first. diff --git a/src/app/api/check-user-exists/route.ts b/src/app/api/check-user-exists/route.ts index 261806bf..cb2b62d0 100644 --- a/src/app/api/check-user-exists/route.ts +++ b/src/app/api/check-user-exists/route.ts @@ -1,7 +1,5 @@ -import { db } from '@/db'; -import { memberTable } from '@/db/schema'; +import { checkUserExists } from '@/db/queries'; import { currentUser } from '@clerk/nextjs'; -import { eq } from 'drizzle-orm'; export async function GET() { const user = await currentUser(); @@ -9,9 +7,6 @@ export async function GET() { return new Response(null, { status: 401 }); } - const existingUser = await db - .select({ id: memberTable.id }) - .from(memberTable) - .where(eq(memberTable.clerkId, user.id)); - return Response.json({ exists: existingUser.length > 0 }); + const exists = await checkUserExists(user.id); + return Response.json({ exists }); } diff --git a/src/db/queries.ts b/src/db/queries.ts new file mode 100644 index 00000000..ffe1706d --- /dev/null +++ b/src/db/queries.ts @@ -0,0 +1,11 @@ +import { eq } from 'drizzle-orm'; +import { db } from '.'; +import { memberTable } from './schema'; + +export const checkUserExists = async (clerkUserId: string) => { + const existingUser = await db + .select({ count: memberTable.id }) + .from(memberTable) + .where(eq(memberTable.clerkId, clerkUserId)); + return existingUser.length > 0; +}; From 796d8b9171a42582c01fb805edb88fa75ed1e1f9 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 02:15:03 +1030 Subject: [PATCH 20/32] refactor(settings/membership): clean unnecessary code & improve DX --- .../settings/tabs/MembershipSettings.tsx | 100 ++++++------------ 1 file changed, 33 insertions(+), 67 deletions(-) diff --git a/src/app/(account)/settings/tabs/MembershipSettings.tsx b/src/app/(account)/settings/tabs/MembershipSettings.tsx index 24a895ef..566a9f2e 100644 --- a/src/app/(account)/settings/tabs/MembershipSettings.tsx +++ b/src/app/(account)/settings/tabs/MembershipSettings.tsx @@ -1,89 +1,55 @@ import Button from '@/components/Button'; -import { useMount } from '@/hooks/use-mount'; +import { fetcher } from '@/lib/fetcher'; import { formatDate } from '@/utils/format-date'; import { useUser } from '@clerk/nextjs'; import Link from 'next/link'; +import { useState } from 'react'; +import type { PaymentLink } from 'square'; +import useSWR from 'swr'; +import useSWRMutation from 'swr/mutation'; -interface MembershipSettingsProps { - membershipStatus: string; - setMembershipStatus: (status: string) => void; - membershipExpirationDate: string; - setMembershipExpirationDate: (date: string) => void; -} +type MembershipStatus = 'Checking...' | 'Paid' | 'Payment Required'; -export default function MembershipSettings({ - membershipStatus, - setMembershipStatus, - membershipExpirationDate, - setMembershipExpirationDate, -}: MembershipSettingsProps) { +export default function MembershipSettings() { const { user } = useUser(); - useMount(() => { - const verifyMembershipPayment = async () => { - console.log('Verifying membership payment'); - try { - const response = await fetch('/api/verify-membership-payment', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - redirectUrl: window.location.href, - }), - }); + const [membershipStatus, setMembershipStatus] = useState('Checking...'); + const [membershipExpirationDate, setMembershipExpirationDate] = useState(''); - if (response.ok) { - const data = await response.json(); - setMembershipStatus('Paid'); - const expirationDate = formatDate(data.membershipExpiresAt); - setMembershipExpirationDate(expirationDate); - } else { - setMembershipStatus('Payment Required'); - } - } catch (error) { - console.error('Error verifying membership payment:', error); - } - }; + useSWR<{ membershipExpiresAt: string }>( + ['verify-membership-payment', { json: { redirectUrl: window.location.href } }], + fetcher.put.query, + { + onSuccess: (data) => { + setMembershipStatus('Paid'); + const expirationDate = formatDate(data.membershipExpiresAt); + setMembershipExpirationDate(expirationDate); + }, + onError: () => { + setMembershipStatus('Payment Required'); + }, + } + ); - verifyMembershipPayment(); + const pay = useSWRMutation('payment', fetcher.post.mutate, { + onSuccess: async (data: PaymentLink) => { + window.location.href = data.url!; + }, }); const handlePayment = async () => { - try { - if (!user || !user.id) { - console.error('User not authenticated or ID not available'); - return; - } - - const response = await fetch('/api/payment', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - product: 'membership', - customerId: user.id, - redirectUrl: window.location.href, - }), - }); - - if (response.ok) { - const paymentLink = await response.json(); - window.location.href = paymentLink.url; - } else { - console.error('Failed to create payment link'); - } - } catch (error) { - console.error('Error creating payment link:', error); - } + pay.trigger({ + product: 'membership', + customerId: user!.id, + redirectUrl: window.location.href, + }); }; return (
{membershipStatus !== null && (
-

+

Membership Status:{' '} Date: Sat, 17 Feb 2024 02:15:45 +1030 Subject: [PATCH 21/32] fix(api): rename table name & minor fix --- src/app/api/get-user-info/route.ts | 41 ++++++++----------- src/app/api/payment/route.ts | 10 ++--- .../api/verify-membership-payment/route.ts | 20 ++++----- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/app/api/get-user-info/route.ts b/src/app/api/get-user-info/route.ts index 28fe335c..feab9895 100644 --- a/src/app/api/get-user-info/route.ts +++ b/src/app/api/get-user-info/route.ts @@ -1,35 +1,30 @@ import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { currentUser } from '@clerk/nextjs'; import { eq } from 'drizzle-orm'; -export async function GET(request: Request) { +export async function GET() { const user = await currentUser(); if (!user) { return new Response(null, { status: 401 }); } - try { - const userData = await db - .select({ - firstName: members.firstName, - lastName: members.lastName, - ageBracket: members.ageBracket, - gender: members.gender, - studentType: members.studentType, - studentStatus: members.studentStatus, - studentId: members.studentId, - }) - .from(members) - .where(eq(members.clerkId, user.id)); + const userData = await db + .select({ + firstName: memberTable.firstName, + lastName: memberTable.lastName, + ageBracket: memberTable.ageBracket, + gender: memberTable.gender, + studentType: memberTable.studentType, + studentStatus: memberTable.studentStatus, + studentId: memberTable.studentId, + }) + .from(memberTable) + .where(eq(memberTable.clerkId, user.id)); - if (!userData) { - return new Response(JSON.stringify({}), { status: 404 }); - } - - return new Response(JSON.stringify(userData), { status: 200 }); - } catch (error) { - console.error('Error fetching user data:', error); - return new Response(null, { status: 500 }); + if (!userData) { + return new Response(null, { status: 404 }); } + + return Response.json(userData); } diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index 2f3f2c02..f94c8e4e 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -6,14 +6,14 @@ */ import { PRODUCTS } from '@/data/products'; import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { env } from '@/env.mjs'; import { redisClient } from '@/lib/redis'; import { squareClient } from '@/lib/square'; import { currentUser } from '@clerk/nextjs'; import { eq } from 'drizzle-orm'; import type { NextRequest } from 'next/server'; -import type { CreatePaymentLinkRequest, OrderLineItem } from 'square'; +import type { CreatePaymentLinkRequest } from 'square'; import { ApiError } from 'square'; import { z } from 'zod'; @@ -73,10 +73,10 @@ export async function POST(request: Request) { // Add user ID and payment ID to Redis cache const [{ id: userId }] = await db .select({ - id: members.id, + id: memberTable.id, }) - .from(members) - .where(eq(members.clerkId, user.id)); + .from(memberTable) + .where(eq(memberTable.clerkId, user.id)); const paymentId = resp.result.paymentLink?.id ?? ''; const createdAt = resp.result.paymentLink?.createdAt ?? ''; await redisClient.hSet(`payment:membership:${userId}`, { diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts index c79311c4..e05ccaef 100644 --- a/src/app/api/verify-membership-payment/route.ts +++ b/src/app/api/verify-membership-payment/route.ts @@ -1,5 +1,5 @@ import { db } from '@/db'; -import { members } from '@/db/schema'; +import { memberTable } from '@/db/schema'; import { redisClient } from '@/lib/redis'; import { squareClient } from '@/lib/square'; import { currentUser } from '@clerk/nextjs'; @@ -41,23 +41,23 @@ export async function PUT(request: Request) { // Get user's membership expiry date from the database const [{ membershipExpiresAt }] = await db .select({ - membershipExpiresAt: members.membershipExpiresAt, + membershipExpiresAt: memberTable.membershipExpiresAt, }) - .from(members) - .where(eq(members.clerkId, user.id)); + .from(memberTable) + .where(eq(memberTable.clerkId, user.id)); // If membership expiry date exists, return the existing date if (membershipExpiresAt) { - return new Response(JSON.stringify({ membershipExpiresAt }), { status: 200 }); + return Response.json({ membershipExpiresAt }); } // Get payment ID from Redis cache const [{ id: userId }] = await db .select({ - id: members.id, + id: memberTable.id, }) - .from(members) - .where(eq(members.clerkId, user.id)); + .from(memberTable) + .where(eq(memberTable.clerkId, user.id)); const paymentId = await redisClient.hGet(`payment:membership:${userId}`, 'paymentId'); if (!paymentId) { return new Response('Membership payment for the user does not exist', { status: 404 }); @@ -72,9 +72,9 @@ export async function PUT(request: Request) { const now = new Date(); const expiryDate = new Date(now.setFullYear(now.getFullYear() + 1)); await db - .update(members) + .update(memberTable) .set({ membershipExpiresAt: expiryDate }) - .where(eq(members.id, userId)); + .where(eq(memberTable.id, userId)); } catch (e) { if (e instanceof ApiError) { return new Response(JSON.stringify(e.errors), { status: e.statusCode }); From 42aba0918101f35173130ce4b364707ff09b27d8 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 02:19:23 +1030 Subject: [PATCH 22/32] fix(settings): tab titles --- src/app/(account)/settings/Settings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index 3e8c65af..dcf5ccd3 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -4,18 +4,18 @@ import { useState } from 'react'; import Sidebar from './Sidebar'; import MembershipSettings from './tabs/MembershipSettings'; -export const TAB_NAMES = ['account', 'personalInfo', 'membership', 'notifications'] as const; +export const TAB_NAMES = ['Account', 'Personal Info', 'Membership', 'Notifications'] as const; export type TabNames = (typeof TAB_NAMES)[number]; const SETTING_TABS = { - account: <>, - personalInfo: <>, - membership: , - notifications: <>, + Account: <>, + 'Personal Info': <>, + Membership: , + Notifications: <>, } as const satisfies Record; export default function Settings() { - const [tab, setTab] = useState('membership'); + const [tab, setTab] = useState('Membership'); return ( <> From 4b21dd14a554137e0cf23846418cc5d27f6878f3 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 02:24:57 +1030 Subject: [PATCH 23/32] fix(header): fetcher query params --- src/components/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 02ea0109..2d2ee757 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -23,7 +23,7 @@ export default function Header() { }; const clerkUser = useUser(); - const checkUserExists = useSWR<{ exists: boolean }>('check-user-exists', fetcher.get.query, { + const checkUserExists = useSWR<{ exists: boolean }>(['check-user-exists'], fetcher.get.query, { isPaused: () => clerkUser.isLoaded && !clerkUser.isSignedIn, }); From 08ed60bb357968445d47493077ba7959d0942dd9 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 13:50:13 +1030 Subject: [PATCH 24/32] feat(settings): enable ssr for all setting tabs --- src/app/(account)/settings/Settings.tsx | 23 +++-- src/app/(account)/settings/page.tsx | 49 +++++++++- .../settings/tabs/MembershipSettings.tsx | 94 +++++++------------ .../api/verify-membership-payment/route.ts | 90 ------------------ src/utils/format-date.ts | 3 +- 5 files changed, 95 insertions(+), 164 deletions(-) delete mode 100644 src/app/api/verify-membership-payment/route.ts diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index dcf5ccd3..a384f6d4 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,26 +1,33 @@ 'use client'; -import { useState } from 'react'; +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: <>, - 'Personal Info': <>, - Membership: , - Notifications: <>, -} as const satisfies Record; + Account: () => <>, + 'Personal Info': () => <>, + Membership: MembershipSettings, + Notifications: () => <>, +} as const satisfies Record; -export default function Settings() { +export default function Settings({ settingData }: { settingData: SettingData }) { const [tab, setTab] = useState('Membership'); + const Tab = SETTING_TABS[tab]; return ( <> setTab(tab)} /> -
{SETTING_TABS[tab]}
+
+ +
); } diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 71b3ca1a..2b610b44 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -1,13 +1,58 @@ 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 }; + } + + const now = new Date(); + const expiryDate = new Date(now.setFullYear(now.getFullYear() + 1)); + await db + .update(memberTable) + .set({ membershipExpiresAt: expiryDate }) + .where(eq(memberTable.id, clerkId)); + return { paid: true as const, membershipExpiresAt: expiryDate }; +}; +export type MembershipPayment = Awaited>; + export default async function SettingsPage() { const user = await currentUser(); - const exists = user !== null && (await checkUserExists(user.id)); + if (!user) return notFound(); + + const exists = await checkUserExists(user.id); + const membershipPayment = await verifyMembershipPayment(user.id); return (
@@ -18,7 +63,7 @@ export default async function SettingsPage() {
{exists ? ( - + ) : (

Please finishing{' '} diff --git a/src/app/(account)/settings/tabs/MembershipSettings.tsx b/src/app/(account)/settings/tabs/MembershipSettings.tsx index 566a9f2e..dadb894f 100644 --- a/src/app/(account)/settings/tabs/MembershipSettings.tsx +++ b/src/app/(account)/settings/tabs/MembershipSettings.tsx @@ -1,36 +1,17 @@ import Button from '@/components/Button'; import { fetcher } from '@/lib/fetcher'; import { formatDate } from '@/utils/format-date'; -import { useUser } from '@clerk/nextjs'; +import { useUser } from '@clerk/clerk-react'; import Link from 'next/link'; -import { useState } from 'react'; import type { PaymentLink } from 'square'; -import useSWR from 'swr'; import useSWRMutation from 'swr/mutation'; +import type { SettingTabProps } from '../Settings'; -type MembershipStatus = 'Checking...' | 'Paid' | 'Payment Required'; - -export default function MembershipSettings() { +export default function MembershipSettings({ + settingData: { membershipPayment: payment }, +}: SettingTabProps) { const { user } = useUser(); - const [membershipStatus, setMembershipStatus] = useState('Checking...'); - const [membershipExpirationDate, setMembershipExpirationDate] = useState(''); - - useSWR<{ membershipExpiresAt: string }>( - ['verify-membership-payment', { json: { redirectUrl: window.location.href } }], - fetcher.put.query, - { - onSuccess: (data) => { - setMembershipStatus('Paid'); - const expirationDate = formatDate(data.membershipExpiresAt); - setMembershipExpirationDate(expirationDate); - }, - onError: () => { - setMembershipStatus('Payment Required'); - }, - } - ); - const pay = useSWRMutation('payment', fetcher.post.mutate, { onSuccess: async (data: PaymentLink) => { window.location.href = data.url!; @@ -47,45 +28,34 @@ export default function MembershipSettings() { return (
- {membershipStatus !== null && ( -
-

- Membership Status:{' '} - - {membershipStatus} - -

+

+ Membership Status:{' '} + + {payment.paid ? 'Paid' : 'Payment Required'} + +

+
+ {payment.paid ? ( +

+ You are a CS Club member! Your membership expires on{' '} + {formatDate(payment.membershipExpiresAt)}. +

+ ) : ( + <> +

+ Finalise your membership by completing the required payment either online + below, at a club event, or contact one of the{' '} + + committee members + + . +

+

Pay Membership Fee

- {membershipStatus === 'Payment Required' && ( - <> -

- Finalise your membership by completing the required payment either - online below, at a club event, or contact one of the{' '} - - committee members - - . -

-

Pay Membership Fee

-
- - - )} - {membershipStatus === 'Paid' && ( -

- You are a CS Club member! Your membership expires on{' '} - {membershipExpirationDate}. -

- )} -
+ + )}
); diff --git a/src/app/api/verify-membership-payment/route.ts b/src/app/api/verify-membership-payment/route.ts deleted file mode 100644 index e05ccaef..00000000 --- a/src/app/api/verify-membership-payment/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { db } from '@/db'; -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 { redirect } from 'next/navigation'; -import { ApiError } from 'square'; -import { z } from 'zod'; - -/** - * Verify that the membership payment has been made and update the database to reflect the payment - * status - * - * A redirect URL can be specified as well. - */ -export async function PUT(request: Request) { - let req; - try { - req = await request.json(); - } catch (e) { - return new Response(null, { status: 400 }); - } - - // Ensure user is logged in - const user = await currentUser(); - if (!user) { - return new Response(null, { status: 401 }); - } - - const schema = z.object({ - redirectUrl: z.string().min(1).url().optional(), - }); - - const reqBody = schema.safeParse(req); - if (!reqBody.success) { - return new Response(JSON.stringify(reqBody.error.format()), { status: 400 }); - } - - try { - // Get user's membership expiry date from the database - const [{ membershipExpiresAt }] = await db - .select({ - membershipExpiresAt: memberTable.membershipExpiresAt, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, user.id)); - - // If membership expiry date exists, return the existing date - if (membershipExpiresAt) { - return Response.json({ membershipExpiresAt }); - } - - // Get payment ID from Redis cache - const [{ id: userId }] = await db - .select({ - id: memberTable.id, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, user.id)); - const paymentId = await redisClient.hGet(`payment:membership:${userId}`, 'paymentId'); - if (!paymentId) { - return new Response('Membership payment for the user does not exist', { status: 404 }); - } - - const resp = await squareClient.checkoutApi.retrievePaymentLink(paymentId); - const respFields = resp.result; - if (!respFields.paymentLink || respFields.paymentLink.id !== paymentId) { - return new Response('Payment has not been made', { status: 404 }); - } - - const now = new Date(); - const expiryDate = new Date(now.setFullYear(now.getFullYear() + 1)); - await db - .update(memberTable) - .set({ membershipExpiresAt: expiryDate }) - .where(eq(memberTable.id, userId)); - } catch (e) { - if (e instanceof ApiError) { - return new Response(JSON.stringify(e.errors), { status: e.statusCode }); - } - return new Response(null, { status: 500 }); - } - - if (reqBody.data.redirectUrl) { - redirect(reqBody.data.redirectUrl); - } - - return new Response(null, { status: 200 }); -} diff --git a/src/utils/format-date.ts b/src/utils/format-date.ts index 63b6c5b3..52048d07 100644 --- a/src/utils/format-date.ts +++ b/src/utils/format-date.ts @@ -1,6 +1,5 @@ // Function to format the date to DD/MM/YY format -export const formatDate = (dateString: string): string => { - const date = new Date(dateString); +export const formatDate = (date: Date) => { const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear().toString().slice(2); From fa0fd433ea9573cddc6c761b4e2c34d411d65ca6 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 13:54:11 +1030 Subject: [PATCH 25/32] feat(setting): add wip for wip tabs --- src/app/(account)/settings/Settings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index a384f6d4..25cacb79 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -12,10 +12,10 @@ export type SettingData = { membershipPayment: MembershipPayment }; export type SettingTabProps = { settingData: SettingData }; type SettingTabComponent = ({ settingData }: SettingTabProps) => React.ReactNode; const SETTING_TABS = { - Account: () => <>, - 'Personal Info': () => <>, + Account: () =>
WIP
, + 'Personal Info': () =>
WIP
, Membership: MembershipSettings, - Notifications: () => <>, + Notifications: () =>
WIP
, } as const satisfies Record; export default function Settings({ settingData }: { settingData: SettingData }) { From 20c8c66f7c41653ca5d612cfb815d1371386dbe8 Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 13:55:04 +1030 Subject: [PATCH 26/32] feat(api): remove unnecessary `getUserInfo` api (wip with ssr) --- src/app/api/get-user-info/route.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 src/app/api/get-user-info/route.ts diff --git a/src/app/api/get-user-info/route.ts b/src/app/api/get-user-info/route.ts deleted file mode 100644 index feab9895..00000000 --- a/src/app/api/get-user-info/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '@/db'; -import { memberTable } from '@/db/schema'; -import { currentUser } from '@clerk/nextjs'; -import { eq } from 'drizzle-orm'; - -export async function GET() { - const user = await currentUser(); - if (!user) { - return new Response(null, { status: 401 }); - } - - const userData = await db - .select({ - firstName: memberTable.firstName, - lastName: memberTable.lastName, - ageBracket: memberTable.ageBracket, - gender: memberTable.gender, - studentType: memberTable.studentType, - studentStatus: memberTable.studentStatus, - studentId: memberTable.studentId, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, user.id)); - - if (!userData) { - return new Response(null, { status: 404 }); - } - - return Response.json(userData); -} From 97d09dd3e1b3ba0701827d682b56d8e1e8a81d0f Mon Sep 17 00:00:00 2001 From: jsun969 Date: Sat, 17 Feb 2024 14:08:18 +1030 Subject: [PATCH 27/32] fix(settings): sidebar style --- src/app/(account)/settings/Sidebar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/(account)/settings/Sidebar.tsx b/src/app/(account)/settings/Sidebar.tsx index c608de04..ed1ba8f6 100644 --- a/src/app/(account)/settings/Sidebar.tsx +++ b/src/app/(account)/settings/Sidebar.tsx @@ -10,8 +10,12 @@ function SidebarTab({ tabName, currentTab, onTabChange }: SidebarTabProps) { const selected = currentTab === tabName; return ( @@ -25,7 +29,7 @@ interface SidebarProps { export default function Sidebar({ currentTab, onTabChange }: SidebarProps) { return ( -
+
{TAB_NAMES.map((tab, i) => ( Date: Sat, 17 Feb 2024 14:12:12 +1030 Subject: [PATCH 28/32] feat(settings): refresh rsc data when tab changes --- src/app/(account)/settings/Settings.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index 25cacb79..d7ba8d12 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useRouter } from 'next/navigation'; import React, { useState } from 'react'; import Sidebar from './Sidebar'; import type { MembershipPayment } from './page'; @@ -22,9 +23,17 @@ export default function Settings({ settingData }: { settingData: SettingData }) const [tab, setTab] = useState('Membership'); const Tab = SETTING_TABS[tab]; + const { refresh } = useRouter(); + return ( <> - setTab(tab)} /> + { + setTab(tab); + refresh(); + }} + />
From 632daab0ab6b8f6220d9c027dd12f4742c6c1b32 Mon Sep 17 00:00:00 2001 From: Ray <22254748+rayokamoto@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:35:30 +1030 Subject: [PATCH 29/32] fix: use Clerk ID for Redis key --- src/app/api/payment/route.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index f94c8e4e..1226ab6d 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -70,16 +70,10 @@ export async function POST(request: Request) { const resp = await squareClient.checkoutApi.createPaymentLink(body); if (reqBody.data.product === 'membership') { - // Add user ID and payment ID to Redis cache - const [{ id: userId }] = await db - .select({ - id: memberTable.id, - }) - .from(memberTable) - .where(eq(memberTable.clerkId, user.id)); + // Add Clerk ID and payment ID to Redis cache const paymentId = resp.result.paymentLink?.id ?? ''; const createdAt = resp.result.paymentLink?.createdAt ?? ''; - await redisClient.hSet(`payment:membership:${userId}`, { + await redisClient.hSet(`payment:membership:${user.id}`, { paymentId: paymentId, createdAt: createdAt, }); From 34815d9d883f69d859858042452ea3f33cb17022 Mon Sep 17 00:00:00 2001 From: Ray <22254748+rayokamoto@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:35:47 +1030 Subject: [PATCH 30/32] feat: delete Redis key once no longer needed --- src/app/(account)/settings/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 2b610b44..872320c1 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -43,6 +43,10 @@ const verifyMembershipPayment = async (clerkId: string) => { .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>; From ca3a5050ebc96268e2f275aeb6e7636ee1eaca74 Mon Sep 17 00:00:00 2001 From: Ray <22254748+rayokamoto@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:37:04 +1030 Subject: [PATCH 31/32] chore: remove unused imports --- src/app/api/payment/route.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/api/payment/route.ts b/src/app/api/payment/route.ts index 1226ab6d..1570a1bc 100644 --- a/src/app/api/payment/route.ts +++ b/src/app/api/payment/route.ts @@ -5,13 +5,10 @@ * verify that the user is signed in (see `src/middleware.ts`) */ import { PRODUCTS } from '@/data/products'; -import { db } from '@/db'; -import { memberTable } from '@/db/schema'; import { env } from '@/env.mjs'; import { redisClient } from '@/lib/redis'; import { squareClient } from '@/lib/square'; import { currentUser } from '@clerk/nextjs'; -import { eq } from 'drizzle-orm'; import type { NextRequest } from 'next/server'; import type { CreatePaymentLinkRequest } from 'square'; import { ApiError } from 'square'; From 1d745354131bc67eb142230bfa28d5add09d97aa Mon Sep 17 00:00:00 2001 From: Ray <22254748+rayokamoto@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:07:37 +1030 Subject: [PATCH 32/32] fix: set expiry date to Jan 1st of following year --- src/app/(account)/settings/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(account)/settings/page.tsx b/src/app/(account)/settings/page.tsx index 872320c1..1eecb0eb 100644 --- a/src/app/(account)/settings/page.tsx +++ b/src/app/(account)/settings/page.tsx @@ -37,8 +37,9 @@ const verifyMembershipPayment = async (clerkId: string) => { 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.setFullYear(now.getFullYear() + 1)); + const expiryDate = new Date(`${now.getFullYear() + 1}-01-01`); await db .update(memberTable) .set({ membershipExpiresAt: expiryDate })