feat: wip on client form

This commit is contained in:
Stevan Freeborn
2025-12-21 06:51:21 -06:00
parent 08ef99ebd2
commit 9c7ce22ded
3 changed files with 218 additions and 1 deletions
+75
View File
@@ -0,0 +1,75 @@
header {
background-color: #181818;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 1rem;
display: flex;
box-shadow: 0 8px 12px #000000b0;
}
header img {
aspect-ratio: 1;
border: 2px solid #b39cd0;
border-radius: 0.25rem;
width: 12rem;
}
form {
background-color: #181818;
border-radius: 0.25rem;
flex-direction: column;
gap: 0.5rem;
width: 22.5rem;
padding: 1rem;
display: flex;
box-shadow: 0 4px 8px #0000001a;
}
form .group {
flex-direction: column;
gap: 0.5rem;
display: flex;
}
form label {
font-weight: 700;
}
form input {
color: #e4e4e4;
background-color: #282828;
border: 1px solid #444;
border-radius: 0.25rem;
padding: 0.5rem;
}
form input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
form input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
form input[type='number'] {
appearance: textfield;
}
.error-message {
color: #ff6b6b;
font-size: 0.875rem;
}
form button {
color: #fff;
background-color: #b39cd0;
border: none;
border-radius: 0.25rem;
padding: 0.75rem;
font-weight: 700;
transition: background-color 0.3s;
}
+111 -1
View File
@@ -1,8 +1,118 @@
import '@/App.css'; import '@/App.css';
import { useRef, useState } from 'react';
// TODO: Maybe be good to require
// the user provide some sort of
// identifier so we can display
// that as part of the feed
function App() { function App() {
const [amount, setAmount] = useState<number | ''>('');
const [email, setEmail] = useState<string>('');
const [errors, setErrors] = useState<{ amount?: string; email?: string }>({});
const amountInputRef = useRef<HTMLInputElement>(null);
const emailInputRef = useRef<HTMLInputElement>(null);
function handleAmountInput(event: React.ChangeEvent<HTMLInputElement>) {
errors.amount = undefined;
const value = parseInt(event.target.value);
setAmount(isNaN(value) ? '' : value);
}
function handleEmailInput(event: React.ChangeEvent<HTMLInputElement>) {
errors.email = undefined;
setEmail(event.target.value);
}
function validateForm() {
const newErrors: { amount?: string; email?: string } = {};
if (amount === '' || amount <= 0) {
newErrors.amount = 'Please enter a valid amount greater than 0.';
}
if (email.trim() !== '' && emailInputRef.current?.validity.typeMismatch) {
newErrors.email = 'Please enter a valid email address.';
}
setErrors(newErrors);
const isValid = Object.keys(newErrors).length === 0;
if (isValid === false) {
if (newErrors.amount) {
amountInputRef.current?.focus();
} else if (newErrors.email) {
emailInputRef.current?.focus();
}
}
return isValid;
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const isValid = validateForm();
if (isValid === false) {
return;
}
// TODO: We will stop displaying our form
// and we will initialize the stripe embedded
// elements and pass along the amount and email
alert(`Form is valid! Amount: ${amount}, Email: ${email}`);
}
return ( return (
<div>Hello World</div> <>
<header>
<img src="https://github.com/StevanFreeborn.png" alt="Logo" />
<p>Some information about me</p>
</header>
<main>
<form
onSubmit={handleSubmit}
noValidate
>
<div className='group'>
<label htmlFor='amount'>Amount</label>
<input
ref={amountInputRef}
id='amount'
type='number'
min={1}
aria-describedby='amountErrorMessage'
aria-invalid={errors.amount ? 'true' : 'false'}
value={amount}
onInput={handleAmountInput}
/>
<span className='error-message'>{errors.amount}</span>
</div>
<div className='group'>
<label htmlFor='email'>Email</label>
<input
ref={emailInputRef}
id='email'
type='email'
aria-describedby='emailErrorMessage'
aria-invalid={errors.email ? 'true' : 'false'}
value={email}
onInput={handleEmailInput}
/>
<span
id='emailErrorMessage'
className='error-message'
>
{errors.email}
</span>
</div>
<button type='submit'>Buy</button>
</form>
</main>
</>
); );
} }
@@ -1,3 +1,15 @@
@font-face {
font-family: 'CaskaydiaCove NFM';
src: url('/CaskaydiaCoveNFM-Regular.eot');
src: url('/CaskaydiaCoveNFM-Regular.eot?#iefix') format('embedded-opentype'),
url('/CaskaydiaCoveNFM-Regular.woff2') format('woff2'),
url('/CaskaydiaCoveNFM-Regular.woff') format('woff'),
url('/CaskaydiaCoveNFM-Regular.svg#CaskaydiaCoveNFM-Regular') format('svg');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* CSS Reset */ /* CSS Reset */
* { * {
@@ -16,6 +28,9 @@ button {
border: none; border: none;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
font-family: inherit;
font-size: inherit;
color: inherit;
} }
a { a {
@@ -40,6 +55,23 @@ table {
/* Base Styles */ /* Base Styles */
:root { :root {
--bg-color: #323232;
--text-color: #E4E4E4;
font-size: 16px;
font-family: 'CaskaydiaCove NFM', monospace;
font-size: 16px; font-size: 16px;
} }
html,
body {
height: 100%;
background-color: var(--bg-color);
color: var(--text-color);
}
main {
padding: 1rem;
padding-top: 2rem;
}