diff --git a/.env.local.example b/.env.local.example index 400206e7..5fa4c60e 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,8 +1,13 @@ # Clerk. See https://clerk.com NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= +# DO NOT modify +NEXT_PUBLIC_CLERK_SIGN_IN_URL=/signin +NEXT_PUBLIC_CLERK_SIGN_UP_URL=/join +NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ +NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ -# Database. Do not modify in development. +# Database (DO NOT modify in development) DATABASE_URL=file:dev.sqlite DATABASE_AUTH_TOKEN= diff --git a/src/app/(account)/forgot-password/ForgotPassword.tsx b/src/app/(account)/forgot-password/ForgotPassword.tsx new file mode 100644 index 00000000..8e4c0800 --- /dev/null +++ b/src/app/(account)/forgot-password/ForgotPassword.tsx @@ -0,0 +1,186 @@ +'use client'; + +import Button from '@/components/Button'; +import ControlledField from '@/components/ControlledField'; +import FancyRectangle from '@/components/FancyRectangle'; +import { useSignIn } from '@clerk/nextjs'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { handleClerkErrors } from '../helpers'; +import { codeSchema, emailSchema, passwordSchema } from '../schemas'; + +const sendCodeSchema = z.object({ + email: emailSchema, +}); +const resetPasswordSchema = z + .object({ + code: codeSchema, + password: passwordSchema, + confirmPassword: z.string().min(1, { message: 'Please confirm password' }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); + +const STEP_INSTRUCTIONS = [ + '', // Step start from 1 + 'Enter your email to receive a reset code.', + 'Enter your new password and the code received in your email.', + 'Password reset complete!', +] as const; + +export default function ForgotPassword() { + const [step, setStep] = useState(1); + const { isLoaded, signIn, setActive } = useSignIn(); + + const sendCodeForm = useForm>({ + defaultValues: { email: '' }, + resolver: zodResolver(sendCodeSchema), + }); + const resetPasswordForm = useForm>({ + defaultValues: { code: '', password: '' }, + resolver: zodResolver(resetPasswordSchema), + }); + + const [sendCodeLoading, setSendCodeLoading] = useState(false); + const [resetPasswordLoading, setResetPasswordLoading] = useState(false); + + const handleSendCode = sendCodeForm.handleSubmit(async ({ email }) => { + if (!isLoaded) return; + + setSendCodeLoading(true); + + try { + const result = await signIn.create({ + strategy: 'reset_password_email_code', + identifier: email, + }); + if (result) { + setStep(2); + } + } catch (error) { + handleClerkErrors(error, sendCodeForm, [ + { + code: 'form_identifier_not_found', + field: 'email', + message: + "Couldn't find account with given email. Please create an account first.", + }, + { + code: 'form_conditional_param_value_disallowed', + field: 'email', + message: 'Account was created through Google. Please sign in using Google.', + }, + ]); + } + + setSendCodeLoading(false); + }); + + const handleResetPassword = resetPasswordForm.handleSubmit(async ({ code, password }) => { + if (!isLoaded) return; + + setResetPasswordLoading(true); + + try { + const resetResult = await signIn.attemptFirstFactor({ + strategy: 'reset_password_email_code', + code, + password, + }); + if (resetResult.status === 'complete') { + setActive({ session: resetResult.createdSessionId }); + setStep(3); + } + } catch (error) { + handleClerkErrors(error, resetPasswordForm, [ + { + code: 'form_password_not_strong_enough', + field: 'password', + message: + 'Given password is not strong enough. For account safety, please use a different password.', + }, + { + code: 'form_password_not_strong_enough', + field: 'password', + message: + 'Password has been found in an online data breach. For account safety, please use a different password.', + }, + { + code: 'form_code_incorrect', + field: 'code', + message: 'Incorrect Code. Please enter the code from your email.', + }, + ]); + } + + setResetPasswordLoading(false); + }); + + return ( +
+ +
+

Forgot Your Password?

+

{STEP_INSTRUCTIONS[step]}

