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:
Teriuihi 2024-01-13 15:53:47 +01:00
parent e5492401fd
commit 49a71097bc
20 changed files with 9554 additions and 4211 deletions

13068
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,19 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.69",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.6",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"react-helmet": "^6.1.0",
"react-scripts": "^5.0.1",
"typescript": "^5.3.3",
"web-vitals": "^3.5.1"
},
"scripts": {
"start": "react-scripts start",
@ -39,5 +40,9 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-helmet": "^6.1.11",
"react-router-dom": "^6.21.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,43 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<head>
<title>Survival Minecraft Server on 1.20.4! | Altitude Community</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description"
content="Start your adventure today! We are a 1.20.4 survival community with McMMO, MyPet, Dynmap, &amp; player shop based economy. Monthly server events. Best community in 1.20.4!">
<meta name="keywords"
content="minecraft,survival,server,servers,community,small,vanilla,semivanilla,dynmap,mcmmo,griefing,1.20.4,altitude,alttd,play,join,find,friends,friendly,simple,private,whitelist,whitelisted,creative,worldedit">
<meta name="author" content="Altitude Community">
<meta name="theme-color" content="#1f9bde">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- Twitter Card data -->
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="Community-centered Survival Minecraft Server"/>
<meta name="twitter:description"
content="Start your adventure today! We host 1.20.4 survival with features suggested by our community: McMMO, MyPet, Dynmap, &amp; economy. Monthly server events..."/>
<!-- Twitter Summary card images must be at least 120x120px -->
<meta name="twitter:image" content="https://www.alttd.com/assets/img/random/seo.jpg"/>
<!-- Open Graph data -->
<meta property="og:title" content="Community-centered 1.20.4 Survival Minecraft Server"/>
<meta property="og:type" content="article"/>
<meta property="og:url" content="https://www.alttd.com/"/>
<meta property="og:image" content="https://www.alttd.com/assets/img/random/seo.jpg"/>
<meta property="og:description"
content="Start your adventure today! We host 1.20.4 survival with features suggested by our community: McMMO, MyPet, Dynmap, &amp; economy. Monthly server events..."/>
<meta property="og:site_name" content="Altitude Community"/>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.png"/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</body>
</html>

BIN
public/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -24,15 +24,19 @@
color: white;
}
.App-link {
color: #61dafb;
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
.app-container {
display: flex;
flex-direction: column;
flex: 1 0 auto; /* This will make sure this container takes all available vertical space */
}
#root {
display: flex;
flex-grow: 1;
}

View File

@ -1,26 +1,26 @@
import React from 'react';
import logo from './logo.svg';
import ContactForm from "./components/contact_form/contact";
import './App.css';
import Home from "./components/home/home";
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import Footer from "./components/footer/footer";
import React from "react";
import VerifyMail from "./components/verify_email/verify_mail";
import ThankYou from "./components/verify_email/thank_you";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
return (
<div className="app-container">
<BrowserRouter>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/contact" element={<ContactForm/>}/>
<Route path="/verify-email" element={<VerifyMail/>}/>
<Route path="/thank-you" element={<ThankYou/>}/>
</Routes>
</BrowserRouter>
<Footer/>
</div>
);
}
export default App;
export default App;

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;
}

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 admin@alttd.com");
} 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 admin@alttd.com")
}
};
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;

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) {
}

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;

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;

View File

@ -0,0 +1,7 @@
.field {
margin-bottom: 20px
}
.header {
font-size: 20px;
}

View File

@ -0,0 +1,5 @@
form {
max-width: 600px;
margin: auto;
padding: 20px;
}

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;

View File

@ -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

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB