DEVELOPER REFERENCE — LIBRARY LOOT
Library Loot
Developer Reference
← Index

Source: pages/Signup.jsx

// src/pages/Signup.jsx
//
// Account creation page. Google sign-in OR email/password signup. Requires
// the user to self-attest they are 18+ and to accept the Privacy Policy +
// Terms of Service before submit is enabled.
//
// Account creation = adult account. Children participate through this
// account as sub-profiles created from the parent dashboard (ITEM 2d).
//
// Created by Miguel Brown on 5/12/26.
// Copyright (c) 2026 Luckey Logic LLC. All rights reserved.

import React, { useEffect, useState }   from 'react'
import { Link, useNavigate }            from 'react-router-dom'

import { useAuth }                      from '../context/AuthContext.jsx'

import styles                           from './Auth.module.css'

/**
 * Signup — Google + Email/Password account creation with 18+ self-attestation
 * and Privacy Policy / Terms acceptance.
 *
 * @returns {JSX.Element}
 */
export default function Signup() {

  const { user, signInWithGoogle, signUpWithEmail, clearError } = useAuth()
  const navigate = useNavigate()

  const [displayName,     setDisplayName]     = useState('')
  const [email,           setEmail]           = useState('')
  const [password,        setPassword]        = useState('')
  const [passwordConfirm, setPasswordConfirm] = useState('')
  const [confirmAdult,    setConfirmAdult]    = useState(false)
  const [confirmTerms,    setConfirmTerms]    = useState(false)
  const [submitting,      setSubmitting]      = useState(false)
  const [localError,      setLocalError]      = useState(null)

  // Clear context error on mount.
  useEffect(() => {
    clearError()
  }, [clearError])

  // If already signed in, skip the form.
  useEffect(() => {
    if (user) {
      navigate('/', { replace: true })
    }
  }, [user, navigate])

  // Submit is enabled only when both consent checkboxes are ticked and the
  // required form fields look filled in. Google flow has the same gate.
  const consentReady    = confirmAdult && confirmTerms
  const emailFormReady  =
    displayName.trim() &&
    email.trim()       &&
    password           &&
    password === passwordConfirm &&
    consentReady

  const handleGoogle = async () => {
    setLocalError(null)
    if (!consentReady) {
      setLocalError('Please confirm you are 18 or older and accept the policies before continuing.')
      return
    }
    setSubmitting(true)
    try {
      await signInWithGoogle()
    } catch (e) {
      setLocalError(e.message)
    } finally {
      setSubmitting(false)
    }
  }

  const handleEmail = async (event) => {
    event.preventDefault()
    setLocalError(null)

    if (!consentReady) {
      setLocalError('Please confirm you are 18 or older and accept the policies before continuing.')
      return
    }
    if (password !== passwordConfirm) {
      setLocalError('Passwords don\'t match. Please re-type them.')
      return
    }
    if (password.length < 6) {
      setLocalError('Use a password of at least 6 characters.')
      return
    }

    setSubmitting(true)
    try {
      await signUpWithEmail(email.trim(), password, displayName.trim())
    } catch (e) {
      setLocalError(e.message)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <section className={styles.authWrap}>
      <div className={styles.card}>

        <p className={styles.eyebrow}>Create an account</p>
        <h1 className={styles.title}>Join Library Loot</h1>
        <p className={styles.subtitle}>
          Parents and librarians only. You&apos;ll add your child(ren) as
          sub-profiles after signing in.
        </p>

        <div className={styles.checkRow} style={{ marginBottom: '0.75rem' }}>
          <input
            id      ="signup-adult"
            type    ="checkbox"
            checked ={confirmAdult}
            onChange={(e) => setConfirmAdult(e.target.checked)}
            disabled={submitting}
          />
          <label htmlFor="signup-adult">
            I confirm I am 18 years of age or older and authorized to create this account.
          </label>
        </div>

        <div className={styles.checkRow} style={{ marginBottom: '1.25rem' }}>
          <input
            id      ="signup-terms"
            type    ="checkbox"
            checked ={confirmTerms}
            onChange={(e) => setConfirmTerms(e.target.checked)}
            disabled={submitting}
          />
          <label htmlFor="signup-terms">
            I have read and accept the
            {' '}<Link to="/privacy" target="_blank" rel="noreferrer">Privacy Policy</Link>
            {' '}and
            {' '}<Link to="/terms" target="_blank" rel="noreferrer">Terms of Service</Link>.
          </label>
        </div>

        <button
          type      ="button"
          className ={styles.googleBtn}
          onClick   ={handleGoogle}
          disabled  ={submitting || !consentReady}
          title     ={!consentReady ? 'Confirm 18+ and accept the policies first.' : ''}
        >
          <GoogleGlyph />
          Continue with Google
        </button>

        <div className={styles.divider}>or</div>

        <form className={styles.form} onSubmit={handleEmail}>

          <div className={styles.field}>
            <label htmlFor="signup-name" className={styles.label}>Your name</label>
            <input
              id        ="signup-name"
              type      ="text"
              autoComplete="name"
              value     ={displayName}
              onChange  ={(e) => setDisplayName(e.target.value)}
              className ={styles.input}
              disabled  ={submitting}
              required
            />
          </div>

          <div className={styles.field}>
            <label htmlFor="signup-email" className={styles.label}>Email</label>
            <input
              id        ="signup-email"
              type      ="email"
              autoComplete="email"
              value     ={email}
              onChange  ={(e) => setEmail(e.target.value)}
              className ={styles.input}
              disabled  ={submitting}
              required
            />
          </div>

          <div className={styles.field}>
            <label htmlFor="signup-pw" className={styles.label}>Password</label>
            <input
              id        ="signup-pw"
              type      ="password"
              autoComplete="new-password"
              value     ={password}
              onChange  ={(e) => setPassword(e.target.value)}
              className ={styles.input}
              disabled  ={submitting}
              minLength ={6}
              required
            />
          </div>

          <div className={styles.field}>
            <label htmlFor="signup-pw2" className={styles.label}>Confirm password</label>
            <input
              id        ="signup-pw2"
              type      ="password"
              autoComplete="new-password"
              value     ={passwordConfirm}
              onChange  ={(e) => setPasswordConfirm(e.target.value)}
              className ={styles.input}
              disabled  ={submitting}
              minLength ={6}
              required
            />
          </div>

          <button
            type      ="submit"
            className ={`btn btn-primary ${styles.submit}`}
            disabled  ={submitting || !emailFormReady}
          >
            {submitting ? 'Creating account…' : 'Create account'}
          </button>
        </form>

        {localError ? (
          <div className={styles.error} role="alert">{localError}</div>
        ) : null}

        <p className={styles.foot}>
          Already have an account? <Link to="/login">Sign in</Link>
        </p>

      </div>
    </section>
  )
}

/**
 * GoogleGlyph — inline multicolor Google "G" mark for the Continue-with-Google
 * button. Duplicated from Login for now; if a third surface needs it, extract.
 * @returns {JSX.Element}
 */
function GoogleGlyph() {
  return (
    <svg className={styles.googleIcon} viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
      <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
      <path fill="#FBBC05" d="M5.84 14.1c-.22-.66-.35-1.36-.35-2.1s.13-1.44.35-2.1V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.83z" />
      <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.83C6.71 7.31 9.14 5.38 12 5.38z" />
    </svg>
  )
}