Better UI
This commit is contained in:
+163
-128
@@ -3,157 +3,192 @@
|
||||
'use client';
|
||||
|
||||
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { PiCheckCircle, PiWarningCircle } from "react-icons/pi";
|
||||
|
||||
interface AlertState {
|
||||
show: boolean;
|
||||
type: 'success' | 'danger' | '';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SubmitResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export default function BeachRegistrationForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [wasValidated, setWasValidated] = useState<boolean>(false);
|
||||
const [alert, setAlert] = useState<AlertState>({ show: false, type: '', message: '' });
|
||||
import React, { useRef, useActionState, useEffect } from 'react';
|
||||
import { FaUser, FaEnvelope, FaPhone, FaUsers, FaTrophy, FaCheckCircle, FaExclamationTriangle } from "react-icons/fa";
|
||||
import { submitRegistration } from './actions';
|
||||
|
||||
export default function BeachRegistrationForm({ config }: { config: any }) {
|
||||
const [state, formAction, isPending] = useActionState(submitRegistration, null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const turnstileRef = useRef<TurnstileInstance>(null);
|
||||
|
||||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
const form = formRef.current;
|
||||
if (!form) return;
|
||||
const classesData = Array.isArray(config.beachPage?.classes)
|
||||
? config.beachPage.classes
|
||||
: [
|
||||
{ name: 'Grön', color: '#16a34a', desc: 'Nybörjare' },
|
||||
{ name: 'Blå', color: '#2563eb', desc: 'Spelat lite' },
|
||||
{ name: 'Svart', color: '#111827', desc: 'Spelat mycket' }
|
||||
];
|
||||
|
||||
setWasValidated(true);
|
||||
if (!form.checkValidity()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setAlert({ show: false, type: '', message: '' });
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
||||
|
||||
const result = (await response.json()) as SubmitResponse;
|
||||
const message = result.detail ? `${result.message} ${result.detail}` : result.message;
|
||||
|
||||
setAlert({
|
||||
show: true,
|
||||
type: result.success ? 'success' : 'danger',
|
||||
message: message,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
form.reset();
|
||||
setWasValidated(false);
|
||||
if (turnstileRef.current) turnstileRef.current.reset();
|
||||
}
|
||||
} catch (error) {
|
||||
setAlert({
|
||||
show: true,
|
||||
type: 'danger',
|
||||
message: 'Ett tekniskt fel uppstod. Försök igen senare.',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
useEffect(() => {
|
||||
// Rensa endast vid lyckad inskickning
|
||||
if (state?.success) {
|
||||
formRef.current?.reset();
|
||||
turnstileRef.current?.reset();
|
||||
} else if (state?.success === false) {
|
||||
// Återställ Turnstile så de kan försöka igen vid fel
|
||||
turnstileRef.current?.reset();
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<section className="w-full flex justify-center px-1.25">
|
||||
<div className="bg-black p-5 rounded-[20px] my-7.5 mx-1.25 w-full max-w-180 shadow-2xl">
|
||||
<h2 className="font-beachday text-[clamp(1.875rem,5.2vw,50px)] text-white text-center m-[5px_0] uppercase tracking-wide">
|
||||
Anmälningsformulär
|
||||
</h2>
|
||||
<section className="w-full flex justify-center px-4 mb-20">
|
||||
<div className="bg-[#fdf5e6] p-8 md:p-12 rounded-[40px] w-full max-w-3xl shadow-2xl border-b-8 border-[#e6be8a]">
|
||||
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleSubmit}
|
||||
className={`group/form flex flex-col items-center w-full mt-5 ${wasValidated ? 'was-validated' : ''}`}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
{/* INPUT FÄLT (Namn, Email, Tel, Lagnamn, Antal) */}
|
||||
{[
|
||||
{ name: 'name', type: 'text', placeholder: 'Ditt namn', auto: 'name', error: 'Glöm inte ditt namn!' },
|
||||
{ name: 'email', type: 'email', placeholder: 'Email', auto: 'email', error: 'Emailadressen är inte giltig.' },
|
||||
{ name: 'tel', type: 'tel', placeholder: 'Telefonnummer', auto: 'tel', error: 'Glöm inte ett telefonnummer!' },
|
||||
{ name: 'team', type: 'text', placeholder: 'Lagnamn', auto: 'organization', error: 'Glöm inte ert lagnamn!' },
|
||||
].map((f) => (
|
||||
<div key={f.name} className="relative w-full mb-3.75">
|
||||
<div className="text-center mb-10 px-2">
|
||||
<h2 className="font-beachday text-[clamp(35px,8vw,60px)] text-[#39979f] uppercase tracking-wide leading-[0.9] wrap-break-word w-full max-w-full">
|
||||
Anmälnings­formulär
|
||||
</h2>
|
||||
<div className="h-1.5 w-24 bg-[#fdb84b] mx-auto mt-4 rounded-full" />
|
||||
</div>
|
||||
|
||||
<form action={formAction} ref={formRef} className="space-y-6">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InputGroup
|
||||
icon={<FaUser />}
|
||||
name="name"
|
||||
placeholder="Ditt namn"
|
||||
required
|
||||
minLength={2}
|
||||
defaultValue={state?.values?.name || ''}
|
||||
/>
|
||||
<InputGroup
|
||||
icon={<FaEnvelope />}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="namn@domän.com"
|
||||
required
|
||||
defaultValue={state?.values?.email || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InputGroup
|
||||
icon={<FaPhone />}
|
||||
name="tel"
|
||||
type="tel"
|
||||
placeholder="Telefonnummer"
|
||||
required
|
||||
pattern="[0-9+ \-]{8,}"
|
||||
defaultValue={state?.values?.tel || ''}
|
||||
/>
|
||||
<InputGroup
|
||||
icon={<FaTrophy />}
|
||||
name="team"
|
||||
placeholder="Lagnamn"
|
||||
required
|
||||
minLength={2}
|
||||
defaultValue={state?.values?.team || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8 bg-white/50 p-6 rounded-3xl border border-[#e6be8a]/30 shadow-inner">
|
||||
<div className="flex-1">
|
||||
<label className="flex items-center gap-2 font-bold text-[#39979f] mb-3 text-sm uppercase">
|
||||
<FaUsers /> Antal spelare
|
||||
</label>
|
||||
<input
|
||||
type={f.type} name={f.name} placeholder={f.placeholder} required autoComplete={f.auto}
|
||||
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] transition-colors"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
name="count"
|
||||
required
|
||||
pattern="^([2-9]|10)$"
|
||||
title="Ange ett antal mellan 2 och 10"
|
||||
onInput={(e) => {
|
||||
e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '');
|
||||
}}
|
||||
className="w-full p-4 rounded-xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none transition-all shadow-inner"
|
||||
placeholder="Minst 2"
|
||||
defaultValue={state?.values?.count || ''}
|
||||
/>
|
||||
<div className="hidden group-[.was-validated]/form:peer-invalid:block absolute right-2.5 top-2.25"><PiWarningCircle color='#dc3545' size={28} /></div>
|
||||
<div className="hidden group-[.was-validated]/form:peer-valid:block absolute right-2.5 top-2.25"><PiCheckCircle color='#198754' size={28} /></div>
|
||||
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">{f.error}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="relative w-full mb-3.75">
|
||||
<input type="number" name="count" placeholder="Antal spelare" min="2" required
|
||||
className="peer bg-transparent text-white border-0 border-b-[5px] border-white w-full h-12.5 outline-none text-[20px] pl-2.5 rounded-none focus:ring-0 group-[.was-validated]/form:invalid:border-[#e60b20] group-[.was-validated]/form:valid:border-[#07ba4c] transition-colors" />
|
||||
<div className="hidden group-[.was-validated]/form:peer-invalid:block text-[#e60b20] text-[14px] pl-2.5 mt-1 text-left w-full">Glöm inte ange antal lagmedlemmar!</div>
|
||||
<div className="flex-2">
|
||||
<label className="font-bold text-[#39979f] mb-3 block text-sm uppercase">Välj klass</label>
|
||||
<div className={`grid grid-cols-2 md:grid-cols-${Math.min(classesData.length, 3)} gap-3`}>
|
||||
{classesData.map((c: any) => (
|
||||
<LevelButton
|
||||
key={c.name}
|
||||
value={c.name}
|
||||
label={c.name}
|
||||
desc={c.desc}
|
||||
color={c.color}
|
||||
defaultChecked={state?.values?.level === c.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RADIOS */}
|
||||
<div className="w-full mt-2.5 text-left group/radios">
|
||||
{[
|
||||
{ id: 'level-gron', val: 'Grön', label: 'Grön', color: 'text-green-700', accent: 'accent-green-600', desc: '(Nybörjare)' },
|
||||
{ id: 'level-bla', val: 'Blå', label: 'Blå', color: 'text-[#0f65bf]', accent: 'accent-[#0f65bf]', desc: '(Spelat lite)' },
|
||||
{ id: 'level-svart', val: 'Svart', label: 'Svart', color: 'text-black', accent: 'accent-black', desc: '(Spelat mycket)' },
|
||||
].map((l) => (
|
||||
<React.Fragment key={l.id}>
|
||||
<input type="radio" id={l.id} name="level" value={l.val} required className={`inline-block align-middle m-[6px_5px] w-4 h-4 ${l.accent} cursor-pointer`} />
|
||||
<label htmlFor={l.id} className="text-white inline-block align-middle cursor-pointer select-none">
|
||||
<strong className={`${l.color} bg-white px-0.75 rounded-sm font-bold`}>{l.label}</strong> {l.desc}
|
||||
</label>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div className="hidden group-[.was-validated]/form:group-has-invalid/radios:block text-[#e60b20] text-[14px] pl-2.5 mt-2">Glöm inte ange spelarnivå!</div>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Övrig info till oss..."
|
||||
className="w-full h-32 p-4 rounded-2xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none resize-none shadow-inner"
|
||||
defaultValue={state?.values?.message || ''}
|
||||
/>
|
||||
|
||||
<input type="text" name="website" className="hidden" tabIndex={-1} />
|
||||
|
||||
<div className="flex justify-center py-2">
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''}
|
||||
options={{ theme: 'light' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea name="message" autoComplete="off" placeholder="Övrig info..." className="w-full min-h-37.5 p-[12px_16px] border-[5px] border-white rounded-[10px] bg-transparent text-white text-[15px] resize-y outline-none mt-5" />
|
||||
|
||||
<div className="hidden">
|
||||
<input type="text" name="website" tabIndex={-1} autoComplete="off" />
|
||||
</div>
|
||||
|
||||
{alert.show && (
|
||||
<div className={`w-full p-4 rounded-md text-center font-bold my-4 ${alert.type === 'success' ? 'bg-[#d1e7dd] text-[#0a3622] border-[#a3cfbb]' : 'bg-[#f8d7da] text-[#58151c] border-[#f1aeb5]'}`}>
|
||||
{alert.message}
|
||||
{state && (
|
||||
<div className={`flex items-center gap-3 p-5 rounded-2xl font-bold transition-all ${state.success ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{state.success ? <FaCheckCircle size={24} className="shrink-0" /> : <FaExclamationTriangle size={24} className="shrink-0" />}
|
||||
{state.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full flex justify-center mt-2.5">
|
||||
<Turnstile ref={turnstileRef} siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''} options={{ theme: 'dark' }} />
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting} className="bg-white text-black uppercase w-full max-w-75 h-12.5 text-[20px] font-bold rounded-[10px] my-5 flex justify-center items-center cursor-pointer hover:bg-gray-200 transition-all active:scale-95 disabled:opacity-70">
|
||||
{isSubmitting && (
|
||||
<svg className="animate-spin h-5 w-5 text-black mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
)}
|
||||
<span>Skicka anmälan</span>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full py-5 bg-[#39979f] text-white rounded-2xl font-bold text-xl uppercase tracking-wide shadow-xl hover:bg-[#fdb84b] hover:text-[#39979f] transition-all active:scale-[0.98] disabled:bg-gray-300 disabled:text-gray-500"
|
||||
>
|
||||
{isPending ? 'Skickar...' : 'Skicka anmälan'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Uppdaterad med defaultChecked support
|
||||
function LevelButton({ value, label, desc, color, defaultChecked }: any) {
|
||||
return (
|
||||
<label className="cursor-pointer flex-1 relative" style={{ '--class-color': color } as React.CSSProperties}>
|
||||
<input
|
||||
type="radio"
|
||||
name="level"
|
||||
value={value}
|
||||
required
|
||||
className="peer sr-only"
|
||||
defaultChecked={defaultChecked}
|
||||
/>
|
||||
|
||||
<div className="text-center p-3 rounded-xl border-2 border-white bg-white text-gray-400 font-black transition-all peer-checked:text-white peer-checked:bg-(--class-color) peer-checked:border-(--class-color) peer-checked:scale-[1.03] shadow-sm h-full flex flex-col justify-center items-center">
|
||||
<span className="uppercase tracking-tighter">{label}</span>
|
||||
{desc && <span className="text-[10px] font-medium opacity-80 mt-1 uppercase tracking-widest">{desc.replace(/[()]/g, '')}</span>}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroup({ icon, ...props }: any) {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-[#e6be8a] group-focus-within:text-[#39979f] transition-colors">
|
||||
{icon}
|
||||
</div>
|
||||
<input
|
||||
{...props}
|
||||
className="w-full pl-12 pr-4 py-4 rounded-xl border-2 border-transparent bg-white focus:border-[#39979f] outline-none transition-all shadow-inner placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user