+ {step === 1 && ( +
+ + + + )} + + {step === 2 && ( +
+ + + + + + )} + + {step === 3 && ( + + )} +
+
+
+ ); +} diff --git a/src/app/(account)/forgot-password/page.tsx b/src/app/(account)/forgot-password/page.tsx index 203968ea..1a52a001 100644 --- a/src/app/(account)/forgot-password/page.tsx +++ b/src/app/(account)/forgot-password/page.tsx @@ -1,191 +1,10 @@ -'use client'; - -import Button from '@/components/Button'; -import ControlledField from '@/components/ControlledField'; -import FancyRectangle from '@/components/FancyRectangle'; -import { useSignIn } from '@clerk/nextjs'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { handleClerkErrors } from '../helpers'; -import { codeSchema, emailSchema, passwordSchema } from '../schemas'; - -// export const metadata: Metadata = { -// title: 'Forgot Password', -// robots: { index: false, follow: false }, -// }; - -const sendCodeSchema = z.object({ - email: emailSchema, -}); -const resetPasswordSchema = z - .object({ - code: codeSchema, - password: passwordSchema, - confirmPassword: z.string().min(1, { message: 'Please confirm password' }), - }) - .refine((data) => data.password === data.confirmPassword, { - message: 'Passwords do not match', - path: ['confirmPassword'], - }); - -const STEP_INSTRUCTIONS = [ - '', // Step start from 1 - 'Enter your email to receive a reset code.', - 'Enter your new password and the code received in your email.', - 'Password reset complete!', -] as const; +import type { Metadata } from 'next'; +import ForgotPassword from './ForgotPassword'; +export const metadata: Metadata = { + title: 'Forgot Password', + robots: { index: false, follow: false }, +}; export default function ForgotPasswordPage() { - const [step, setStep] = useState(1); - const { isLoaded, signIn, setActive } = useSignIn(); - - const sendCodeForm = useForm>({ - defaultValues: { email: '' }, - resolver: zodResolver(sendCodeSchema), - }); - const resetPasswordForm = useForm>({ - defaultValues: { code: '', password: '' }, - resolver: zodResolver(resetPasswordSchema), - }); - - const [sendCodeLoading, setSendCodeLoading] = useState(false); - const [resetPasswordLoading, setResetPasswordLoading] = useState(false); - - const handleSendCode = sendCodeForm.handleSubmit(async ({ email }) => { - if (!isLoaded) return; - - setSendCodeLoading(true); - - try { - const result = await signIn.create({ - strategy: 'reset_password_email_code', - identifier: email, - }); - if (result) { - setStep(2); - } - } catch (error) { - handleClerkErrors(error, sendCodeForm, [ - { - code: 'form_identifier_not_found', - field: 'email', - message: - "Couldn't find account with given email. Please create an account first.", - }, - { - code: 'form_conditional_param_value_disallowed', - field: 'email', - message: 'Account was created through Google. Please sign in using Google.', - }, - ]); - } - - setSendCodeLoading(false); - }); - - const handleResetPassword = resetPasswordForm.handleSubmit(async ({ code, password }) => { - if (!isLoaded) return; - - setResetPasswordLoading(true); - - try { - const resetResult = await signIn.attemptFirstFactor({ - strategy: 'reset_password_email_code', - code, - password, - }); - if (resetResult.status === 'complete') { - setActive({ session: resetResult.createdSessionId }); - setStep(3); - } - } catch (error) { - handleClerkErrors(error, resetPasswordForm, [ - { - code: 'form_password_not_strong_enough', - field: 'password', - message: - 'Given password is not strong enough. For account safety, please use a different password.', - }, - { - code: 'form_password_not_strong_enough', - field: 'password', - message: - 'Password has been found in an online data breach. For account safety, please use a different password.', - }, - { - code: 'form_code_incorrect', - field: 'code', - message: 'Incorrect Code. Please enter the code from your email.', - }, - ]); - } - - setResetPasswordLoading(false); - }); - - return ( -
- -
-

Forgot Your Password?

-

{STEP_INSTRUCTIONS[step]}

