diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 34e8adf..00bd750 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -1,11 +1,8 @@ /* Copyright (C) 2024 Manuel Bustillo*/ 'use client'; -import Link from 'next/link'; -import styles from '@/app/ui/home.module.css'; import LoginForm from '@/app/ui/components/login-form'; - - +import RegistrationForm from '@/app/ui/components/registration-form'; export default async function Page({ params }: { params: Promise<{ slug: string }> }) { @@ -14,17 +11,15 @@ export default async function Page({ params }: { params: Promise<{ slug: string
- Already have an account? Sign in -
Don't have an account? Register now! +
-
); } diff --git a/app/api/authentication.tsx b/app/api/authentication.tsx index 6b57d06..5390fd5 100644 --- a/app/api/authentication.tsx +++ b/app/api/authentication.tsx @@ -1,10 +1,9 @@ /* Copyright (C) 2024 Manuel Bustillo*/ -import { getCsrfToken, getSlug } from '@/app/lib/utils'; -import { User } from '@/app/lib/definitions'; +import { asArray, getCsrfToken, getSlug } from '@/app/lib/utils'; +import { Captcha, StructuredErrors, User } from '@/app/lib/definitions'; export function login({ email, password, onLogin }: { email: string, password: string, onLogin: (user: User) => void }) { - console.log(email, password); return fetch(`/api/${getSlug()}/users/sign_in`, { method: 'POST', body: JSON.stringify({ user: { email, password } }), @@ -34,3 +33,45 @@ export function logout({ onLogout }: { onLogout: () => void }) { }).then(onLogout) .catch((error) => console.error(error)); } + +function flattenErrors(errors: StructuredErrors): string[] { + if(errors instanceof Array) { + return errors; + } + return Object.keys(errors).map((key) => { + return `${key}: ${asArray(errors[key]).join(', ')}`; + }); +} + +export function register({slug, email, password, passwordConfirmation, captcha, onRegister, onError}: { + slug: string, + email: string, + password: string, + passwordConfirmation: string, + captcha: Captcha, + onRegister: () => void, + onError: (errors: string[]) => void +}){ + fetch(`/api/${slug}/users`, { + method: 'POST', + body: JSON.stringify( + { + user: { email, password, password_confirmation: passwordConfirmation }, + captcha: { id: captcha.id, answer: captcha.answer } + } + ), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then((response) => { + if(response.ok) { + response.json().then(onRegister); + } else { + response.json().then((data: any) => { + onError(data.errors && flattenErrors(data.errors) || [data.error]); + }); + } + }) +} diff --git a/app/api/captcha.tsx b/app/api/captcha.tsx new file mode 100644 index 0000000..e49ac00 --- /dev/null +++ b/app/api/captcha.tsx @@ -0,0 +1,19 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + +import { data } from "autoprefixer"; +import { getCsrfToken } from "../lib/utils"; + +export function getCaptchaChallenge({onRetrieve}: {onRetrieve: (id: string, url: string) => void}){ + return fetch('/api/captcha', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }) + .then((response) => response.json()) + .then((data: any) => { + onRetrieve(data.id, data.media_url) + }) + .catch((error) => console.error(error)); +} \ No newline at end of file diff --git a/app/lib/definitions.ts b/app/lib/definitions.ts index 45b5536..aba529b 100644 --- a/app/lib/definitions.ts +++ b/app/lib/definitions.ts @@ -64,4 +64,13 @@ export type guestsTable = { export type User = { id: string; email: string; -} \ No newline at end of file +} + +export type Captcha = { + id: string; + answer: string; +} + +export type StructuredErrors = { + [key: string]: string[]|string; +}; \ No newline at end of file diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 069744a..bbd6700 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -13,3 +13,8 @@ export const getSlug = () => localStorage.getItem('slug') || 'default'; export const capitalize = (val:string) => { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } + +// From https://stackoverflow.com/a/62118163/3607039 +export function asArray(value: T | T[]): T[] { + return ([] as T[]).concat(value) +} \ No newline at end of file diff --git a/app/ui/components/registration-form.tsx b/app/ui/components/registration-form.tsx new file mode 100644 index 0000000..3c7b5d7 --- /dev/null +++ b/app/ui/components/registration-form.tsx @@ -0,0 +1,98 @@ +/* Copyright (C) 2024 Manuel Bustillo*/ + +'use client'; + +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState, useEffect } from 'react'; +import { classNames } from './button'; +import { getSlug } from '@/app/lib/utils'; +import { register } from '@/app/api/authentication'; +import { getCaptchaChallenge } from '@/app/api/captcha'; + +export default function RegistrationForm() { + const [submitted, setSubmitted] = useState(false); + const [errors, setErrors] = useState([]); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [passwordConfirmation, setPasswordConfirmation] = useState(""); + const [slug, setSlug] = useState(getSlug()); + + const [captchaId, setCaptchaId] = useState(""); + const [captchaUrl, setCaptchaUrl] = useState(""); + const [captchaAnswer, setCaptchaAnswer] = useState(""); + + const refreshCaptcha = () => { + getCaptchaChallenge({ + onRetrieve: (id, url) => { + console.log(id, url); + setCaptchaId(id); + setCaptchaUrl(url); + setCaptchaAnswer(""); + } + }); + } + + useEffect(refreshCaptcha, []) + + return ( + submitted ? ( +
+
Registration successful. Check your email for a confirmation link.
+
+ ) : ( + +
+ + setEmail(e.target.value)} /> + + + + setPassword(e.target.value)} /> + + + + setPasswordConfirmation(e.target.value)} /> + + + + setSlug(e.target.value)} /> + + + captcha + + setCaptchaAnswer(e.target.value)} /> + + + + {errors.map((error, index) => ( +
{error}
+ ))} + + + +
+ ) + + ); +} \ No newline at end of file