diff --git a/app/(api)/_actions/auth/register.ts b/app/(api)/_actions/auth/register.ts index 9bede8a4..01f1ed2c 100644 --- a/app/(api)/_actions/auth/register.ts +++ b/app/(api)/_actions/auth/register.ts @@ -3,5 +3,18 @@ import Register from '@datalib/auth/register'; export default async function RegisterAction(body: object) { - return Register(body); + try { + const result = await Register(body); + if (!result.ok) { + return result; + } + return { ok: true, body: null, error: null }; + } catch (e) { + const error = e as Error; + return { + ok: false, + body: null, + error: error.message, + }; + } } diff --git a/app/(pages)/(hackers)/_components/AuthFormBackground/AuthFormBackground.tsx b/app/(pages)/(hackers)/_components/AuthFormBackground/AuthFormBackground.tsx index ecab3ae4..822f0f62 100644 --- a/app/(pages)/(hackers)/_components/AuthFormBackground/AuthFormBackground.tsx +++ b/app/(pages)/(hackers)/_components/AuthFormBackground/AuthFormBackground.tsx @@ -14,10 +14,12 @@ export default function AuthFormBackground({ title, subtitle, children, + showAngelCow = true, }: { title: string; subtitle: string; children: React.ReactNode; + showAngelCow?: boolean; }) { const [mascotsVisibility, setMascotsVisibility] = useState(true); const pathname = usePathname(); @@ -35,13 +37,15 @@ export default function AuthFormBackground({
- Angel Cow + {showAngelCow && ( + Angel Cow + )}

{title}

diff --git a/app/(pages)/(hackers)/_components/AuthForms/DetailForm.module.scss b/app/(pages)/(hackers)/_components/AuthForms/DetailForm.module.scss deleted file mode 100644 index 4b589984..00000000 --- a/app/(pages)/(hackers)/_components/AuthForms/DetailForm.module.scss +++ /dev/null @@ -1,249 +0,0 @@ -@import 'app/(pages)/_globals/mixins'; - -.container { - display: flex; - flex-direction: column; - width: 100%; - - @include tablet-l { - margin-top: 5%; - } -} - -.header { - display: flex; - flex-direction: row; - gap: 26px; - margin-left: 5%; - margin-right: 5%; - margin-top: max(114px, 6%); - - .header_text { - display: flex; - flex-direction: column; - gap: 16px; - - h1 { - font-size: 1.75rem; - font-style: normal; - font-weight: 600; - line-height: normal; - letter-spacing: 0.56px; - - @include tablet-s { - font-size: 1.25rem; - } - - @include mobile-l { - font-size: 1rem; - } - } - - p { - font-size: 1.125rem; - font-style: normal; - font-weight: 400; - line-height: 145%; - letter-spacing: 0.36px; - - @include tablet-s { - font-size: 0.875rem; - } - - @include mobile-l { - font-size: 0.75rem; - } - } - } -} - -.form { - display: flex; - flex-direction: column; - align-self: center; - align-items: center; - width: 70%; - margin-top: 5%; - - @include desktop-m { - width: 80%; - } - - @include tablet-l { - padding-bottom: 32px; - } -} - -.characterGrid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 40px; - height: 100%; - width: 100%; - - @include desktop-m { - gap: 20px; - } - - @include tablet-l { - display: grid; - grid-template-columns: repeat(2, 1fr); - justify-content: space-between; - gap: 0; - width: 50%; - } - - @include tablet-s { - width: 75%; - } - - @include mobile-l { - width: 85%; - } -} - -.characterOption { - display: flex; - align-self: flex-end; - height: 100%; - - - &.selected { - border-radius: 20px; - border: 1px dashed #005271; - } -} - -.radioInput { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} - -.character { - display: flex; - flex-direction: column; - align-items: center; - height: 100%; - width: 100%; - cursor: pointer; -} - -.characterImage { - width: 100%; - height: 100%; - display: flex; - margin-bottom: 16px; - transition: transform 0.2s; - - &.selected { - filter: drop-shadow(32px 32px 2rem rgba(255, 243, 216, 0.60)); - } -} - -.characterImage:hover { - filter: drop-shadow(32px 32px 2rem rgba(255, 243, 216, 0.60)); -} - -.characterLabel { - text-align: center; - font-size: 1.5rem; - font-style: normal; - font-weight: 700; - line-height: normal; - padding-bottom: 32px; - - @include desktop-l { - font-size: 1rem; - } - - @include desktop-m { - font-size: 0.875rem; - padding-bottom: 16px; - } - - &.selected { - color: #005271; - } -} - -.bottom { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; - margin-top: 28px; - - @include tablet-l { - flex-direction: column; - align-items: end; - gap: 20px; - } -} - -.beginnerOption { - display: flex; - align-items: center; - gap: 16px; - margin-left: 40px; -} - -.checkboxInput { - appearance: none; - width: 23px; - height: 23px; - border: 1px solid #000; - border-radius: 4px; - position: relative; - cursor: pointer; - - &:checked { - background-color: #4a90e2; - border-color: #4a90e2; - - &:after { - content: "✓"; - color: white; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } -} - -.beginnerLabel { - font-size: 1.125rem; -} - -.submit_button { - padding: 12px 24px; - font-size: 1.125rem; - font-weight: 600; - line-height: normal; - font-style: normal; - line-height: 100%; - letter-spacing: 0.36px; - color: var(--text-light); - background: var(--background-secondary); - border: none; - cursor: not-allowed; - border-radius: 20px; - opacity: 0.3; - - @include tablet-l { - padding: 8px 16px; - font-size: 1rem; - letter-spacing: 0.24px; - } -} - -.valid { - opacity: 1; - cursor: pointer; -} - -.error_msg { - color: var(--text-error); -} diff --git a/app/(pages)/(hackers)/_components/AuthForms/DetailForm.tsx b/app/(pages)/(hackers)/_components/AuthForms/DetailForm.tsx deleted file mode 100644 index d00d7d0c..00000000 --- a/app/(pages)/(hackers)/_components/AuthForms/DetailForm.tsx +++ /dev/null @@ -1,149 +0,0 @@ -'use client'; - -import { useEffect, useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import Image from 'next/image'; - -import { updateUser } from '@actions/users/updateUser'; -import Loader from '@components/Loader/Loader'; -import DeveloperCow from 'public/hackers/login/developer_cow.svg'; -import DesignerBunny from 'public/hackers/login/designer_bunny.svg'; -import PmFroggy from 'public/hackers/login/pm_froggy.svg'; -import OtherDucky from 'public/hackers/login/other_ducky.svg'; -import styles from './DetailForm.module.scss'; - -const characters = [ - { - label: 'DEVELOPER', - role: 'developer', - image: DeveloperCow, - alt: 'Developer Cow', - }, - { - label: 'DESIGNER', - role: 'designer', - image: DesignerBunny, - alt: 'Designer Bunny', - }, - { - label: 'PROJECT MANAGER', - role: 'pm', - image: PmFroggy, - alt: 'PM Froggy', - }, - { - label: 'OTHER', - role: 'other', - image: OtherDucky, - alt: 'Other Ducky', - }, -]; - -export default function DetailForm({ id }: any) { - const router = useRouter(); - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [valid, setValid] = useState(false); - const [selectedPosition, setSelectedPosition] = useState(''); - - const handleRegister = async (e: FormEvent) => { - e.preventDefault(); - - setLoading(true); - setError(''); - - const formData = new FormData(e.currentTarget); - - const position = formData.get('position') as string; - const is_beginner = formData.get('beginner') !== null; - - const userRes = await updateUser(id, { - $set: { - position, - is_beginner, - }, - }); - - if (userRes.ok) { - router.push('/'); - } else { - setError(userRes.error ?? 'Error updating details'); - } - - setLoading(false); - }; - - useEffect(() => { - setValid(selectedPosition !== ''); - }, [selectedPosition]); - - return ( -
-
- {error &&

{error}

} - -
- {characters.map(({ label, role, image, alt }) => ( -
- setSelectedPosition(role)} - className={styles.radioInput} - /> - -
- ))} -
- -
-
- - -
- - -
-
- - {loading && } -
- ); -} diff --git a/app/(pages)/(hackers)/_components/AuthForms/register/ChooseLevel.tsx b/app/(pages)/(hackers)/_components/AuthForms/register/ChooseLevel.tsx new file mode 100644 index 00000000..0d7f9142 --- /dev/null +++ b/app/(pages)/(hackers)/_components/AuthForms/register/ChooseLevel.tsx @@ -0,0 +1,164 @@ +'use client'; + +import Image from 'next/image'; +import AuthFormBackground from '../../AuthFormBackground/AuthFormBackground'; + +type ChooseLevelProps = { + value?: string; + onSelect: (value: string) => void; + onBack: () => void; + onNext: () => void; + loading?: boolean; + error?: string; +}; + +const levels = [ + { + id: 'beginner', + title: 'Beginner', + tag: 'NEW TO THE SCENE', + description: + "You're here to build your foundation, explore new tools, and ship your first few projects.", + image: '/hackers/register/beginner-frog.svg', + }, + { + id: 'experienced', + title: 'Experienced', + tag: 'A SEASONED HACKER', + description: + "You're comfortable with the 36-hour grind and ready to push the boundaries of your projects.", + image: '/hackers/register/experienced-frog.svg', + }, +]; + +export default function ChooseLevel({ + value, + onSelect, + onBack, + onNext, + loading = false, + error = '', +}: ChooseLevelProps) { + return ( + +
+ {/* cards (ALWAYS SIDE BY SIDE) */} +
+ {levels.map((level) => { + const selected = value === level.id; + return ( + + ); + })} +
+ + {/* footer */} +
+ {/* indicators */} +
+
+
+
+ {/* nav buttons */} +
+ {/* error message - takes up vertical space*/} +

+ {error} +

+
+ {/* NEXT (top on mobile) */} + + + {/* BACK */} + +
+
+
+
+ + ); +} diff --git a/app/(pages)/(hackers)/_components/AuthForms/register/ChooseRole.tsx b/app/(pages)/(hackers)/_components/AuthForms/register/ChooseRole.tsx new file mode 100644 index 00000000..64d52205 --- /dev/null +++ b/app/(pages)/(hackers)/_components/AuthForms/register/ChooseRole.tsx @@ -0,0 +1,144 @@ +'use client'; + +import Image from 'next/image'; +import AuthFormBackground from '../../AuthFormBackground/AuthFormBackground'; + +type ChooseRoleProps = { + value?: string; + onSelect: (value: string) => void; + onBack: () => void; + onNext: () => void; +}; + +const roles = [ + { + id: 'developer', + label: 'Developer', + image: '/hackers/register/dev-cow.svg', + }, + { + id: 'designer', + label: 'Designer', + image: '/hackers/register/designer-bunny.svg', + }, + { + id: 'pm', + label: 'Product', + image: '/hackers/register/product-frog.svg', + }, + { + id: 'other', + label: 'Other', + image: '/hackers/register/explorer-duck.svg', + }, +]; + +export default function ChooseRole({ + value, + onSelect, + onBack, + onNext, +}: ChooseRoleProps) { + return ( + + {/* cards */} +
+ {roles.map((role) => { + const selected = value === role.id; + return ( + + ); + })} +
+ + {/* Bottom navigation */} +
+ {/* indicators */} +
+
+
+
+ + {/* nav buttons */} +
+ {/* NEXT (top on mobile) */} + + + {/* BACK */} + +
+
+ + ); +} diff --git a/app/(pages)/(hackers)/_components/AuthForms/register/DetailForm.tsx b/app/(pages)/(hackers)/_components/AuthForms/register/DetailForm.tsx new file mode 100644 index 00000000..af9af33a --- /dev/null +++ b/app/(pages)/(hackers)/_components/AuthForms/register/DetailForm.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +import { updateUser } from '@actions/users/updateUser'; +import ChooseRole from './ChooseRole'; +import ChooseLevel from './ChooseLevel'; + +type DetailFormProps = { + id: string; +}; + +export default function DetailForm({ id }: DetailFormProps) { + const router = useRouter(); + + const [stage, setStage] = useState<0 | 1>(0); + const [role, setRole] = useState(); + const [level, setLevel] = useState(); + const [loading, setLoading] = useState(false); + + const [error, setError] = useState(''); + + const handleRegister = async () => { + if (!role || !level) return; + + setLoading(true); + setError(''); + + const is_beginner = level === 'beginner'; + + const userRes = await updateUser(id, { + $set: { + position: role, + is_beginner, + }, + }); + + if (userRes.ok) { + router.push('/'); + } else { + setError(userRes.error ?? 'Error updating details'); + } + + setLoading(false); + }; + + return ( +
+ {stage === 0 ? ( + router.push('/register')} + onNext={() => { + if (!role) return; + setStage(1); + }} + /> + ) : ( + setStage(0)} + onNext={handleRegister} + loading={loading} + error={error} + /> + )} +
+ ); +} diff --git a/app/(pages)/(hackers)/_components/AuthForms/RegisterForm.tsx b/app/(pages)/(hackers)/_components/AuthForms/register/RegisterForm.tsx similarity index 100% rename from app/(pages)/(hackers)/_components/AuthForms/RegisterForm.tsx rename to app/(pages)/(hackers)/_components/AuthForms/register/RegisterForm.tsx diff --git a/app/(pages)/(hackers)/register/details/page.tsx b/app/(pages)/(hackers)/register/details/page.tsx index e35aad4c..4f7a45d8 100644 --- a/app/(pages)/(hackers)/register/details/page.tsx +++ b/app/(pages)/(hackers)/register/details/page.tsx @@ -1,7 +1,6 @@ import { redirect } from 'next/navigation'; -import DetailForm from '@pages/(hackers)/_components/AuthForms/DetailForm'; -import AuthFormBackground from '../../_components/AuthFormBackground/AuthFormBackground'; +import DetailForm from '@pages/(hackers)/_components/AuthForms/register/DetailForm'; import getActiveUser from 'app/(pages)/_utils/getActiveUser'; export default async function DetailPage() { @@ -9,13 +8,5 @@ export default async function DetailPage() { if (user.role === 'judge') redirect('/judges/register'); - return ( - - - - ); + return ; } diff --git a/app/(pages)/(hackers)/register/page.tsx b/app/(pages)/(hackers)/register/page.tsx index b421fe5c..bcd8d9b8 100644 --- a/app/(pages)/(hackers)/register/page.tsx +++ b/app/(pages)/(hackers)/register/page.tsx @@ -2,9 +2,9 @@ import { redirect } from 'next/navigation'; import { auth } from '@/auth'; import { getInviteData } from '@actions/invite/getInviteData'; -import RegisterForm from '../_components/AuthForms/RegisterForm'; -import AuthFormBackground from '../_components/AuthFormBackground/AuthFormBackground'; import InviteOnlyRoute from '@components/InviteOnlyRoute/InviteOnlyRoute'; +import AuthFormBackground from '@pages/(hackers)/_components/AuthFormBackground/AuthFormBackground'; +import RegisterForm from '../_components/AuthForms/register/RegisterForm'; export default async function RegisterPage() { const session = await auth(); @@ -19,9 +19,8 @@ export default async function RegisterPage() { return ( diff --git a/public/hackers/login/designer_bunny.svg b/public/hackers/login/designer_bunny.svg deleted file mode 100644 index eb23fe1e..00000000 --- a/public/hackers/login/designer_bunny.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/hackers/login/developer_cow.svg b/public/hackers/login/developer_cow.svg deleted file mode 100644 index a3ca0f45..00000000 --- a/public/hackers/login/developer_cow.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/hackers/login/other_ducky.svg b/public/hackers/login/other_ducky.svg deleted file mode 100644 index ab11d59e..00000000 --- a/public/hackers/login/other_ducky.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/hackers/login/pm_froggy.svg b/public/hackers/login/pm_froggy.svg deleted file mode 100644 index 8b9790e7..00000000 --- a/public/hackers/login/pm_froggy.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/hackers/register/beginner-frog.svg b/public/hackers/register/beginner-frog.svg new file mode 100644 index 00000000..65d3130c --- /dev/null +++ b/public/hackers/register/beginner-frog.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/hackers/register/designer-bunny.svg b/public/hackers/register/designer-bunny.svg new file mode 100644 index 00000000..228c44a5 --- /dev/null +++ b/public/hackers/register/designer-bunny.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/hackers/register/dev-cow.svg b/public/hackers/register/dev-cow.svg new file mode 100644 index 00000000..221e7061 --- /dev/null +++ b/public/hackers/register/dev-cow.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/hackers/register/experienced-frog.svg b/public/hackers/register/experienced-frog.svg new file mode 100644 index 00000000..6d58185c --- /dev/null +++ b/public/hackers/register/experienced-frog.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/hackers/register/explorer-duck.svg b/public/hackers/register/explorer-duck.svg new file mode 100644 index 00000000..a01f6056 --- /dev/null +++ b/public/hackers/register/explorer-duck.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/hackers/register/product-frog.svg b/public/hackers/register/product-frog.svg new file mode 100644 index 00000000..637a53f8 --- /dev/null +++ b/public/hackers/register/product-frog.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +