Create site with contact form and email verification

This commit removes the redundant logo.svg file. It then adds several new components including 'footer', 'contact_form', 'home', 'verify_email' in forms/src/components directory. It also includes associated CSS files for styling these components. Updates have also been made in the index.html file for SEO metadata. Changes made aim to enhance functionality and improve user interface.
This commit is contained in:
2024-01-13 15:53:47 +01:00
parent e5492401fd
commit 49a71097bc
20 changed files with 9554 additions and 4211 deletions
+53
View File
@@ -0,0 +1,53 @@
.container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
form {
max-width: 600px;
margin: auto;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
label {
display: block;
margin-bottom: 20px;
}
input[type="text"],
input[type="email"],
textarea {
width: 100%;
padding: 10px;
font-size: 1em;
border-radius: 5px;
border: 1px solid #ccc;
}
button,
input[type="submit"] {
padding: 10px 20px;
font-size: 1em;
border-radius: 5px;
border: none;
color: #fff;
background-color: #007BFF;
cursor: pointer;
}
button[disabled],
input[type="submit"][disabled] {
background-color: #ccc;
}
#footer{
padding-bottom: 0 !important;
}
+161
View File
@@ -0,0 +1,161 @@
import React, {ChangeEvent, useState} from "react";
import './Contact.css';
import { useNavigate } from 'react-router-dom'
type InputNames = "username" | "email" | "question";
interface Step {
label: string;
name: InputNames;
type: "text" | "email" | "textarea";
min_length: number;
max_length: number;
required: boolean;
pattern: string;
}
type FormDataType = Record<InputNames, string>;
const ContactForm = () => {
const navigate = useNavigate()
const [formData, setFormData] = useState<FormDataType>({
username: "",
email: "",
question: ""
});
const [currentStep, setCurrentStep] = useState<number>(0);
const [errorMessage, setErrorMessage] = useState<string>("");
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
const { name, value } = e.target;
setErrorMessage("");
switch (name) {
case 'username':
if (/^[a-zA-Z0-9_]*$/.test(value)) {
setFormData(prevState => ({ ...prevState, [name]: value }));
} else {
setErrorMessage('Invalid character entered. Please use only alphanumeric characters and underscores.');
}
break;
default:
setFormData(prevState => ({ ...prevState, [name]: value }));
}
}
const steps: Step[] = [
{
label: "Username",
name: "username",
type: "text",
min_length: 3,
max_length: 16,
required: true,
pattern: ""
},
{
label: "Email",
name: "email",
type: "email",
min_length: 3,
max_length: 254,
required: false,
pattern: ""
},
{
label: "Question",
name: "question",
type: "textarea",
min_length: 10,
max_length: 2000,
required: true,
pattern: ""
},
]
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:8080/api/contact/submitContactForm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
if (!response.ok) {
alert("Your form submission was denied by the server, or the server was unable to process it, if you didn't mess with the data please contact the administrator at [email protected]");
} else {
navigate('/verify-email', {
state: {
email: formData.email
}
});
}
} catch (e) {
alert("Your form submission was denied by the server, if you didn't mess with the data please contact the administrator at [email protected]")
}
};
const next = () => {
setCurrentStep(current => current + 1)
}
const prev = () => {
setCurrentStep(current => Math.max(current - 1, 0))
setErrorMessage('');
}
function isNextDisabled(): boolean {
let step: Step = steps[currentStep];
if (!step)
return true;
if (!step.required)
return false;
return formData[step.name].length === 0
}
function getInputField(step: Step): JSX.Element {
switch (step.type) {
case "text":
case "email":
return <input pattern={step.pattern} type={step.type} name={step.name} minLength={step.min_length}
maxLength={step.max_length} onChange={handleChange} value={formData[step.name]} required/>
case "textarea":
return <textarea name={step.name} minLength={step.min_length} maxLength={step.max_length}
onChange={handleChange} value={formData.question} required/>
}
}
return (
<div className="container">
<div>
<h1>Contact Form</h1>
</div>
{errorMessage && <p>{errorMessage}</p>}
<div>
<form onSubmit={handleSubmit}>
<div>
<label>
{steps[currentStep].label}
{getInputField(steps[currentStep])}
</label>
</div>
<button type="button" onClick={prev} disabled={currentStep === 0}>
Previous
</button>
<button type="button" onClick={next} disabled={currentStep === steps.length - 1 || isNextDisabled()}>
Next
</button>
<input type="submit" value="Submit" disabled={currentStep !== steps.length - 1}
hidden={currentStep !== steps.length - 1}/>
</form>
</div>
</div>
);
};
export default ContactForm;
+122
View File
@@ -0,0 +1,122 @@
footer {
background-color: var(--footer-color);
transition: 0.5s ease;
}
#footer {
padding: 80px 0;
width: 80%;
max-width: 1020px;
margin: auto;
}
#footerinner {
display: flex;
justify-content: space-between;
}
#footertext {
flex-basis: 45%;
margin-right: 60px;
}
.footernav {
flex-grow: 1;
border-left: 1px solid rgb(19, 19, 19);
padding-left: 10px;
}
.footernav li {
padding-bottom: 5px;
}
footer ul, footer p {
list-style: none;
font-size: 0.9em;
color: rgb(168, 168, 168);
font-family: 'opensans',sans-serif;
}
footer h2 {
margin-bottom: 10px;
font-size: 1.3em;
color: var(--white);
}
#copyright {
margin-top: 50px;
}
.followus {
padding-top: 15px;
}
.followus img {
margin-right: 5px;
float: left;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
transition: all 0.2s;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
}
.followus img:hover {
margin-bottom: 5px;
filter: grayscale(0%);
-webkit-filter: grayscale(0%);
-moz-filter: grayscale(0%);
transform: scale(1.1);
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
}
@media (max-width: 1150px) {
#footerinner {
flex-wrap: wrap;
text-align: center;
}
#footertext {
flex: 1 1 100%;
margin-right: 0;
margin-bottom: 30px;
}
.footernav {
border-left: none;
padding-left: 0px;
padding-bottom: 15px;
min-width: 200px;
}
.followus {
width: 100px;
margin: 0 auto;
}
.footernav {
border-left: none;
padding-left: 0px;
padding-bottom: 15px;
min-width: 200px;
}
#copyright {
margin-top: 30px;
text-align: center;
}
}
@media (max-width: 1000px) {
}
@media (max-width: 690px) {
}
@media (min-width: 690px) {
}
+57
View File
@@ -0,0 +1,57 @@
import React, { FC } from 'react';
import './Footer.css';
const Footer: FC = () => {
const version: string = "1.20.4"; // replace with your version
return (
<footer>
<div id="footer">
<div id="footerinner">
<div id="footertext">
<h2>ABOUT US</h2>
<p>Altitude is a community-centered {version} version survival server. We're one of those
servers you come to call "home". We are your place to get together with friends and play
survival, with a few extra features suggested by our community!</p>
<div className="followus">
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
<img src="https://alttd.com//assets/img/logos/discord.png" alt="Discord Button"/>
</a>
<a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
<img src="https://alttd.com//assets/img/logos/twitter.png" alt="Twitter Button"/>
</a>
<a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">
<img src="https://alttd.com/assets/img/logos/instagram.png" alt="Instagram Button"/>
</a>
</div>
</div>
<div className="footernav">
<h2>COMMUNITY</h2>
<ul>
<li><a target="_blank" rel="noopener"
href="https://discordapp.com/invite/TGqpzCJ">Discord</a></li>
<li><a target="_blank" rel="noopener" href="https://alttd.com/blog">Blog</a></li>
<li><a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">Twitter</a></li>
<li><a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">Instagram</a>
</li>
<li><a target="_blank" rel="noopener" href="https://reddit.com/r/alttd">Reddit</a></li>
</ul>
</div>
<div className="footernav">
<h2>SERVER</h2>
<ul>
<li><a href="https://alttd.com/about">About Us</a></li>
<li><a href="https://alttd.com/team">Staffing Team</a></li>
<li><a href="https://alttd.com/policy">Privacy Policy</a></li>
<li><a href="https://alttd.com/terms">Terms of Use</a></li>
</ul>
</div>
</div>
<p id="copyright">Copyright © 2015-2023 Altitude. All rights Reserved. Not affiliated with Mojang AB or
Microsoft.</p>
</div>
</footer>
)
};
export default Footer;
+19
View File
@@ -0,0 +1,19 @@
import React, { FunctionComponent } from 'react';
import { Helmet } from 'react-helmet';
const Home: FunctionComponent = () => {
return (
<div>
<Helmet>
<title>Forms</title>
<meta name="Forms home page" content="The home page for all Altitude forms"/>
</Helmet>
<header className="App-header">
<p>Welcome to the Altitude forms page</p>
<a href="/contact">Contact us.</a>
</header>
</div>
);
}
export default Home;
+7
View File
@@ -0,0 +1,7 @@
.field {
margin-bottom: 20px
}
.header {
font-size: 20px;
}
@@ -0,0 +1,5 @@
form {
max-width: 600px;
margin: auto;
padding: 20px;
}
+37
View File
@@ -0,0 +1,37 @@
import {FC, useState} from "react";
import {useLocation} from "react-router-dom";
const ThankYou: FC = () => {
const location = useLocation();
const [code, setCode] = useState('000000');
type DynamicFormData = {
[key: string]: string;
};
if (location.state === null || location.state.formData === undefined) {
return (
<div className="container">
<p>Are you in the right place? It doesn't look like you completed a form or an email verification!</p>
</div>
)
}
const formData: DynamicFormData = location.state.formData;
return (
<div className="container">
<p>Thank you for completing the form and verifying your email! This is the data you entered:</p>
<div>
{Object.entries(formData).map(([key, value]) => (
<div className="field">
<p className="header">{key}</p>
<p>{value}</p>
</div>
))}
</div>
</div>
);
}
export default ThankYou;
@@ -0,0 +1,83 @@
import {FC, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
interface VerificationData {
eMail: string;
code: string;
}
const VerifyMail: FC = () => {
const navigate = useNavigate()
const location = useLocation();
const [code, setCode] = useState('000000');
if (location.state === null || location.state.email === undefined) {
return (
<div className="container">
<p>Are you in the right place? It doesn't look like you have an email to verify!</p>
</div>
)
}
const email = location.state.email;
function setFormData(data: DynamicFormData) {
console.log("Setting form data")
navigate('/thank-you', {
state: {
formData: data
}
});
}
type DynamicFormData = {
[key: string]: string;
};
const handleCodeSubmit = async () => {
const verificationData: VerificationData = {
code: code,
eMail: email
}
console.log(verificationData);
fetch('http://localhost:8080/api/verify_email/form', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(verificationData)
})
.then(response => {
if (!response.ok) {
//TODO fix error and make message here?
response.json().then(result => console.log(result))
throw new Error('Invalid code');
}
//TODO check if its json if not its just text we need to handle and put in a p tag
return response.json()
})
.then((data: DynamicFormData) => {
setFormData(data);
})
.catch(error => {
console.error('Received an unexpected error: ' + error);
})
}
return (
<div className="container">
<p>Hi, you just completed a form and need to verify your email ({email}).</p>
<p>Please check your email for a verification code and enter it below:</p>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
className="verification-code-input"
/>
<button onClick={handleCodeSubmit} className="submit-button">Submit Code</button>
</div>
)
}
export default VerifyMail