15 Commits
Author SHA1 Message Date
auto 8fda5d9fb9 Initial appeal commit 2024-10-06 00:27:29 +02:00
auto ee54e91051 Add event application form
Implemented a new event application form with validations and necessary fields such as username, email, and event experience. Incorporated the form into the home page with a navigation link and updated form interfaces and data configurations.
2024-09-20 18:01:38 +02:00
auto 1c4f4b154b Enhance email verification layout styling
Added CSS styles for the fields and field containers to improve the layout and alignment of the email verification page. This change ensures a more structured and visually appealing presentation of the form data.
2024-08-11 19:13:49 +02:00
auto 30720d0738 Refactor type and string handling in ThankYou component
Changed the `DynamicFormData` type to accept any data type, enhancing flexibility. Updated string handling to be more explicit with type checks, ensuring newline handling works correctly for strings only.
2024-08-11 19:09:31 +02:00
auto 25c6a15413 Handle single-line and multi-line email verification messages
Previously, multi-line messages were parsed for line breaks while single-line messages were not appropriately handled. This update checks if the value contains line breaks and processes it accordingly, ensuring both single-line and multi-line messages are displayed correctly.
2024-08-11 19:01:00 +02:00
auto 821e63953b Refactor email verification UI to enhance data display
Replaced table with div and paragraph elements to improve readability of user-submitted data. Added logic to handle multiline form data and remove empty lines, ensuring cleaner and more user-friendly presentation.
2024-08-11 18:47:19 +02:00
auto ceaca60ece Add additional_info field to form requirements
Include extra guidance for the user to specify their timezone. This aims to reduce ambiguity and ensure better form submissions. The change is minimal but improves overall user experience.
2024-08-11 18:41:56 +02:00
auto b00b857f29 Add "Apply for staff" link to homepage
This commit enhances the homepage by adding a new link to the "Apply for staff" page, making it easier for visitors to find and access staff application information directly from the main page. The new link is placed alongside the existing "Contact us" link for better visibility.
2024-08-11 18:32:57 +02:00
auto cf7bdeb481 Enforce integer validation on age and average time fields
Added integer validation messages to the age and avg_time fields to ensure users enter whole numbers. This enhances the form's data integrity by preventing input errors.
2024-08-10 04:35:54 +02:00
auto afb833bd19 Add useCallback to optimize handleCheckForm
Refactored handleCheckForm using useCallback to prevent unnecessary re-renders. This ensures the function is only re-created when formData.backendFormName changes, improving performance. Updated useEffect dependencies to include handleCheckForm for proper effect cleanup.
2024-08-10 03:27:28 +02:00
auto 6215944972 Fix missing promise handling in formActiveRedirect
Added a then() clause to handle the resolved promise after calling handleCheckForm in the useEffect. This ensures any asynchronous operations complete as expected and avoids potential unhandled promise rejections.
2024-08-10 03:24:46 +02:00
auto a08c55ec41 Remove GenericForm import from App.tsx
GenericForm was imported but never used in App.tsx. This clean-up helps in maintaining the codebase by removing unnecessary imports, thus enhancing readability and reducing potential confusion.
2024-08-10 03:24:07 +02:00
auto 2a56144bbc Enhance npm cache clean error handling in Jenkinsfile
Wrapped npm cache clean command in a try-catch block to improve error handling. This ensures that the build process can proceed even if the npm cache clean step fails. Added an informational echo message for better diagnostics in case of failure.
2024-08-10 03:21:16 +02:00
auto 98b86363f5 Add npm install fallback and remove unused variable
Added a fallback npm install command in the Jenkinsfile to handle legacy peer dependencies. Also removed an unused variable from genericForm.tsx to improve code clarity and performance.
2024-08-10 03:19:26 +02:00
auto 75559af7c8 Add form active status check and redirection
Introduced `FormActiveRedirect` component to check if forms are active before rendering. Updated form data to include `backendFormName` and adjusted routes in `App.tsx` to use the new component. This ensures that inactive forms redirect users with a relevant message.
2024-08-10 03:10:25 +02:00
14 changed files with 394 additions and 20 deletions
Vendored
+11 -1
View File
@@ -11,7 +11,11 @@ pipeline {
def success = false def success = false
// Clean npm cache and try normal install and npm ci once // Clean npm cache and try normal install and npm ci once
sh 'npm cache clean --force' try {
sh 'npm cache clean --force'
} catch (Exception e0) {
echo 'npm cache clean --force failed trying more things'
}
try { try {
sh 'npm install' sh 'npm install'
success = true success = true
@@ -39,6 +43,12 @@ pipeline {
echo "Retry ${retryCount}/${MAX_RETRIES} failed" echo "Retry ${retryCount}/${MAX_RETRIES} failed"
} }
} }
try {
sh 'npm install --legacy-peer-deps --force'
success = true
} catch (Exception e4) {
echo "npm install --legacy-peer-deps --force failed"
}
} }
if (!success) { if (!success) {
+4 -2
View File
@@ -6,9 +6,10 @@ import Footer from "./components/footer/footer";
import VerifyMail from "./components/verify_email/verify_mail"; import VerifyMail from "./components/verify_email/verify_mail";
import ThankYou from "./components/verify_email/thank_you"; import ThankYou from "./components/verify_email/thank_you";
import DEBUG from "./components/DEBUG/DEBUG"; import DEBUG from "./components/DEBUG/DEBUG";
import GenericForm from "./components/form/genericForm";
import {getFormProperties} from "./components/form/formData"; import {getFormProperties} from "./components/form/formData";
import {FormProperties} from "./components/form/formInterfaces"; import {FormProperties} from "./components/form/formInterfaces";
import FormActiveRedirect from "./components/form/formActiveRedirect";
import Appeal from "./components/appeal";
function App() { function App() {
return ( return (
@@ -21,11 +22,12 @@ function App() {
<Route <Route
key={property.path} key={property.path}
path={property.path} path={property.path}
element={<GenericForm {...property.formData} />} element={<FormActiveRedirect {...property.formData} />}
/> />
))} ))}
<Route path="/verify-email" element={<VerifyMail/>}/> <Route path="/verify-email" element={<VerifyMail/>}/>
<Route path="/thank-you" element={<ThankYou/>}/> <Route path="/thank-you" element={<ThankYou/>}/>
<Route path="/appeal" element={<Appeal/>}/>
{process.env.NODE_ENV === 'development' && <Route path="/debug" element={<DEBUG/>}/>} {process.env.NODE_ENV === 'development' && <Route path="/debug" element={<DEBUG/>}/>}
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
+21
View File
@@ -0,0 +1,21 @@
// you might need to adjust imports according to your project structure
import React, { FunctionComponent } from 'react';
import { Helmet } from 'react-helmet';
const Appeal: FunctionComponent = () => {
return (
<div>
<Helmet>
<title>Appeals Selection</title>
<meta name="Appeals selection page" content="Choose the type of appeal"/>
</Helmet>
<header className="App-header">
<h1>Welcome to the Appeals page</h1>
<h2><a href="/appeal/minecraft">Appeal a Minecraft punishment.</a></h2>
<h2><a href="/appeal/discord">Appeal a Discord punishment.</a></h2>
</header>
</div>
);
}
export default Appeal;
+104
View File
@@ -0,0 +1,104 @@
import {FormData} from "../formInterfaces";
import * as Yup from "yup";
type PunishmentsForUser = {
punishments: string[]
};
type UserForPunishments = {
username: string
};
export const minecraft_appeal: FormData = {
steps: [
{
label: "What is your Minecraft username?",
additional_info: "Use the username you had when you last tried to join the server.",
name: "username",
type: "text",
min_length: 3,
max_length: 16,
required: true,
},
{
label: "What is your email?",
additional_info: "It does not have to be your minecraft email.",
name: "email",
type: "email",
min_length: 3,
max_length: 254,
required: true,
},
{
label: "What punishment would you like to appeal?",
additional_info: "Please select it below.",
name: "punishment",
type: "dropdown",
min_length: 10,
max_length: 2000,
required: true,
drop_down: [],
processInput: (input: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
const userForPunishments: UserForPunishments = {
username: input,
}
fetch(`${process.env.REACT_APP_BACKEND_BASE_URL}/api/appeal/retrieve-minecraft-punishments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userForPunishments)
})
.then(response => {
if (!response.ok) {
console.log(response)
reject(new Error('Invalid username'));
}
return response.json()
})
.then((punishmentsForUser: PunishmentsForUser) => {
resolve(punishmentsForUser.punishments);
})
.catch(error => {
console.error('Received an unexpected error: ' + error);
reject(error);
})
});
}
},
{
label: "Why should your punishment be reduced or removed?",
additional_info: "Please take your time writing this, we're more likely to accept an appeal if effort was put into it.",
name: "appeal",
type: "textarea",
min_length: 2,
max_length: 2000,
required: true,
},
],
//TODO warning if no punishments
backend: `${process.env.REACT_APP_BACKEND_BASE_URL}/api/appeal/minecraft`,
userInput: {username: '', email: '', punishment: '', appeal: ''},
spec: Yup.object().shape({
username: Yup.string()
.min(3, 'Username should be at least 3 characters')
.max(16, 'Username should not exceed 16 characters')
.matches(/^[a-zA-Z0-9_]*$/, 'Username should only include alphanumeric characters and underscore')
.required('Username is required'),
email: Yup.string()
.email('Invalid email')
.min(3, 'Email should be at least 3 characters')
.max(254, 'Email should not exceed 254 characters')
.required('Email is required'),
punishment: Yup.string()
.required('You are required to select a punishment'),
appeal: Yup.string()
.min(3, 'Your appeal needs to be at least 100 characters')
.max(2000, 'Your appeal can not be longer than 2000 characters')
.required()
}),
title: "Minecraft Appeal",
backendFormName: "MinecraftAppeal",
};
+5 -1
View File
@@ -89,6 +89,7 @@ export const apply: FormData = {
min_length: 3, min_length: 3,
max_length: 256, max_length: 256,
required: true, required: true,
additional_info: "Please include your timezone."
}, },
{ {
label: "Do you have any previous experience being staff?", label: "Do you have any previous experience being staff?",
@@ -163,6 +164,7 @@ export const apply: FormData = {
age: Yup.number() age: Yup.number()
.typeError('Input must be a number') .typeError('Input must be a number')
.integer('Please enter a whole number')
.min(0, 'Please enter a valid age') .min(0, 'Please enter a valid age')
.max(999, 'We do not accept players older than 999 years old sorry!') .max(999, 'We do not accept players older than 999 years old sorry!')
.required('You are required to fill out your age'), .required('You are required to fill out your age'),
@@ -175,6 +177,7 @@ export const apply: FormData = {
avg_time: Yup.number() avg_time: Yup.number()
.typeError('Please enter a number') .typeError('Please enter a number')
.integer('Please enter a whole number')
.min(0, 'Please enter a positive number') .min(0, 'Please enter a positive number')
.max(168, 'There are only 168 hours in a week') .max(168, 'There are only 168 hours in a week')
.required('Please enter the average time you will be available each week'), .required('Please enter the average time you will be available each week'),
@@ -213,5 +216,6 @@ export const apply: FormData = {
other: Yup.string() other: Yup.string()
.max(2000, 'Please provide at most 2000 characters') .max(2000, 'Please provide at most 2000 characters')
}), }),
title: "Staff Application" title: "Staff Application",
backendFormName: "StaffApplication",
}; };
+2 -1
View File
@@ -48,5 +48,6 @@ export const contact: FormData = {
.max(2000, 'Question should not exceed 2000 characters') .max(2000, 'Question should not exceed 2000 characters')
.required('Question is required') .required('Question is required')
}), }),
title: "Contact Form" title: "Contact Form",
backendFormName: "ContactForm",
}; };
+140
View File
@@ -0,0 +1,140 @@
import {FormData} from "../formInterfaces";
import * as Yup from "yup";
export const event_apply: FormData = {
steps: [
{
label: "What is your Minecraft username?",
name: "username",
type: "text",
min_length: 3,
max_length: 16,
required: true,
},
{
label: "What is your email?",
name: "email",
type: "email",
min_length: 3,
max_length: 254,
required: true,
},
{
label: "What is your Discord username?",
name: "discord",
type: "text",
min_length: 2,
max_length: 32,
required: true,
},
{
label: "What is your age?",
name: "age",
type: "text",
min_length: 1,
max_length: 3,
required: true,
},
{
label: "What is your preferred pronoun?",
name: "pronoun",
type: "dropdown",
min_length: 0,
max_length: 16,
required: true,
drop_down: ["They/them", "She/her", "He/him", ""],
},
{
label: "On average, how many hours per week are you available to help with events?",
name: "avg_time",
type: "text",
min_length: 1,
max_length: 2,
required: true,
},
{
label: "Do you have any previous experience running events of any kind?",
name: "event_experience",
type: "textarea",
min_length: 2,
max_length: 2000,
required: true,
},
{
label: "Are you able to use voice chat on Discord",
name: "discord_vc",
type: "dropdown",
min_length: 2,
max_length: 3,
required: true,
additional_info: "Use voice chat in Discord with reasonable audio quality\n" +
"Take screenshots through your pc (normally f2 in minecraft)\n" +
"Record your screen through your pc at 20fps+ and 720p+ at normal Minecraft settings (with free programs like OBS)\n",
drop_down: ["Yes", "No", ""],
},
{
label: "You may share anything else you wish here.",
name: "other",
type: "textarea",
min_length: 2,
max_length: 2000,
required: false,
},
],
backend: `${process.env.REACT_APP_BACKEND_BASE_URL}/api/apply/staffApplication`,
userInput: {
username: '', email: '', discord: ''
, age: '', pronoun: '', avg_time: '', event_experience: ''
, discord_vc: '', other: ''
},
spec: Yup.object().shape({
username: Yup.string()
.min(3, 'Username should be at least 3 characters')
.max(16, 'Username should not exceed 16 characters')
.matches(/^[a-zA-Z0-9_]*$/, 'Username should only include alphanumeric characters and underscore')
.required('Please enter your username'),
email: Yup.string()
.email('Invalid email')
.min(3, 'Email should be at least 3 characters')
.max(254, 'Email should not exceed 254 characters')
.required('Please enter your email'),
discord: Yup.string()
.min(2, 'Discord name should be at least 2 characters')
.max(32, 'Discord name should be at most 32 characters')
.matches(/^(?!.*\.\.)([a-z0-9._]{2,32})$/, 'Please enter a valid Discord name')
.required('Please enter your Discord name'),
age: Yup.number()
.typeError('Input must be a number')
.integer('Please enter a whole number')
.min(0, 'Please enter a valid age')
.max(999, 'We do not accept players older than 999 years old sorry!')
.required('You are required to fill out your age'),
pronoun: Yup.string()
.max(16, 'Pronouns can\'t be longer than 16 characters'),
avg_time: Yup.number()
.typeError('Please enter a number')
.integer('Please enter a whole number')
.min(0, 'Please enter a positive number')
.max(168, 'There are only 168 hours in a week')
.required('Please enter the average time you will be available each week'),
event_experience: Yup.string()
.min(2, 'Please provide your experience with running/working on events, if you don\'t have any you can say that instead')
.max(2000, 'Please provide at most 2000 characters')
.required('Please enter your event experience or simply say you don\'t have any'),
discord_vc: Yup.string()
.matches(/(yes|no)$/i, 'Yes or no')
.required('An answer is required'),
other: Yup.string()
.max(2000, 'Please provide at most 2000 characters')
}),
title: "Event Application",
backendFormName: "EventApplication",
};
@@ -0,0 +1,39 @@
import {FormData} from "./formInterfaces";
import {useEffect, useState, useCallback} from "react";
import GenericForm from "./genericForm";
const FormActiveRedirect = (formData: FormData) => {
const [isLoading, setLoading] = useState(true);
const [isFormActive, setFormActive] = useState(false);
const handleCheckForm = useCallback(async () => {
setFormActive(true);
setLoading(false);
// const result = await fetch(`${process.env.REACT_APP_BACKEND_BASE_URL}/api/checks/formActive`, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ formName: formData.backendFormName })
// });
// const response = await result.json();
// setFormActive(response.isActive);
// setLoading(false);
}, [formData.backendFormName]);
useEffect(() => {
handleCheckForm().then(() => {});
}, [handleCheckForm]);
if (isLoading) {
return <div className={"container"}><h2>Checking if form is active</h2></div>;
}
if (!isFormActive) {
return <div className={"container"}><h2>The {formData.title} is not currently active</h2></div>;
}
return (
<GenericForm {...formData} />
);
}
export default FormActiveRedirect;
+10
View File
@@ -1,6 +1,8 @@
import {FormProperties} from "./formInterfaces"; import {FormProperties} from "./formInterfaces";
import {contact} from "./data/contact"; import {contact} from "./data/contact";
import {apply} from "./data/apply"; import {apply} from "./data/apply";
import {event_apply} from "./data/event_apply";
import {minecraft_appeal} from "./data/appeal";
const formProperties: FormProperties[] = [ const formProperties: FormProperties[] = [
{ {
@@ -11,6 +13,14 @@ const formProperties: FormProperties[] = [
path: 'apply', path: 'apply',
formData: apply formData: apply
}, },
{
path: 'event-apply',
formData: event_apply
},
{
path: 'appeal/minecraft',
formData: minecraft_appeal
},
] ]
export function getFormProperties(): FormProperties[] { export function getFormProperties(): FormProperties[] {
+23 -1
View File
@@ -1,4 +1,4 @@
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import './GenericForm.css'; import './GenericForm.css';
import {Field} from "formik"; import {Field} from "formik";
import {FormHandlerProps} from "./formInterfaces"; import {FormHandlerProps} from "./formInterfaces";
@@ -22,6 +22,22 @@ const FormHTML: React.FC<FormHandlerProps> = ({
const [selectedOptions, setSelectedOptions] = useState(fieldValues); const [selectedOptions, setSelectedOptions] = useState(fieldValues);
const [options, setOptions] = useState<string[]>([]);
useEffect(() => {
const { processInput, drop_down = [] } = steps[currentStep];
if (processInput) {
processInput(values['username']).then(newOptions => {
setOptions(newOptions);
//TODO be an unsetter for some error field
});
} else {
setOptions(drop_down);
console.log("No data") //TODO be a setter for some error field
}
}, [currentStep, steps]);
if (currentField.type === 'select') { if (currentField.type === 'select') {
return ( return (
<ReactSelect <ReactSelect
@@ -65,6 +81,12 @@ const FormHTML: React.FC<FormHandlerProps> = ({
{option} {option}
</option> </option>
))} ))}
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
{options.length === 0 ? "<p>No data found, please check if your username is valid. Or if this was a discord punishment, please use the discord appeal form</p>":"<p></p>"}
</Field> </Field>
); );
} else { } else {
+3 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
type InputNames = "username" | "email" | "question" | "discord" | "pc_requirements" | "age" | "pronoun" | "join_date" | type InputNames = "username" | "email" | "question" | "discord" | "pc_requirements" | "age" | "pronoun" | "join_date" |
"avg_time" | "available_days" | "available_time" | "staff_experience" | "plugin_experience" | "why_staff" | "avg_time" | "available_days" | "available_time" | "staff_experience" | "plugin_experience" | "why_staff" |
"expectations_mod" | "other"; "expectations_mod" | "event_experience" | "discord_vc" | "other" | "punishment" | "appeal";
export interface Step { export interface Step {
label: string; label: string;
@@ -15,6 +15,7 @@ export interface Step {
additional_info?: string; additional_info?: string;
drop_down?: string[] drop_down?: string[]
multiple?: boolean multiple?: boolean
processInput?: (input: string) => Promise<string[]>;
} }
export interface UserInput { export interface UserInput {
@@ -22,6 +23,7 @@ export interface UserInput {
} }
export type FormData = { export type FormData = {
backendFormName: string;
steps: Step[]; steps: Step[];
backend: string; backend: string;
userInput: UserInput; userInput: UserInput;
+3
View File
@@ -11,6 +11,9 @@ const Home: FunctionComponent = () => {
<header className="App-header"> <header className="App-header">
<h1>Welcome to the Altitude forms page</h1> <h1>Welcome to the Altitude forms page</h1>
<h2><a href="/contact">Contact us.</a></h2> <h2><a href="/contact">Contact us.</a></h2>
<h2><a href="/apply">Apply for staff.</a></h2>
<h2><a href="/event-apply">Apply for events.</a></h2>
<h2><a href="/appeal">Appeal a punishment.</a></h2>
</header> </header>
</div> </div>
); );
+12
View File
@@ -38,3 +38,15 @@ table {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
} }
.fields {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 1020px;
width: 80%;
}
.field {
margin-bottom: 20px;
}
+17 -13
View File
@@ -6,7 +6,7 @@ import './ThankYou.css';
const ThankYou: FC = () => { const ThankYou: FC = () => {
const location = useLocation(); const location = useLocation();
type DynamicFormData = { type DynamicFormData = {
[key: string]: string; [key: string]: any;
}; };
if (location.state === null || location.state.formData === undefined) { if (location.state === null || location.state.formData === undefined) {
@@ -26,19 +26,23 @@ const ThankYou: FC = () => {
</Helmet> </Helmet>
<header className="header">Thank you for completing the form and verifying your email!<br></br>This is the data you entered:</header> <header className="header">Thank you for completing the form and verifying your email!<br></br>This is the data you entered:</header>
<div className="fields"> <div className="fields">
<table> {Object.entries(formData).map(([key, value]) => (
<tbody> <div className={"field"}>
{Object.entries(formData).map(([key, value]) => ( <p><strong>{key}</strong></p>
<tr className="form-data-row" id={key}> {
<td className="form-data-key">{key}</td> typeof value === "string" && value.includes("\n") ?
<td className="form-data-value">{value}</td> value
</tr> .split("\n")
))} .filter(str => str.trim() !== "")
</tbody> .map((str, index) => <p style={{ wordWrap: 'break-word' }} key={index}>{str}</p>)
</table> : <p style={{ wordWrap: 'break-word' }}>{value}</p>
}
</div>
))}
</div> </div>
</div> </div>
); )
;
} }