From 20b984720e4e7b7ac4b06fabfa0b65eafd6f5fef Mon Sep 17 00:00:00 2001 From: Rohin Juneja Date: Thu, 5 Dec 2024 13:05:39 -0800 Subject: [PATCH 1/9] [feat] new success and verification pages with full functionality EXCEPT automatic rerouting from verification to success is not yet functional --- api/supabase/queries/auth.ts | 56 ++++++++++++ app/(auth)/signup/page.tsx | 2 +- app/(auth)/success/page.tsx | 38 ++++++++ app/(auth)/verification/page.tsx | 128 ++++++++++++++++++++++++++ app/onboarding/finalize/page.tsx | 4 +- public/images/bud.svg | 8 ++ public/images/email.svg | 4 + public/images/rose-greenbg.svg | 8 ++ styles/styles.ts | 152 +++++++++++++++++++++++++++++++ utils/AuthProvider.tsx | 17 +++- 10 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 app/(auth)/success/page.tsx create mode 100644 app/(auth)/verification/page.tsx create mode 100644 public/images/bud.svg create mode 100644 public/images/email.svg create mode 100644 public/images/rose-greenbg.svg create mode 100644 styles/styles.ts diff --git a/api/supabase/queries/auth.ts b/api/supabase/queries/auth.ts index bd16eef..f1d42ea 100644 --- a/api/supabase/queries/auth.ts +++ b/api/supabase/queries/auth.ts @@ -5,6 +5,7 @@ export async function handleSignUp( password: string, ): Promise<{ success: boolean; message: string }> { try { + await ensureLoggedOutForNewUser(email); const { data, error } = await supabase.auth.signUp({ email, password }); if (error) { @@ -37,6 +38,8 @@ export async function handleSignUp( }; } + localStorage.setItem('tempEmail', email); + return { success: true, message: 'Sign-up successful!' }; } catch (err) { if (err instanceof Error) { @@ -54,6 +57,7 @@ export async function handleSignIn( password: string, ): Promise<{ success: boolean; message: string }> { try { + await ensureLoggedOutForNewUser(email); const { error: signInError } = await supabase.auth.signInWithPassword({ email, password, @@ -96,6 +100,37 @@ export async function handleSignIn( } } +export const handleSignOut = async () => { + const { error } = await supabase.auth.signOut(); + if (error) { + console.error('Error during logout:', error.message); + return; + } +}; + +export async function ensureLoggedOutForNewUser( + newEmail: string, +): Promise { + const { data, error } = await supabase.auth.getSession(); + + if (error) { + console.error('Error fetching session:', error.message); + throw new Error('Failed to fetch session.'); + } + + const session = data.session; + + if ( + session && + session.user && + session.user.email && + session.user.email !== newEmail + ) { + console.log(`Logging out current user: ${session.user.email}`); + await handleSignOut(); + } +} + export async function checkUserExists( userId: string, userType: 'volunteer' | 'facility', @@ -118,3 +153,24 @@ export async function checkUserExists( return false; } } + +export async function resendVerificationEmail(email: string): Promise { + try { + const { error } = await supabase.auth.resend({ + type: 'signup', + email: email, + }); + + if (error) { + return `Error: ${error.message}`; + } + + return 'Verification email resent successfully!'; + } catch (err) { + return 'Unexpected error while resending verification email.'; + } +} + +export function getTempEmail(): string | null { + return localStorage.getItem('tempEmail'); +} diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index f600f19..a47b75f 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -44,7 +44,7 @@ export default function SignUp() { setIsError(!success); if (success) { - router.push('/onboarding/role-selection'); + router.push('/verification'); } }; diff --git a/app/(auth)/success/page.tsx b/app/(auth)/success/page.tsx new file mode 100644 index 0000000..adf1e7e --- /dev/null +++ b/app/(auth)/success/page.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import Rose from '@/public/images/rose-greenbg.svg'; +import { + Background, + Image, + InlineContainer, + ReviewContainer, + RoundedCornerButton, + Title, +} from '../../../styles/styles'; + +export default function Success() { + const router = useRouter(); // Initialize useRouter + + const handleContinue = () => { + router.push('/onboarding/general'); // Navigate to the onboarding/general page + }; + + return ( + + Rose + + + Successfully verified! + + Your email has been verified. Please use this email address to login + in the future. + + + Continue + + + + + ); +} diff --git a/app/(auth)/verification/page.tsx b/app/(auth)/verification/page.tsx new file mode 100644 index 0000000..61b4b93 --- /dev/null +++ b/app/(auth)/verification/page.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import supabase from '@/api/supabase/createClient'; +import { + getTempEmail, + resendVerificationEmail, +} from '@/api/supabase/queries/auth'; +import Bud from '@/public/images/bud.svg'; +import EmailIcon from '@/public/images/email.svg'; +import { + Background, + EmailContainer, + EmailIconStyled, + EmailText, + Footer, + Image, + InlineContainer, + Link, + ResendMessage, + ReviewContainer, + RoundedCornerButton, + Title, +} from '../../../styles/styles'; + +export default function Verification() { + const router = useRouter(); // Initialize useRouter + const [tempEmail, setTempEmail] = useState(null); + const [resendStatus, setResendStatus] = useState(''); + const [isError, setIsError] = useState(false); + + useEffect(() => { + // TODO: checking if user is verified every 5 seconds until user is verified or if page is reloaded/closed. + // Not sure if this is the most efficient approach + + // TODO: this still isn't working rn because session is null until sign-in + const checkVerificationStatus = async () => { + const { + data: { session }, + } = await supabase.auth.getSession(); + console.log(session); + + if (session?.user?.user_metadata.email_verified) { + router.push('/success'); + } + }; + + const interval = setInterval(checkVerificationStatus, 5000); + + return () => clearInterval(interval); + }, [router]); + + useEffect(() => { + const email = getTempEmail(); + setTempEmail(email); + }, []); + + const handleResendLink = async () => { + if (tempEmail) { + const message = await resendVerificationEmail(tempEmail); + setIsError(message.includes('Error')); + setResendStatus(message); + } else { + setIsError(true); + } + }; + + const handleUseAnotherAccount = () => { + router.push('/signin'); + localStorage.removeItem('tempEmail'); + }; + + // TODO: Restyle error message on lines 95-100 (the message containing link back to sign-up) + + return ( + + Bud + + + Verification Needed + Thanks for signing up! + + A verification link has been sent to the email you specified, please + check your inbox for next steps. + + + + + + {tempEmail ? tempEmail : 'Email address not found'} + + + + + Use another account + + +
+ Didn't receive it?{' '} + + Resend link + +
+ + {isError && !tempEmail && ( + + Email address not found!{' '} + router.push('/signup')}> + Return to the sign-up page + {' '} + to restart the sign-up process. + + )} + + {resendStatus && tempEmail && ( + {resendStatus} + )} +
+
+
+ ); +} diff --git a/app/onboarding/finalize/page.tsx b/app/onboarding/finalize/page.tsx index 416fefc..41e9c01 100644 --- a/app/onboarding/finalize/page.tsx +++ b/app/onboarding/finalize/page.tsx @@ -3,14 +3,14 @@ import { useRouter } from 'next/navigation'; import Rose from '@/public/images/rose.svg'; import { SMALL } from '@/styles/text'; -import { Background } from '../styles'; import { ContinueButton, Image, InlineContainer, ReviewContainer, Title, -} from './styles'; +} from '../../../styles/styles'; +import { Background } from '../styles'; export default function Onboarding() { const router = useRouter(); // Initialize useRouter diff --git a/public/images/bud.svg b/public/images/bud.svg new file mode 100644 index 0000000..7adb2f4 --- /dev/null +++ b/public/images/bud.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/email.svg b/public/images/email.svg new file mode 100644 index 0000000..f591575 --- /dev/null +++ b/public/images/email.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/rose-greenbg.svg b/public/images/rose-greenbg.svg new file mode 100644 index 0000000..4e20d89 --- /dev/null +++ b/public/images/rose-greenbg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/styles/styles.ts b/styles/styles.ts new file mode 100644 index 0000000..9f7f10c --- /dev/null +++ b/styles/styles.ts @@ -0,0 +1,152 @@ +'use client'; + +import NextImage from 'next/image'; +import styled from 'styled-components'; +import { Sans } from '@/styles/fonts'; +import { SMALL } from '@/styles/text'; +import COLORS from './colors'; + +export const Background = styled.main` + flex-direction: column; + min-width: 100%; + min-height: 100svh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +`; + +export const Title = styled.h1` + font-size: 24px; + text-align: start; + color: ${COLORS.gray11}; + font-weight: 500; +`; + +export const InlineContainer = styled.main` + width: 25%; + flex-direction: column; + margin-top: 2%; + margin-bottom: 2%; + @media (max-width: 1200px) { + width: 30%; + } + @media (max-width: 768px) { + width: 80%; + } +`; + +export const Image = styled(NextImage)` + width: 85px; + height: 85px; + flex-shrink: 0; + position: relative; + top: 60px; + z-index: 1; +`; + +export const Circle = styled.main` + width: 85px; + height: 85px; + flex-shrink: 0; + background-color: #f7c1bd; + border-radius: 100%; + position: relative; + top: 60px; + z-index: 1; +`; + +export const ReviewContainer = styled.main` + position: relative; /* This makes the Circle position relative to this container */ + display: flex; + padding: 60px 32px 32px 32px; + flex-direction: column; + justify-content: center; + align-items: start; + gap: 20px; + border-radius: 16px; + background: var(--bread-1, #fefdfc); + box-shadow: 4px 4px 4px 0px rgba(0, 0, 0, 0.15); +`; + +export const ContinueButton = styled.button` + display: flex; + margin-top: 0.75rem; + padding: 12px 16px; + justify-content: center; + align-items: center; + align-self: stretch; + border-radius: 99999px; + background: ${COLORS.pomegranate}; + border-style: solid; + border-color: ${COLORS.gray12}; + cursor: pointer; +`; + +interface RoundedCornerButtonProps { + width?: string; + bgColor?: string; + textColor?: string; +} + +export const RoundedCornerButton = styled.button` + font-family: ${Sans.style.fontFamily}; + background-color: ${props => props.bgColor || COLORS.pomegranate}; + color: ${props => props.textColor || 'white'}; + font-size: 1rem; + padding: 0.55rem; + border: 1.5px solid black; + border-radius: 8px; + + cursor: pointer; + margin-top: 0.5rem; + width: ${props => props.width || '100%'}; + margin-left: auto; + margin-right: auto; +`; + +export const Footer = styled.div` + font-family: ${Sans.style.fontFamily}; + text-align: center; + margin-top: -0.75rem; + width: 100%; + padding: 0.5rem; +`; + +export const Link = styled.a` + color: ${COLORS.lilac9}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +`; + +export const EmailContainer = styled.div` + display: flex; + align-items: center; + margin-top: -0.9rem; + gap: 8px; +`; + +export const EmailIconStyled = styled(NextImage)` + width: 24px; + height: 24px; + margin-right: -4px; +`; + +export const EmailText = styled.p` + font-size: 1rem; + color: ${COLORS.gray11}; + line-height: 1.5; + display: inline-block; +`; + +export const ResendMessage = styled(SMALL)<{ $isError: boolean }>` + color: ${({ $isError }) => ($isError ? 'red' : 'green')}; + text-align: center; + font-weight: 400; + margin-top: -0.75rem; + font-size: 1rem; + display: block; +`; diff --git a/utils/AuthProvider.tsx b/utils/AuthProvider.tsx index 97549d9..22057a5 100644 --- a/utils/AuthProvider.tsx +++ b/utils/AuthProvider.tsx @@ -48,18 +48,27 @@ export function AuthContextProvider({ setSession(newSession); }; - const signInWithEmail = async (email: string, password: string) => - supabase.auth.signInWithPassword({ + const signInWithEmail = async (email: string, password: string) => { + const response = supabase.auth.signInWithPassword({ email, password, }); // will trigger the use effect to update the session - const signUp = async (email: string, password: string) => - supabase.auth.signUp({ + + return response; + }; + + const signUp = async (email: string, password: string) => { + const response = await supabase.auth.signUp({ email, password, }); // will trigger the use effect to update the session + + return response; + }; + const signOut = () => { supabase.auth.signOut(); + localStorage.removeItem('tempEmail'); setSession(null); }; From fd937c01e968139bdb44f3b917aaecc14a2922d0 Mon Sep 17 00:00:00 2001 From: Rohin Juneja Date: Sat, 7 Dec 2024 12:22:39 -0800 Subject: [PATCH 2/9] [fix] added automatic routing from verification to success page --- api/supabase/queries/auth.ts | 5 ++++- app/(auth)/success/page.tsx | 4 +++- app/(auth)/verification/page.tsx | 27 +++++++-------------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/api/supabase/queries/auth.ts b/api/supabase/queries/auth.ts index f1d42ea..c4683f3 100644 --- a/api/supabase/queries/auth.ts +++ b/api/supabase/queries/auth.ts @@ -6,7 +6,10 @@ export async function handleSignUp( ): Promise<{ success: boolean; message: string }> { try { await ensureLoggedOutForNewUser(email); - const { data, error } = await supabase.auth.signUp({ email, password }); + const { data, error } = await supabase.auth.signUp({ + email, + password, + }); if (error) { return { success: false, message: `Sign-up failed: ${error.message}` }; diff --git a/app/(auth)/success/page.tsx b/app/(auth)/success/page.tsx index adf1e7e..cddae6b 100644 --- a/app/(auth)/success/page.tsx +++ b/app/(auth)/success/page.tsx @@ -1,6 +1,8 @@ 'use client'; +import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import supabase from '@/api/supabase/createClient'; import Rose from '@/public/images/rose-greenbg.svg'; import { Background, @@ -15,7 +17,7 @@ export default function Success() { const router = useRouter(); // Initialize useRouter const handleContinue = () => { - router.push('/onboarding/general'); // Navigate to the onboarding/general page + router.push('/onboarding/role-selection'); // Navigate to the onboarding/general page }; return ( diff --git a/app/(auth)/verification/page.tsx b/app/(auth)/verification/page.tsx index 61b4b93..7216f86 100644 --- a/app/(auth)/verification/page.tsx +++ b/app/(auth)/verification/page.tsx @@ -2,13 +2,14 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import supabase from '@/api/supabase/createClient'; import { getTempEmail, resendVerificationEmail, } from '@/api/supabase/queries/auth'; import Bud from '@/public/images/bud.svg'; import EmailIcon from '@/public/images/email.svg'; +// import supabase from '@/api/supabase/createClient'; +import { useSession } from '@/utils/AuthProvider'; import { Background, EmailContainer, @@ -29,27 +30,13 @@ export default function Verification() { const [tempEmail, setTempEmail] = useState(null); const [resendStatus, setResendStatus] = useState(''); const [isError, setIsError] = useState(false); + const { session } = useSession(); useEffect(() => { - // TODO: checking if user is verified every 5 seconds until user is verified or if page is reloaded/closed. - // Not sure if this is the most efficient approach - - // TODO: this still isn't working rn because session is null until sign-in - const checkVerificationStatus = async () => { - const { - data: { session }, - } = await supabase.auth.getSession(); - console.log(session); - - if (session?.user?.user_metadata.email_verified) { - router.push('/success'); - } - }; - - const interval = setInterval(checkVerificationStatus, 5000); - - return () => clearInterval(interval); - }, [router]); + if (session) { + router.push('/success'); + } + }, [session, router]); useEffect(() => { const email = getTempEmail(); From 4549cc0df75724648a09f5d9b09b36d376dba685 Mon Sep 17 00:00:00 2001 From: Rohin Juneja Date: Sat, 7 Dec 2024 12:23:01 -0800 Subject: [PATCH 3/9] [fix] added automatic routing from verification to success page --- app/(auth)/success/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/(auth)/success/page.tsx b/app/(auth)/success/page.tsx index cddae6b..30210a7 100644 --- a/app/(auth)/success/page.tsx +++ b/app/(auth)/success/page.tsx @@ -1,8 +1,6 @@ 'use client'; -import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import supabase from '@/api/supabase/createClient'; import Rose from '@/public/images/rose-greenbg.svg'; import { Background, From fb0872595722616b877431516de32f3543f79e7e Mon Sep 17 00:00:00 2001 From: Rohin Juneja Date: Sat, 7 Dec 2024 14:27:07 -0800 Subject: [PATCH 4/9] [fix] addressed comments: removed unused imports, reformatted import path directories, and removed local styles from global styles file --- app/(auth)/verification/page.tsx | 22 +++++++------ .../verification/verification-styles.ts | 33 +++++++++++++++++++ styles/styles.ts | 30 ----------------- 3 files changed, 45 insertions(+), 40 deletions(-) create mode 100644 app/(auth)/verification/verification-styles.ts diff --git a/app/(auth)/verification/page.tsx b/app/(auth)/verification/page.tsx index 7216f86..4793ea5 100644 --- a/app/(auth)/verification/page.tsx +++ b/app/(auth)/verification/page.tsx @@ -8,22 +8,24 @@ import { } from '@/api/supabase/queries/auth'; import Bud from '@/public/images/bud.svg'; import EmailIcon from '@/public/images/email.svg'; -// import supabase from '@/api/supabase/createClient'; -import { useSession } from '@/utils/AuthProvider'; import { Background, - EmailContainer, - EmailIconStyled, - EmailText, Footer, Image, InlineContainer, Link, - ResendMessage, ReviewContainer, RoundedCornerButton, Title, -} from '../../../styles/styles'; +} from '@/styles/styles'; +import { P } from '@/styles/text'; +import { useSession } from '@/utils/AuthProvider'; +import { + EmailContainer, + EmailIconStyled, + EmailText, + ResendMessage, +} from './verification-styles'; export default function Verification() { const router = useRouter(); // Initialize useRouter @@ -66,11 +68,11 @@ export default function Verification() { Verification Needed - Thanks for signing up! - +

Thanks for signing up!

+

A verification link has been sent to the email you specified, please check your inbox for next steps. - +

diff --git a/app/(auth)/verification/verification-styles.ts b/app/(auth)/verification/verification-styles.ts new file mode 100644 index 0000000..72c7b67 --- /dev/null +++ b/app/(auth)/verification/verification-styles.ts @@ -0,0 +1,33 @@ +import NextImage from 'next/image'; +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { SMALL } from '@/styles/text'; + +export const EmailContainer = styled.div` + display: flex; + align-items: center; + margin-top: -0.9rem; + gap: 8px; +`; + +export const EmailIconStyled = styled(NextImage)` + width: 24px; + height: 24px; + margin-right: -4px; +`; + +export const EmailText = styled.p` + font-size: 1rem; + color: ${COLORS.gray11}; + line-height: 1.5; + display: inline-block; +`; + +export const ResendMessage = styled(SMALL)<{ $isError: boolean }>` + color: ${({ $isError }) => ($isError ? 'red' : 'green')}; + text-align: center; + font-weight: 400; + margin-top: -0.75rem; + font-size: 1rem; + display: block; +`; diff --git a/styles/styles.ts b/styles/styles.ts index 9f7f10c..18ec141 100644 --- a/styles/styles.ts +++ b/styles/styles.ts @@ -3,7 +3,6 @@ import NextImage from 'next/image'; import styled from 'styled-components'; import { Sans } from '@/styles/fonts'; -import { SMALL } from '@/styles/text'; import COLORS from './colors'; export const Background = styled.main` @@ -121,32 +120,3 @@ export const Link = styled.a` text-decoration: underline; } `; - -export const EmailContainer = styled.div` - display: flex; - align-items: center; - margin-top: -0.9rem; - gap: 8px; -`; - -export const EmailIconStyled = styled(NextImage)` - width: 24px; - height: 24px; - margin-right: -4px; -`; - -export const EmailText = styled.p` - font-size: 1rem; - color: ${COLORS.gray11}; - line-height: 1.5; - display: inline-block; -`; - -export const ResendMessage = styled(SMALL)<{ $isError: boolean }>` - color: ${({ $isError }) => ($isError ? 'red' : 'green')}; - text-align: center; - font-weight: 400; - margin-top: -0.75rem; - font-size: 1rem; - display: block; -`; From 682949c545577b3c77b5f95011cc67f6ce44794c Mon Sep 17 00:00:00 2001 From: aidenm1 <65800707+aidenm1@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:11:36 -0800 Subject: [PATCH 5/9] feat: availability card component and display (#38) * created querying functions * created AvailabilityCard component and added to general page * changed styling and updates colors file to include new pomegranate color * resolved ESLint errors * added message for no availabilities * updated to reflect new design and implemented content moving when menubar is expanded * changed design for date range on availability card --- api/supabase/queries/availability.ts | 40 +++++ app/(auth)/auth-styles.ts | 2 +- app/availability/general/page.tsx | 138 ++++++++++++++++++ app/availability/general/styles.ts | 45 ++++++ app/onboarding/finalize/styles.ts | 7 +- app/onboarding/styles.ts | 4 +- .../AvailabilityCard/AvailabilityCard.tsx | 86 +++++++++++ components/AvailabilityCard/styles.ts | 65 +++++++++ components/MenuBar/MenuBar.tsx | 11 +- components/MenuBar/styles.ts | 5 +- public/images/add.svg | 5 + public/images/additionalinfo.svg | 5 + public/images/clock.svg | 8 + styles/colors.ts | 3 +- types/schema.d.ts | 2 + 15 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 app/availability/general/page.tsx create mode 100644 app/availability/general/styles.ts create mode 100644 components/AvailabilityCard/AvailabilityCard.tsx create mode 100644 components/AvailabilityCard/styles.ts create mode 100644 public/images/add.svg create mode 100644 public/images/additionalinfo.svg create mode 100644 public/images/clock.svg diff --git a/api/supabase/queries/availability.ts b/api/supabase/queries/availability.ts index 4d57d5d..9ac5a78 100644 --- a/api/supabase/queries/availability.ts +++ b/api/supabase/queries/availability.ts @@ -23,3 +23,43 @@ export async function fetchAvailabilitiesByFacilityId(facility_id: UUID) { return null; // Return null on unexpected error } } + +export async function fetchAllAvailabilities() { + try { + const { data, error } = await supabase.from('availabilities').select('*'); + if (error) { + throw new Error(error.message); + } + if (data && data.length == 0) { + console.log('No availabilities found'); + return []; + } + return data; + } catch (error) { + console.error('An unexpected error occured:', error); + return null; + } +} + +export async function fetchDatesByAvailabilityID(availability_id: UUID) { + try { + const { data, error } = await supabase + .from('available_dates') + .select('*') + .eq('availability_id', availability_id); + if (error) { + throw new Error(error.message); + } + if (data && data.length == 0) { + console.log( + 'No dates found for this availability id or availability_id is undefined', + availability_id, + ); + return []; + } + return data; + } catch (error) { + console.error('An unexpected error occured:', error); + return null; + } +} diff --git a/app/(auth)/auth-styles.ts b/app/(auth)/auth-styles.ts index 617990e..b10782a 100644 --- a/app/(auth)/auth-styles.ts +++ b/app/(auth)/auth-styles.ts @@ -79,7 +79,7 @@ export const Input = styled.input` export const Button = styled.button` font-family: ${Sans.style.fontFamily}; - background-color: ${COLORS.pomegranate}; + background-color: ${COLORS.pomegranate12}; color: white; font-size: 1rem; padding: 0.75rem; diff --git a/app/availability/general/page.tsx b/app/availability/general/page.tsx new file mode 100644 index 0000000..0c65c42 --- /dev/null +++ b/app/availability/general/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + fetchAllAvailabilities, + fetchDatesByAvailabilityID, +} from '@/api/supabase/queries/availability'; +import AvailabilityCard from '@/components/AvailabilityCard/AvailabilityCard'; +import MenuBar from '@/components/MenuBar/MenuBar'; +import Add from '@/public/images/add.svg'; +import COLORS from '@/styles/colors'; +import { H3 } from '@/styles/text'; +import { Availabilities, AvailableDates } from '@/types/schema'; +import * as styles from './styles'; + +type AvailabilitiesByYear = { + [year: string]: { + availability: Availabilities; + available_dates: AvailableDates[]; + }[]; +}; + +export default function AvailabilityPage() { + const [groupedByYear, setGroupedByYear] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [menuExpanded, setMenuExpanded] = useState(false); // Track the expanded state of the menu + + useEffect(() => { + async function fetchAndGroupData() { + try { + // Step 1: Fetch all availabilities + const availabilities = await fetchAllAvailabilities(); + + if (!availabilities) { + return; + } + + // Step 2: Group by year while fetching associated dates + const grouped: AvailabilitiesByYear = {}; + for (const availability of availabilities) { + const availableDates = await fetchDatesByAvailabilityID( + availability.availability_id, + ); + + const year = availableDates?.[0]?.available_date + ? new Date(availableDates[0].available_date) + .getFullYear() + .toString() + : null; + + //don't display availability cards that have no availabilities associated + if (year == null) { + continue; + } + + if (!grouped[year]) { + grouped[year] = []; + } + + grouped[year].push({ + availability, + available_dates: availableDates ?? [], + }); + } + for (const year in grouped) { + grouped[year].sort((a, b) => { + const firstDateA = new Date( + a.available_dates[0]?.available_date ?? 0, + ).getTime(); + const firstDateB = new Date( + b.available_dates[0]?.available_date ?? 0, + ).getTime(); + return firstDateA - firstDateB; + }); + } + + setGroupedByYear(grouped); + setIsLoading(false); // Stop loading after data fetch + } catch (error) { + console.error('Error fetching data:', error); + } + } + + fetchAndGroupData(); + }, []); + + return ( +
+ {' '} + {/* Pass function to update state */} + + + +

+ Availabilities +

+ +
+ {/* Check if there are no availabilities */} + {isLoading ? ( + + Loading availabilities... + + ) : Object.keys(groupedByYear).length === 0 ? ( + + No availabilities yet, +
+ add one with the plus sign! +
+ ) : ( + Object.entries(groupedByYear).map(([year, availabilities]) => ( +
+ + {year} + + {availabilities.map(({ availability, available_dates }) => ( + + ))} +
+ )) + )} +
+
+
+ ); +} diff --git a/app/availability/general/styles.ts b/app/availability/general/styles.ts new file mode 100644 index 0000000..e219b00 --- /dev/null +++ b/app/availability/general/styles.ts @@ -0,0 +1,45 @@ +'use client'; + +import NextImage from 'next/image'; +import styled from 'styled-components'; +import { H6, P } from '@/styles/text'; + +export const Page = styled.main<{ $menuExpanded: boolean }>` + display: flex; + min-width: 100%; + min-height: 100vh; + justify-content: center; + overflow: hidden; + margin-left: ${({ $menuExpanded }) => + $menuExpanded ? '10%' : '0'}; /* Fixed margin for the expanded menu */ + transition: margin-left 0.3s ease; /* Smooth transition */ +`; + +export const AllAvailabilitiesHolder = styled.main` + display: flex; + width: 28.75%; + flex-direction: column; +`; + +export const TitleContainer = styled.main` + display: flex; + margin-top: 4.43rem; + margin-bottom: 2.62rem; + justify-content: space-between; + align-items: center; + width: 100%; +`; + +export const YearText = styled(H6)` + margin-bottom: 1.5rem; +`; + +export const AddImage = styled(NextImage)` + width: 1.5rem; + height: 1.5rem; +`; + +export const message = styled(P)` + text-align: center; + margin-top: 5.75rem; +`; diff --git a/app/onboarding/finalize/styles.ts b/app/onboarding/finalize/styles.ts index 759e7ad..34635a6 100644 --- a/app/onboarding/finalize/styles.ts +++ b/app/onboarding/finalize/styles.ts @@ -64,8 +64,13 @@ export const ContinueButton = styled.button` justify-content: center; align-items: center; align-self: stretch; +<<<<<<< HEAD:app/onboarding/yay/styles.ts + border-radius: 99999px; + background: ${COLORS.pomegranate12}; +======= border-radius: 8px; - background: ${COLORS.pomegranate}; + background: ${COLORS.pomegranate12}; +>>>>>>> c33d5a0fb4596e1d2e004014b99e33e17cee93e9:app/onboarding/finalize/styles.ts border-style: solid; border-color: ${COLORS.gray12}; cursor: pointer; diff --git a/app/onboarding/styles.ts b/app/onboarding/styles.ts index 2eefc8b..659830e 100644 --- a/app/onboarding/styles.ts +++ b/app/onboarding/styles.ts @@ -150,9 +150,9 @@ export const Button = styled.button<{ disabled?: boolean }>` width: 30%; height: 2.75rem; background-color: ${({ disabled }) => - disabled ? COLORS.pomegranate10 : COLORS.pomegranate}; + disabled ? COLORS.pomegranate10 : COLORS.pomegranate12}; border-color: ${({ disabled }) => - disabled ? COLORS.pomegranate10 : COLORS.pomegranate}; + disabled ? COLORS.pomegranate10 : COLORS.pomegranate12}; border-style: solid; border-radius: 8px; display: inline-flex; diff --git a/components/AvailabilityCard/AvailabilityCard.tsx b/components/AvailabilityCard/AvailabilityCard.tsx new file mode 100644 index 0000000..c13da6c --- /dev/null +++ b/components/AvailabilityCard/AvailabilityCard.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import Arrow from '@/public/images/white_back.svg'; +import COLORS from '@/styles/colors'; +import { H5, P } from '@/styles/text'; +import { Availabilities, AvailableDates } from '@/types/schema'; +import * as styles from './styles'; + +interface AvailabilityCardProps { + availability: Availabilities; + availableDates: AvailableDates[]; +} + +export default function AvailabilityCard({ + availability, + availableDates, +}: AvailabilityCardProps) { + const dateRange = + availableDates.length > 0 + ? { + earliest: new Date( + Math.min( + ...availableDates.map(date => + new Date(date.available_date).getTime(), + ), + ), + ), + latest: new Date( + Math.max( + ...availableDates.map(date => + new Date(date.available_date).getTime(), + ), + ), + ), + } + : null; + + // Format the dates + let formattedRange = dateRange + ? `${dateRange.earliest.toLocaleString('default', { + month: 'short', + day: 'numeric', + })} - ${dateRange.latest.toLocaleString('default', { + month: 'short', + day: 'numeric', + })}` + : 'No dates available'; + + if (dateRange && dateRange.earliest.getDate() == dateRange.latest.getDate()) { + formattedRange = `${dateRange.earliest.toLocaleString('default', { + month: 'short', + day: 'numeric', + })}`; + } + + return ( + + + +
+ {availability.name} +
+

+ {formattedRange} +

+
+ +
+ +
+ +

+ Description +

+ + {availability.additional_info} + +
+
+
+
+ ); +} diff --git a/components/AvailabilityCard/styles.ts b/components/AvailabilityCard/styles.ts new file mode 100644 index 0000000..be08d1f --- /dev/null +++ b/components/AvailabilityCard/styles.ts @@ -0,0 +1,65 @@ +'use client'; + +import NextImage from 'next/image'; +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { SMALL } from '@/styles/text'; + +export const AvailabilityContainer = styled.main` + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 1.5rem; + border-radius: 16px; + width: 100%; + background: ${COLORS.bread1}; + margin-bottom: 3rem; + box-shadow: 0px 6px 15px -2px rgba(0, 0, 0, 0.08); +`; + +export const AvailabilityHeader = styled.main` + display: flex; + padding: 16px 24px; + justify-content: space-between; + align-items: center; + background: ${COLORS.pomegranate11}; + border-radius: 16px 16px 0 0; + width: 100%; +`; + +export const AvailabilityTitle = styled.main` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +`; + +export const Content = styled.main` + padding: 0px 24px 24px 24px; + display: flex; + flex-direction: column; + gap: 1.5rem; +`; + +export const SubHeader = styled.main` + display: flex; + flex-direction: column; + gap: 4px; + justify-content: start; + margin-bottom: 0.25rem; +`; + +export const Arrow = styled(NextImage)` + width: 24px; + height: 24px; + transform: rotate(180deg); +`; + +export const TruncatedText = styled(SMALL)` + display: -webkit-box; + -webkit-line-clamp: 2; /* Limit to 2 lines */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; +`; diff --git a/components/MenuBar/MenuBar.tsx b/components/MenuBar/MenuBar.tsx index 44813d0..aad6b6e 100644 --- a/components/MenuBar/MenuBar.tsx +++ b/components/MenuBar/MenuBar.tsx @@ -12,14 +12,19 @@ import { ToggleButton, } from './styles'; -const MenuBar: React.FC = () => { +const MenuBar: React.FC<{ setMenuExpanded?: (expanded: boolean) => void }> = ({ + setMenuExpanded = () => {}, // Default to a no-op function +}) => { const [expanded, setExpanded] = useState(false); const [activeItem, setActiveItem] = useState(null); const router = useRouter(); - const toggleMenu = () => setExpanded(!expanded); + const toggleMenu = () => { + const newExpanded = !expanded; + setExpanded(newExpanded); + setMenuExpanded(newExpanded); // Update parent component about expanded state + }; - // TODO: add navigation by passing in path prop const handleClick = (item: string, path: string) => { setActiveItem(item); router.push(path); diff --git a/components/MenuBar/styles.ts b/components/MenuBar/styles.ts index cf8e665..173e705 100644 --- a/components/MenuBar/styles.ts +++ b/components/MenuBar/styles.ts @@ -8,7 +8,7 @@ export const MenuContainer = styled.div<{ $expanded: boolean }>` height: 100vh; z-index: 9999; background-color: ${({ $expanded }) => - $expanded ? COLORS.pomegranate : 'transparent'}; + $expanded ? COLORS.pomegranate12 : 'transparent'}; display: flex; flex-direction: column; padding-left: 1.25rem; @@ -47,7 +47,8 @@ export const MenuIconWrapper = styled.div<{ $expanded: boolean }>` width: 20px; height: 20px; & svg path { - fill: ${({ $expanded }) => ($expanded ? COLORS.gray3 : COLORS.pomegranate)}; + fill: ${({ $expanded }) => + $expanded ? COLORS.gray3 : COLORS.pomegranate12}; } `; diff --git a/public/images/add.svg b/public/images/add.svg new file mode 100644 index 0000000..8aa80d3 --- /dev/null +++ b/public/images/add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/additionalinfo.svg b/public/images/additionalinfo.svg new file mode 100644 index 0000000..0811a52 --- /dev/null +++ b/public/images/additionalinfo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/clock.svg b/public/images/clock.svg new file mode 100644 index 0000000..e911704 --- /dev/null +++ b/public/images/clock.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/styles/colors.ts b/styles/colors.ts index 1f35755..0f9b245 100644 --- a/styles/colors.ts +++ b/styles/colors.ts @@ -51,8 +51,9 @@ const COLORS = { lilac11: '#423EAF', lilac12: '#383975', - pomegranate: '#342A2F', pomegranate10: '#6F585E', + pomegranate11: '#633A4F', + pomegranate12: '#342A2F', }; export default COLORS; diff --git a/types/schema.d.ts b/types/schema.d.ts index 6e6c04e..533adb6 100644 --- a/types/schema.d.ts +++ b/types/schema.d.ts @@ -76,6 +76,8 @@ export interface AvailableDates { date_id: UUID; availability_id: UUID; available_date: string; //date + test_col: string; //timestamptz + test_col2: string; //timestamptz } export interface Event { From 8a8e48ee08bba5b7c57d6b8ed4ce1079363c9f40 Mon Sep 17 00:00:00 2001 From: Celine Ji-Won Choi <107969914+celinechoiii@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:13:31 -0800 Subject: [PATCH 6/9] fix: update continue button style --- app/onboarding/finalize/styles.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/onboarding/finalize/styles.ts b/app/onboarding/finalize/styles.ts index 34635a6..f0678ad 100644 --- a/app/onboarding/finalize/styles.ts +++ b/app/onboarding/finalize/styles.ts @@ -64,13 +64,8 @@ export const ContinueButton = styled.button` justify-content: center; align-items: center; align-self: stretch; -<<<<<<< HEAD:app/onboarding/yay/styles.ts - border-radius: 99999px; - background: ${COLORS.pomegranate12}; -======= border-radius: 8px; background: ${COLORS.pomegranate12}; ->>>>>>> c33d5a0fb4596e1d2e004014b99e33e17cee93e9:app/onboarding/finalize/styles.ts border-style: solid; border-color: ${COLORS.gray12}; cursor: pointer; From 68cd5eee3723b8ac6979c51df62e91b1a8d9447c Mon Sep 17 00:00:00 2001 From: Rohin Juneja Date: Sat, 7 Dec 2024 14:32:58 -0800 Subject: [PATCH 7/9] [fix] addressed comments: removed unused imports, reformatted import path directories, and removed local styles from global styles file --- styles/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/styles.ts b/styles/styles.ts index 18ec141..82399b3 100644 --- a/styles/styles.ts +++ b/styles/styles.ts @@ -76,7 +76,7 @@ export const ContinueButton = styled.button` align-items: center; align-self: stretch; border-radius: 99999px; - background: ${COLORS.pomegranate}; + background: ${COLORS.pomegranate12}; border-style: solid; border-color: ${COLORS.gray12}; cursor: pointer; @@ -90,7 +90,7 @@ interface RoundedCornerButtonProps { export const RoundedCornerButton = styled.button` font-family: ${Sans.style.fontFamily}; - background-color: ${props => props.bgColor || COLORS.pomegranate}; + background-color: ${props => props.bgColor || COLORS.pomegranate12}; color: ${props => props.textColor || 'white'}; font-size: 1rem; padding: 0.55rem; From 9f5e3a7bcbf6c57feb046495029c10a607c96fee Mon Sep 17 00:00:00 2001 From: Celine Choi Date: Mon, 30 Dec 2024 02:47:54 -0800 Subject: [PATCH 8/9] fix: redirection fixes and separate inserting user flow --- api/supabase/queries/auth.ts | 63 ++++++++++++++++++-------------- app/(auth)/verification/page.tsx | 55 +++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 37 deletions(-) diff --git a/api/supabase/queries/auth.ts b/api/supabase/queries/auth.ts index c4683f3..ba351f6 100644 --- a/api/supabase/queries/auth.ts +++ b/api/supabase/queries/auth.ts @@ -6,41 +6,18 @@ export async function handleSignUp( ): Promise<{ success: boolean; message: string }> { try { await ensureLoggedOutForNewUser(email); - const { data, error } = await supabase.auth.signUp({ + const { error } = await supabase.auth.signUp({ email, password, + options: { + emailRedirectTo: 'http://localhost:3000/verification', + }, }); if (error) { return { success: false, message: `Sign-up failed: ${error.message}` }; } - const user = data.user; - if (!user) { - return { - success: false, - message: 'Sign-up failed: User was not created.', - }; - } - - const { error: insertError } = await supabase.from('volunteers').insert([ - { - user_id: user.id, - email, - first_name: '', - last_name: '', - phone_number: '', - notifications_opt_in: true, // default value - }, - ]); - - if (insertError) { - return { - success: false, - message: `Error storing user data: ${insertError.message}`, - }; - } - localStorage.setItem('tempEmail', email); return { success: true, message: 'Sign-up successful!' }; @@ -55,6 +32,38 @@ export async function handleSignUp( } } +export const insertVolunteer = async (user: { id: string; email: string }) => { + if (!user) { + return { + success: false, + message: 'User data is missing. Cannot insert into volunteers table.', + }; + } + + const { error: insertError } = await supabase.from('volunteers').insert([ + { + user_id: user.id, + email: user.email, + first_name: '', + last_name: '', + phone_number: '', + notifications_opt_in: true, + }, + ]); + + if (insertError) { + return { + success: false, + message: `Error storing user data: ${insertError.message}`, + }; + } + + return { + success: true, + message: 'User successfully added to volunteers table.', + }; +}; + export async function handleSignIn( email: string, password: string, diff --git a/app/(auth)/verification/page.tsx b/app/(auth)/verification/page.tsx index 4793ea5..f59d35e 100644 --- a/app/(auth)/verification/page.tsx +++ b/app/(auth)/verification/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { getTempEmail, + insertVolunteer, resendVerificationEmail, } from '@/api/supabase/queries/auth'; import Bud from '@/public/images/bud.svg'; @@ -28,23 +29,59 @@ import { } from './verification-styles'; export default function Verification() { - const router = useRouter(); // Initialize useRouter + const router = useRouter(); + const { session } = useSession(); const [tempEmail, setTempEmail] = useState(null); const [resendStatus, setResendStatus] = useState(''); const [isError, setIsError] = useState(false); - const { session } = useSession(); - - useEffect(() => { - if (session) { - router.push('/success'); - } - }, [session, router]); + const [isEmailConfirmed, setIsEmailConfirmed] = useState(false); useEffect(() => { const email = getTempEmail(); setTempEmail(email); }, []); + // Using session to check if the email is confirmed + useEffect(() => { + if (session?.user) { + const isVerified = session.user.email_confirmed_at !== null; + setIsEmailConfirmed(isVerified); + } + }, [session]); + + useEffect(() => { + if (isEmailConfirmed && session?.user) { + const addUserToVolunteers = async () => { + const email = session.user.email; + + if (!email) { + setIsError(true); + setResendStatus('Email is undefined. Please try again.'); + return; + } + + try { + const result = await insertVolunteer({ + id: session.user.id, + email, + }); + if (result.success) { + router.push('/success'); + } else { + setIsError(true); + setResendStatus(result.message); + } + } catch (error) { + console.error('Error adding user to volunteers:', error); + setIsError(true); + setResendStatus('An error occurred while processing your request.'); + } + }; + + addUserToVolunteers(); + } + }, [isEmailConfirmed, session, router]); + const handleResendLink = async () => { if (tempEmail) { const message = await resendVerificationEmail(tempEmail); @@ -60,8 +97,6 @@ export default function Verification() { localStorage.removeItem('tempEmail'); }; - // TODO: Restyle error message on lines 95-100 (the message containing link back to sign-up) - return ( Bud From 69852fdbc511f5e52b81d15284fb3732a6f88cb3 Mon Sep 17 00:00:00 2001 From: Celine Choi Date: Mon, 30 Dec 2024 02:51:39 -0800 Subject: [PATCH 9/9] fix: linting issues --- app/availability/general/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/availability/general/styles.ts b/app/availability/general/styles.ts index c348dff..687bcdd 100644 --- a/app/availability/general/styles.ts +++ b/app/availability/general/styles.ts @@ -22,7 +22,7 @@ export const AllAvailabilitiesHolder = styled.main` display: flex; width: 28.75%; flex-direction: column; - + @media (max-width: 900px) { width: 80%; }