This commit is contained in:
metacryst
2025-09-06 20:26:07 -05:00
parent 7194b48b14
commit eff0c160a5
47 changed files with 2470 additions and 254 deletions

View File

@@ -0,0 +1,19 @@
css(`
nav-bar {
position: fixed;
top: 0px;
left: 0px;
}
`)
export default class NavBar extends HTMLElement {
connectedCallback() {
this.innerHTML += /* html */ `
`
}
}
customElements.define("nav-bar", NavBar)

View File

@@ -0,0 +1,581 @@
css(`
questionnaire- {
display: grid;
grid-template-columns: 250px 1fr;
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
margin-top: 17vh;
}
.toc {
position: sticky;
top: 17vh;
align-self: start;
background: var(--tan);
padding: 10px;
border: 1px solid var(--brown);
border-radius: 5px;
}
.toc h2 {
margin-top: 0;
font-size: 1.1em;
}
.toc a {
display: block;
margin: 5px 0;
text-decoration: none;
color: var(--green);
}
.toc a.active {
font-weight: bold;
color: var(--red);
}
section {
border: 1px solid var(--brown);
margin-bottom: 2vh;
padding: 15px;
border-radius: 5px;
background: none;
user-select: none;
transition: background 0.2s, border 0.2s, color 0.2s;
}
.section-content {
scroll-margin-top: 20px;
padding-bottom: 40px;
border-bottom: 1px solid #ccc;
}
input:focus,
textarea:focus,
select:focus {
outline: 1px solid #ddbb36;
border: 1px solid #c4a52f
}
/* Normal state */
form button {
transition: all 0.3s ease;
padding: 8px 16px;
border-radius: 4px;
}
/* Loading: morph into circle + pulse */
form button.loading {
width: 40px;
height: 40px;
border-radius: 50%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
animation: pulse 1s infinite;
}
/* Success state */
button.success {
background: #28a745;
color: white;
border-radius: 4px;
width: auto;
animation: none;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
`)
export default class Questionnaire extends HTMLElement {
constructor() {
super();
this.pages = [
{ title: "Demographic Information", slug: "demographic", content: this.demographicContent() },
{ title: "Contact Information", slug: "contact", content: this.contactContent() },
{ title: "Personal History", slug: "personal-history", content: this.personalHistoryContent() },
{ title: "Ancestry", slug: "ancestry", content: this.ancestryContent() },
{ title: "Skills", slug: "skills", content: this.skillsContent() },
{ title: "Interest", slug: "interest", content: this.interestContent() },
{ title: "Criminal Record", slug: "criminal", content: this.criminalContent() },
{ title: "Political Perspectives", slug: "political", content: this.politicalContent() },
{ title: "Religious/Philosophical Views", slug: "religious", content: this.religiousContent() },
{ title: "Federal Employment", slug: "federal-employment", content: this.federalEmploymentContent() },
{ title: "Social Perspectives", slug: "social", content: this.socialContent() },
];
this.pageHTML = {};
}
connectedCallback() {
this.fetchData();
}
async fetchData() {
try {
const res = await fetch("/api/get-application");
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
this.data = await res.json();
console.log(this.data)
} catch (err) {
console.error("Failed to fetch questionnaire:", err);
this.data = {};
}
this.render()
}
render() {
this.Layout();
this.Forms();
this.setupScrollSpy();
this.populateFormData();
this.markSectionsComplete();
}
Layout() {
this.innerHTML = /* html */`
<nav class="toc">
<h2>Contents</h2>
${this.pages.map(p => `
<a href="#${p.slug}" data-slug="${p.slug}">${p.title}</a>
`).join("")}
<a href="#submit">Submit</a>
</nav>
<div class="sections">
${this.pages.map(p => /* html */`
<section id="${p.slug}" class="section-content">
<h1 style="margin-left: 3rem; margin-top: 3rem">${p.title}</h1>
<div class="form-container">
<form></form>
</div>
</section>
`).join("")}
<section id="submit" class="section-content">
<h1 style="margin-left: 3rem; margin-top: 3rem">Submit Application</h1>
<button style="margin-left: 3rem; margin-top: 3rem; background-color: gray" disabled>Submit</button>
</section>
</div>
`;
}
Forms() {
this.pages.forEach(page => {
const form = this.querySelector(`#${page.slug} form`);
if (form) form.innerHTML = page.content;
form.innerHTML += `<button type="submit">Save</button>`
form.addEventListener("submit", this.onFormSubmit)
});
}
populateFormData() {
Object.entries(this.data).forEach(([key, value]) => {
const fields = document.querySelectorAll(`[name="${key}"]`);
if (!fields.length) return;
fields.forEach(field => {
if (field.type === "checkbox" || field.type === "radio") {
if (Array.isArray(value)) {
field.checked = value.includes(field.value);
} else {
field.checked = field.value === value;
}
} else if (field.tagName === "SELECT" && field.multiple && Array.isArray(value)) {
Array.from(field.options).forEach(opt => {
opt.selected = value.includes(opt.value);
});
} else {
field.value = value;
}
});
});
}
markSectionsComplete() {
let allFormsComplete = true
let forms = document.querySelectorAll("form")
for(let i =0; i<forms.length; i++) {
let form = forms[i]
if(form.checkValidity()) {
form.closest("section").style.backgroundColor = "#0080003d"
let toc = document.querySelector(`[data-slug=${form.closest("section").id}]`)
if(!toc.innerHTML.includes("✓"))
toc.innerHTML += " ✓"
} else {
allFormsComplete = false
}
}
if(allFormsComplete) {
let button = document.querySelector("section#submit button")
button.style.backgroundColor = ""
button.removeAttribute("disabled")
button.addEventListener("click", () => window.location.href = "/complete")
}
}
setupScrollSpy() {
const tocLinks = this.querySelectorAll(".toc a");
const sections = this.querySelectorAll(".section-content");
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
history.replaceState(null, "", `#${id}`);
tocLinks.forEach(link => {
link.classList.toggle("active", link.dataset.slug === id);
});
}
});
}, {
rootMargin: "-50% 0px -50% 0px", // middle of screen
threshold: 0
});
sections.forEach(section => observer.observe(section));
}
onFormSubmit = (e) => {
e.preventDefault();
let form = e.target
let button = form.querySelector('button[type="submit"]');
if (!form.checkValidity()) {
// Let the browser display native validation errors
return;
}
const showButtonLoading = () => {
button.classList.add('loading');
button.disabled = true;
button.textContent = '';
}
const showButtonError = () => {
button.classList.remove('loading');
button.disabled = false;
button.textContent = 'Save';
if(!form.querySelector("p .error")) {
form.parentElement.appendChild(html(`<p class="error" style="color: red">There has been an error. Please try again.</p>`))
}
}
const showButtonSuccess = () => {
if(form.parentElement.querySelector("p")) {
form.parentElement.querySelector("p").remove()
}
button.classList.remove('loading');
button.classList.add('success');
button.textContent = 'Success!';
console.log(this)
this.markSectionsComplete()
}
showButtonLoading()
// Collect form data
const data = new FormData(form);
const values = {};
data.forEach((v, k) => {
if (values[k]) {
// Already an array? push new value
if (Array.isArray(values[k])) {
values[k].push(v);
} else {
values[k] = [values[k], v];
}
} else {
values[k] = v;
}
});
fetch('/api/application-save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values)
})
.then(res => {
if (!res.ok) {
throw new Error(`Server error: ${res.status}`);
}
return res.text();
})
.then(responseData => {
console.log(form, form.parentElement)
showButtonSuccess()
console.log('Server response:', responseData);
})
.catch(err => {
showButtonError()
console.error('Submission error:', err);
});
console.log("Form submitted:", values);
}
demographicContent() {
return /* html */`
<label for="firstName">First Name *</label>
<input type="text" name="firstName" class="form-control" required>
<br><br>
<label for="lastName">Last Name *</label>
<input type="text" name="lastName" class="form-control" required>
<br><br>
<label for="age">Age *</label>
<input type="text" name="age" class="form-control" required>
<br><br>
<label for="gender">Gender *</label>
<select name="gender" class="form-control" required>
<option value="">Select...</option>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
<br><br>
<label for="ethnicity">Type of Application *</label>
<select name="applicationType" class="form-control" required>
<option value="">Select...</option>
<option value="single">Single</option>
<option value="couple">Couple</option>
<option value="family">Family</option>
</select>
<br><br>
<label for="sexualOrientation">Sexual Orientation *</label>
<select name="sexualOrientation" class="form-control" required>
<option value="">Select...</option>
<option value="straight">Straight</option>
<option value="gay">Gay</option>
<option value="bisexual">Bisexual</option>
<option value="other">Other</option>
</select>
<br><br>
`;
}
contactContent() {
return /* html */`
<div class="form-group">
<label for="telegram">Telegram Username *</label>
<input type="text" name="telegram" class="form-control" required>
<small style="display:block;margin-top:.25rem;color:#666;">A Telegram account is required to be interviewed by the PMA</small>
</div>
<div class="form-group">
<label for="email-2">Secondary Email</label>
<input type="email" name="email-2" class="form-control">
<small style="display:block;margin-top:.25rem;color:#666;">Not required - you may add a secondary email in case our messages don't reach you</small>
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" name="phone" class="form-control">
<small style="display:block;margin-top:.25rem;color:#666;">Not required, but recommended if you don't provide a Telegram handle.</small>
</div>
`;
}
personalHistoryContent() {
return /* html */`
<div class="form-group">
<label for="personalHistory">Where did you come from, where did you grow up, and where do you live now? *</label>
<textarea name="personalHistory" class="form-control" required minlength="10"></textarea>
</div>
`;
}
ancestryContent() {
return /* html */`
<div class="form-group">
<p>Please select all that apply to your ancestry/heritage:</p>
<div class="ancestry-grid">
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="english"> English</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="irish"> Irish</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="scottish"> Scottish</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="welsh"> Welsh</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="german"> German</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="french"> French</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="dutch"> Dutch</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="scandinavian"> Scandinavian</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="slavic"> Slavic</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="balkan"> Balkan</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="iberian"> Iberian</label>
<label class="ancestry-option"><input type="checkbox" name="ancestry" value="other">Other</label>
</div>
</div>
<div class="form-group">
<label for="ancestryDetails">Tell us more about your ancestry and family history *</label>
<textarea name="ancestryDetails" class="form-control" required minlength="20" placeholder="Please provide details about your family background, heritage, and any ancestral connections you're aware of."></textarea>
</div>
`;
}
skillsContent() {
return /* html */`
<div class="form-group">
<label for="skills">Describe any special skills or trades you have. *</label>
<textarea name="skills" class="form-control" required minlength="50" placeholder="e.g., carpentry, plumbing, electrical, masonry, welding"></textarea>
</div>
`;
}
interestContent() {
return /* html */`
<div class="form-group">
<label for="interest">Why are you interested in Return To The Land, and how did you find out about us? *</label>
<textarea name="interest" class="form-control" required minlength="20"></textarea>
</div>
`;
}
criminalContent() {
return /* html */`
<label for="criminal">Do you have a criminal record? If so, please explain. Otherwise, just say "no". *</label>
<textarea name="criminal" class="form-control" required></textarea>
<br><br>
`;
}
politicalContent() {
return /* html */`
<label for="politicsNotes">Give a quick summary of your political ideas. *</label>
<textarea name="politicsNotes" class="form-control" style="width:100%;min-height:120px" required minlength="40"></textarea>
<br><br>
`;
}
religiousContent() {
return /* html */`
<div class="form-group">
<label>Do you identify with a particular religion? *</label>
<div class="radio-row">
<label><input type="radio" name="religionIdent" value="yes" required> Yes</label>
<label><input type="radio" name="religionIdent" value="no"> No</label>
</div>
</div>
<div class="form-group">
<label for="religionName">If yes, which?</label>
<input name="religionName" class="form-control" placeholder="e.g., Catholic, Orthodox, Protestant, Deist, Other">
</div>
<div class="form-group">
<label>Do you regularly attend services or observances?</label>
<div class="radio-row">
<label><input type="radio" name="attendance" value="weekly"> Weekly</label>
<label><input type="radio" name="attendance" value="monthly"> Monthly</label>
<label><input type="radio" name="attendance" value="occasionally"> Occasionally</label>
<label><input type="radio" name="attendance" value="never"> Never</label>
</div>
</div>
<div class="form-group">
<label for="philosophy">Briefly describe your moral or philosophical outlook *</label>
<textarea name="philosophy" class="form-control" required></textarea>
</div>
`;
}
federalEmploymentContent() {
return /* html */`
<div class="form-group">
<label>Are you currently employed by, or otherwise receive funds to perform work for, the United States federal government or a contractor/affiliate/partner? *</label>
<div class="radio-row">
<label><input type="radio" name="employedByGovernment" value="yes" required> Yes</label>
<label><input type="radio" name="employedByGovernment" value="no"> No</label>
</div>
</div>
`;
}
socialContent() {
return /* html */`
<p class="note">Please answer these questions regarding controversial topics. Anything that needs further explanation can be done in the interview.</p>
<br>
<div class="form-group">
<label>I support gay marriage *</label>
<div class="radio-row">
<label><input type="radio" name="gays" value="yes" required> Yes</label>
<label><input type="radio" name="gays" value="no"> No</label>
<label><input type="radio" name="gays" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support transgenderism *</label>
<div class="radio-row">
<label><input type="radio" name="trannies" value="yes" required> Yes</label>
<label><input type="radio" name="trannies" value="no"> No</label>
<label><input type="radio" name="trannies" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support foreign immigration *</label>
<div class="radio-row">
<label><input type="radio" name="immigration" value="yes" required> Yes</label>
<label><input type="radio" name="immigration" value="no"> No</label>
<label><input type="radio" name="immigration" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support the COVID vaccine and mask *</label>
<div class="radio-row">
<label><input type="radio" name="vax" value="yes" required> Yes</label>
<label><input type="radio" name="vax" value="no"> No</label>
<label><input type="radio" name="vax" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support abortion *</label>
<div class="form-group">
<label><input type="radio" name="abortion" value="yes" required> Yes</label>
<label><input type="radio" name="abortion" value="no"> No</label>
<label><input type="radio" name="abortion" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support segregation *</label>
<div class="radio-row">
<label><input type="radio" name="segregation" value="yes" required> Yes</label>
<label><input type="radio" name="segregation" value="no"> No</label>
<label><input type="radio" name="segregation" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>I support procreation *</label>
<div class="radio-row">
<label><input type="radio" name="procreation" value="yes" required> Yes</label>
<label><input type="radio" name="procreation" value="no"> No</label>
<label><input type="radio" name="procreation" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>Multiculturalism preference *</label>
<div class="radio-row">
<label style="display:block;margin:.35rem 0"><input type="radio" name="multiculturalism" value="yes" required> I prefer a community with a variety of different ancestral origins</label>
<label style="display:block;margin:.35rem 0"><input type="radio" name="multiculturalism" value="no"> I prefer a community where everyone shares common continental ancestry</label>
<label style="display:block;margin:.35rem 0"><input type="radio" name="multiculturalism" value="neutral"> Neutral</label>
</div>
</div>
<div class="form-group">
<label>How often do you think about the Roman empire? *</label>
<div class="radio-row">
<label style="display:block;margin:.35rem 0"><input type="radio" name="romanEmpire" value="daily" required> Every day, at least once</label>
<label style="display:block;margin:.35rem 0"><input type="radio" name="romanEmpire" value="weekly"> A few times a week, probably</label>
<label style="display:block;margin:.35rem 0"><input type="radio" name="romanEmpire" value="rarely"> Very rarely</label>
<label style="display:block;margin:.35rem 0"><input type="radio" name="romanEmpire" value="never"> Never</label>
</div>
</div>
`;
}
}
customElements.define("questionnaire-", Questionnaire);