- {step === 1 && ( -
- - - - )} - - {step === 2 && ( -
- - - - - - )} - - {step === 3 && ( - - )} -
-
-
- ); + return ; } diff --git a/src/app/(account)/join/Join.tsx b/src/app/(account)/join/Join.tsx new file mode 100644 index 00000000..e1114c03 --- /dev/null +++ b/src/app/(account)/join/Join.tsx @@ -0,0 +1,73 @@ +'use client'; + +import FancyRectangle from '@/components/FancyRectangle'; +import Title from '@/components/Title'; +import { SignedIn, SignedOut, useUser } from '@clerk/nextjs'; +import Link from 'next/link'; +import { useEffect } from 'react'; +import ProgressBar from './ProgressBar'; +import StepFour from './steps/StepFour'; +import StepOne from './steps/StepOne'; +import StepThree from './steps/StepThree'; +import StepTwo from './steps/StepTwo'; +import { useJoinUsHeading, useJoinUsStep } from './store'; + +export default function Join() { + const { step, setStep } = useJoinUsStep(); + const { heading } = useJoinUsHeading(); + + const { isSignedIn } = useUser(); + useEffect(() => { + if (isSignedIn) { + setStep(2); + } + }, [isSignedIn]); + + return ( +
+ Join Us +
+
+

New Members are

+
+

Always Welcome

+
+
+
+

+ Membership costs $10 for the full year. + You can pay for membership online here at our website. Alternatively, you + can pay at a club event or contact one of the{' '} + + committee members + + . +

+

+ Create an account below to start the + registration process. +

+
+
+
+ +
+

{heading.title}

+

{heading.description}

+ + + + + + + { + // eslint-disable-next-line react/jsx-key + [, , ][step - 2] + } + +
+
+
+
+ ); +} diff --git a/src/app/(account)/join/page.tsx b/src/app/(account)/join/page.tsx index 6147cb90..cfeb0c00 100644 --- a/src/app/(account)/join/page.tsx +++ b/src/app/(account)/join/page.tsx @@ -1,77 +1,20 @@ -'use client'; +import { checkUserExists } from '@/server/check-user-exists'; +import { currentUser } from '@clerk/nextjs'; +import type { Metadata } from 'next'; +import { redirect } from 'next/navigation'; +import Join from './Join'; -import FancyRectangle from '@/components/FancyRectangle'; -import Title from '@/components/Title'; -import { SignedIn, SignedOut, useUser } from '@clerk/nextjs'; -import Link from 'next/link'; -import { useEffect } from 'react'; -import ProgressBar from './ProgressBar'; -import StepFour from './steps/StepFour'; -import StepOne from './steps/StepOne'; -import StepThree from './steps/StepThree'; -import StepTwo from './steps/StepTwo'; -import { useJoinUsHeading, useJoinUsStep } from './store'; +export const metadata: Metadata = { + title: 'Join', +}; -// export const metadata: Metadata = { -// title: 'Join', -// }; - -export default function JoinPage() { - const { step, setStep } = useJoinUsStep(); - const { heading } = useJoinUsHeading(); - - const { isSignedIn } = useUser(); - useEffect(() => { - if (isSignedIn) { - setStep(2); +export default async function JoinPage() { + const user = await currentUser(); + if (user) { + const userExists = await checkUserExists(user.id); + if (userExists) { + redirect('/settings'); } - }, [isSignedIn]); - - return ( -
- Join Us -
-
-

New Members are

-
-

Always Welcome

-
-
-
-

- Membership costs $10 for the full year. - You can pay for membership online here at our website. Alternatively, you - can pay at a club event or contact one of the{' '} - - committee members - - . -

-

- Create an account below to start the - registration process. -

-
-
-
- -
-

{heading.title}

-

{heading.description}

- - - - - - - { - // eslint-disable-next-line react/jsx-key - [, , ][step - 2] - } - -
-
-
-
- ); + } + return ; } diff --git a/src/app/(account)/join/steps/StepFour.tsx b/src/app/(account)/join/steps/StepFour.tsx index a32dc9ff..4441da7e 100644 --- a/src/app/(account)/join/steps/StepFour.tsx +++ b/src/app/(account)/join/steps/StepFour.tsx @@ -53,11 +53,17 @@ export default function StepFour() { type="checkbox" /> -
- -
diff --git a/src/app/(account)/join/steps/StepOne.tsx b/src/app/(account)/join/steps/StepOne.tsx index d4a710fd..a2d8edba 100644 --- a/src/app/(account)/join/steps/StepOne.tsx +++ b/src/app/(account)/join/steps/StepOne.tsx @@ -153,14 +153,15 @@ export default function StepOne() { return (
-
-
+

or

-
+
@@ -173,11 +174,12 @@ export default function StepOne() { control={form.control} name="password" /> -
+
-
diff --git a/src/app/(account)/join/steps/StepTwo.tsx b/src/app/(account)/join/steps/StepTwo.tsx index 7e9e7de2..8331bca8 100644 --- a/src/app/(account)/join/steps/StepTwo.tsx +++ b/src/app/(account)/join/steps/StepTwo.tsx @@ -70,11 +70,9 @@ export default function StepTwo() { {form.watch('studentStatus') === 'At The University of Adelaide' && ( )} -
- -
+ ); } diff --git a/src/app/(account)/settings/Settings.tsx b/src/app/(account)/settings/Settings.tsx index ee14e335..8282fce5 100644 --- a/src/app/(account)/settings/Settings.tsx +++ b/src/app/(account)/settings/Settings.tsx @@ -26,7 +26,7 @@ export default function Settings({ settingData }: { settingData: SettingData }) const { refresh } = useRouter(); return ( - <> +
{ @@ -34,9 +34,9 @@ export default function Settings({ settingData }: { settingData: SettingData }) refresh(); }} /> -
+
- +
); } diff --git a/src/app/(account)/settings/Sidebar.tsx b/src/app/(account)/settings/Sidebar.tsx index ed1ba8f6..2b1ce4ea 100644 --- a/src/app/(account)/settings/Sidebar.tsx +++ b/src/app/(account)/settings/Sidebar.tsx @@ -10,7 +10,7 @@ function SidebarTab({ tabName, currentTab, onTabChange }: SidebarTabProps) { const selected = currentTab === tabName; return ( + +
+
+

or

+
+
+ + + + + Forgot password? + + + + + {/* Sign-up option */} +
+

+ Don't have an account yet?{' '} + + Join Us + +

+
+
+ + + + ); +} diff --git a/src/app/(account)/signin/page.tsx b/src/app/(account)/signin/page.tsx index 2540ef35..7db5f167 100644 --- a/src/app/(account)/signin/page.tsx +++ b/src/app/(account)/signin/page.tsx @@ -1,151 +1,14 @@ -'use client'; - -import Button from '@/components/Button'; -import ControlledField from '@/components/ControlledField'; -import FancyRectangle from '@/components/FancyRectangle'; -import { useSignIn } from '@clerk/clerk-react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { FcGoogle } from 'react-icons/fc'; -import { z } from 'zod'; -import { handleClerkErrors } from '../helpers'; -import { emailSchema } from '../schemas'; - -// export const metadata: Metadata = { -// title: 'Sign In', -// }; - -const signInSchema = z.object({ - email: emailSchema, - password: z.string().min(1, { message: 'Please enter your password' }), -}); - -export default function SignInPage() { - const { isLoaded, signIn, setActive } = useSignIn(); - - const form = useForm>({ - defaultValues: { email: '', password: '' }, - resolver: zodResolver(signInSchema), - }); - - const [signInLoading, setSignInLoading] = useState(false); - - const router = useRouter(); - const handleSignIn = form.handleSubmit(async ({ email, password }) => { - if (!isLoaded) return; - - setSignInLoading(true); - - try { - const result = await signIn.create({ - identifier: email, - password, - }); - - if (result.status === 'complete') { - await setActive({ session: result.createdSessionId }); - router.push('/'); - router.refresh(); - } else { - console.log(result); - } - } catch (error) { - handleClerkErrors(error, form, [ - { - code: 'form_identifier_not_found', - field: 'email', - message: "Can't find your account.", - }, - { - code: 'form_password_incorrect', - field: 'password', - message: 'Password is incorrect. Try again, or use another method.', - }, - { - code: 'strategy_for_user_invalid', - field: 'password', - message: - 'Account is not set up for password sign-in. Please sign in with Google.', - }, - ]); - } - - setSignInLoading(false); - }); - - const handleGoogleSignIn = async () => { - if (!isLoaded) return; - try { - await signIn.authenticateWithRedirect({ - strategy: 'oauth_google', - redirectUrl: '/sso-callback', - redirectUrlComplete: '/', - }); - } catch (error) { - // Handle any errors that might occur during the sign-in process - console.error('Google Sign-In Error:', error); - } - }; - - return ( -
- -
- {/* Heading */} -

Sign In

-

Sign into your account

- - - -
-
-

or

-
-
-
- - - - Forgot password? - - - - - {/* Sign-up option */} -
-

- Don't have an account yet?{' '} - - Join Us - -

-
-
-
-
- ); +import { currentUser } from '@clerk/nextjs'; +import type { Metadata } from 'next'; +import { redirect } from 'next/navigation'; +import SignIn from './SignIn'; + +export const metadata: Metadata = { + title: 'Sign In', +}; + +export default async function SignInPage() { + const user = await currentUser(); + if (user) redirect('/settings'); + return ; } diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 8b321096..13fd40d5 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -27,14 +27,14 @@ export default function HomePage() {

LEARN,

-
+

SOCIALISE,

-
+
CODE. -
+

Computer Science Club

@@ -46,7 +46,7 @@ export default function HomePage() { {/* Right side */}
- +
-
+
{/* */}
-
+
Thinking about Joining? diff --git a/src/app/about/FAQ.tsx b/src/app/about/FAQ.tsx index 41c48e18..1ba269d8 100644 --- a/src/app/about/FAQ.tsx +++ b/src/app/about/FAQ.tsx @@ -19,7 +19,7 @@ const FAQ = ({ question, answer, colour }: FAQProps) => { }; return ( - +
- + {'Meetto
- +
  • @@ -161,7 +161,7 @@ export default function AboutPage() {

- + {'Quiz
FAQ
-
+
{/* FAQ */}
diff --git a/src/app/events/FridayNight.tsx b/src/app/events/FridayNight.tsx index 674ee31c..d900ab0c 100644 --- a/src/app/events/FridayNight.tsx +++ b/src/app/events/FridayNight.tsx @@ -52,7 +52,7 @@ function Ducks() { function Details() { return ( - +
  • Need to blow off some steam from a long week of studying?
  • diff --git a/src/app/events/Info.tsx b/src/app/events/Info.tsx index 89b75b15..aaecb161 100644 --- a/src/app/events/Info.tsx +++ b/src/app/events/Info.tsx @@ -15,7 +15,7 @@ export default function Info({ className }: { className?: string }) {

    For further information, take a look at some of the events below!

- +
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index ba12f1ec..1339ac22 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -11,20 +11,31 @@ interface ButtonProps { type?: 'button' | 'submit' | 'reset'; width?: string; loading?: boolean; + size?: 'base' | 'small'; font?: string; } -const Button = ({ children, colour, href, onClick, width, type, loading, font }: ButtonProps) => { +const Button = ({ + children, + colour, + href, + onClick, + width, + type, + loading, + font, + size = 'base', +}: ButtonProps) => { const isAnchor = !!href; const Component = isAnchor ? 'a' : 'button'; return ( - + {children} diff --git a/src/components/FancyRectangle.tsx b/src/components/FancyRectangle.tsx index b14d8e3d..07db8bc5 100644 --- a/src/components/FancyRectangle.tsx +++ b/src/components/FancyRectangle.tsx @@ -39,7 +39,7 @@ const FancyRectangle = ({ className={`absolute bottom-0 right-0 h-full w-full ${ filled ? BG_COLOURS[colour] : BORDER_COLOURS[colour] } ${rounded ? 'rounded-xl' : ''}`} - >
+ />
{children}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 513f0282..906e768d 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -11,7 +11,7 @@ interface LinkProps { export default function Link({ name, link, icon: Icon, borderColour }: LinkProps) { return ( - + { - return !authRoutes.includes(req.nextUrl.pathname); - }, + publicRoutes: (req) => !authRoutes.includes(req.url), }); export const config = {