diff --git a/bun.lockb b/bun.lockb index 598e05a5..03621b42 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/messages/en.json b/messages/en.json index ba8261a8..003813a2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -21,7 +21,9 @@ "selectMonth": "Select month", "selectYear": "Select year", "pickDate": "Pick a date", - "dateFormat": "dd/MM/yyyy" + "dateFormat": "dd/MM/yyyy", + "hidePassword": "Hide password", + "showPassword": "Show password" }, "error": { "notFound": "404 - Page not found", @@ -37,7 +39,31 @@ "signInWith": "Sign in with", "hackerspaceAccount": "Hackerspace Account", "success": "Success!", - "home": "Home" + "home": "Home", + "signIn": "Sign in", + "useYourAccount": "Use your account", + "forgotPassword": "Forgot password?", + "submit": "Submit", + "form": { + "username": { + "label": "Username", + "required": "Username is required", + "minLength": "Username must be at least 5 characters", + "maxLength": "Username must be less than 8 characters", + "invalid": "Username must contain only letters" + }, + "password": { + "label": "Password", + "required": "Password is required", + "minLength": "Password must be at least 8 characters", + "maxLength": "Password must be less than 50 characters", + "uppercase": "Password must contain at least one uppercase letter", + "specialChar": "Password must contain at least one special character", + "confirmLabel": "Confirm password", + "mismatch": "Passwords do not match", + "weak": "Password is too weak" + } + } }, "layout": { "hackerspaceHome": "Hackerspace homepage", diff --git a/messages/no.json b/messages/no.json index f52f7ba2..b3592f51 100644 --- a/messages/no.json +++ b/messages/no.json @@ -21,7 +21,9 @@ "selectMonth": "Velg måned", "selectYear": "Velg år", "pickDate": "Velg en dato", - "dateFormat": "dd.MM.yyyy" + "dateFormat": "dd.MM.yyyy", + "hidePassword": "Gjem passord", + "showPassword": "Vis passord" }, "error": { "notFound": "404 - Siden ble ikke funnet", @@ -37,7 +39,31 @@ "signInWith": "Logg inn med", "hackerspaceAccount": "Hackerspace-konto", "success": "Suksess!", - "home": "Hjem" + "home": "Hjem", + "signIn": "Logg inn", + "useYourAccount": "Bruk din konto", + "forgotPassword": "Glemt passord?", + "submit": "Send", + "form": { + "username": { + "label": "Brukernavn", + "required": "Brukernavn er påkrevd", + "minLength": "Brukernavn må være minst 5 tegn", + "maxLength": "Brukernavn må være mindre enn 8 tegn", + "invalid": "Brukernavn må kun inneholde bokstaver" + }, + "password": { + "label": "Passord", + "required": "Passord er påkrevd", + "minLength": "Passord må være minst 8 tegn", + "maxLength": "Passord må være mindre enn 50 tegn", + "uppercase": "Passord må inneholde minst én stor bokstav", + "specialChar": "Passord må inneholde minst ett spesialtegn", + "confirmLabel": "Bekreft passord", + "mismatch": "Passordene stemmer ikke overens", + "weak": "Passordet er for svakt" + } + } }, "layout": { "hackerspaceHome": "Hackerspace hjemmeside", diff --git a/src/app/[locale]/auth/account/page.tsx b/src/app/[locale]/auth/account/page.tsx new file mode 100644 index 00000000..30725ec8 --- /dev/null +++ b/src/app/[locale]/auth/account/page.tsx @@ -0,0 +1,12 @@ +import { AccountSignInForm } from '@/components/auth/AccountSignInForm'; +import { setRequestLocale } from 'next-intl/server'; + +export default async function AccountPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setRequestLocale(locale); + return ; +} diff --git a/src/app/[locale]/auth/create-account/page.tsx b/src/app/[locale]/auth/create-account/page.tsx new file mode 100644 index 00000000..dbe8e683 --- /dev/null +++ b/src/app/[locale]/auth/create-account/page.tsx @@ -0,0 +1,32 @@ +import { Button } from '@/components/ui/Button'; +import { Separator } from '@/components/ui/Separator'; +import { Link } from '@/lib/locale/navigation'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; + +export default async function CreateAccountPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setRequestLocale(locale); + const t = await getTranslations('auth'); + return ( +
+
+

{t('success')}

+

+ { + 'you are now a member of Hackerspace. Now you can finally start praying to our one true leader' + } +

+
+ +
+ +
+
+ ); +} diff --git a/src/app/[locale]/auth/page.tsx b/src/app/[locale]/auth/page.tsx index 948b62e5..e64c7e31 100644 --- a/src/app/[locale]/auth/page.tsx +++ b/src/app/[locale]/auth/page.tsx @@ -1,6 +1,7 @@ import { FeideLogo } from '@/components/assets/logos/FeideLogo'; import { Button } from '@/components/ui/Button'; import { Separator } from '@/components/ui/Separator'; +import { Link } from '@/lib/locale/navigation'; import { FingerprintIcon } from 'lucide-react'; import { getTranslations, setRequestLocale } from 'next-intl/server'; @@ -21,12 +22,17 @@ export default async function SignInPage({

{t('signInWith')}

- -
diff --git a/src/components/auth/AccountSignInForm.tsx b/src/components/auth/AccountSignInForm.tsx new file mode 100644 index 00000000..085df6e8 --- /dev/null +++ b/src/components/auth/AccountSignInForm.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { accountSignInSchema } from '@/validations/auth/accountSignInSchema'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; + +import { api } from '@/lib/api/client'; +import { Link } from '@/lib/locale/navigation'; +import { useRouter } from '@/lib/locale/navigation'; + +import { usePending } from '@/components/auth/PendingBar'; +import { PasswordInput } from '@/components/composites/PasswordInput'; +import { Button } from '@/components/ui/Button'; +import { + Form, + FormControl, + FormItem, + FormLabel, + FormMessage, + useForm, +} from '@/components/ui/Form'; +import { Input } from '@/components/ui/Input'; + +function AccountSignInForm() { + const router = useRouter(); + const t = useTranslations('auth'); + const [accountCreated, setAccountCreated] = useState(false); + const formSchema = accountSignInSchema(t as (key: string) => string, false); + const { isPending, setPending } = usePending(); + + const form = useForm(formSchema, { + defaultValues: { + username: '', + password: '', + }, + onSubmit: () => { + if (!accountCreated) { + router.push('/auth/create-account'); + } + router.push('/'); + }, + }); + + return ( +
+
+

{t('signIn')}

+

{t('useYourAccount')}

+
+
+ + {(field) => ( + + {t('form.username.label')} + + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + + + + )} + + + {(field) => ( + +
+ {t('form.password.label')} + +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + + +
+ )} +
+
+ [state.canSubmit]}> + {([canSubmit]) => ( + + )} + +
+
+
+ ); +} + +export { AccountSignInForm }; diff --git a/src/components/composites/PasswordInput.tsx b/src/components/composites/PasswordInput.tsx new file mode 100644 index 00000000..432a3ba2 --- /dev/null +++ b/src/components/composites/PasswordInput.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { EyeIcon, EyeOffIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import * as React from 'react'; + +import { cx } from '@/lib/utils'; + +import { Button } from '@/components/ui/Button'; +import { Input, type InputProps } from '@/components/ui/Input'; + +const PasswordInput = React.forwardRef( + ({ className, ...props }, ref) => { + const t = useTranslations('ui'); + const [showPassword, setShowPassword] = React.useState(false); + const disabled = + props.value === '' || props.value === undefined || props.disabled; + + return ( +
+ + +
+ ); + }, +); +PasswordInput.displayName = 'PasswordInput'; + +export { PasswordInput }; diff --git a/src/components/layout/header/ProfileMenu.tsx b/src/components/layout/header/ProfileMenu.tsx index 13c9b860..9504a666 100644 --- a/src/components/layout/header/ProfileMenu.tsx +++ b/src/components/layout/header/ProfileMenu.tsx @@ -7,6 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/DropdownMenu'; +import { Link } from '@/lib/locale/navigation'; import { UserIcon } from 'lucide-react'; import * as React from 'react'; @@ -20,7 +21,9 @@ function ProfileMenu({ t }: { t: { profile: string; signIn: string } }) { - {t.signIn} + + {t.signIn} + ); diff --git a/src/lib/locale/index.ts b/src/lib/locale/index.ts index 17bc19a6..ce6b211f 100644 --- a/src/lib/locale/index.ts +++ b/src/lib/locale/index.ts @@ -22,6 +22,10 @@ const routing = defineRouting({ en: '/auth', no: '/autentisering', }, + '/auth/account': { + en: '/auth/account', + no: '/autentisering/konto', + }, '/auth/create-account': { en: '/auth/create-account', no: '/autentisering/opprett-konto', diff --git a/src/validations/auth/accountSignInSchema.ts b/src/validations/auth/accountSignInSchema.ts new file mode 100644 index 00000000..d55ac683 --- /dev/null +++ b/src/validations/auth/accountSignInSchema.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +function accountSignInSchema(t: (key: string) => string, isStrict = true) { + let passwordSchema = z.string().min(1, t('form.password.required')); + + if (isStrict) { + passwordSchema = passwordSchema + .min(8, t('form.password.minLength')) + .max(50, t('form.password.maxLength')) + .regex(/[A-Z]/, t('form.password.uppercase')) + .regex(/[^a-zA-Z0-9]/, t('form.password.specialChar')); + } + + return z.object({ + username: z + .string() + .min(1, t('form.username.required')) + .min(5, t('form.username.minLength')) + .max(8, t('form.username.maxLength')) + .regex(/^[a-z]+$/, t('form.username.invalid')), + password: passwordSchema, + }); +} + +export { accountSignInSchema };