Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ let doubled = $derived(count * 2);

// Effects
$effect(() => {
console.log('Count changed:', count);
if (selectedType) {
formData.contribution_type = selectedType.id;
}
});
```

Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/src/components/ProfileCompletionGuard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@
width: 420px;
max-width: 90vw;
max-height: 85vh;
overflow: hidden;
overflow-y: auto;
scroll-behavior: smooth;
animation: slideUp 0.2s ease-out;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@
selectedType = t;
selectedMission = null;
selectedMissionData = null;
formData.contribution_type = t.id;
showTypeDropdown = false;
searchQuery = t.name;
if (error === "Please select a contribution type") error = "";
Expand All @@ -205,7 +204,6 @@
selectedType = item.parentType;
selectedMission = item.data.id;
selectedMissionData = item.data;
formData.contribution_type = item.parentType.id;
showTypeDropdown = false;
searchQuery = item.data.name;
if (error === "Please select a contribution type") error = "";
Expand Down Expand Up @@ -288,101 +286,102 @@

// Submission
async function handleSubmit(e) {
e.preventDefault();
e.preventDefault();

if (!formData.contribution_type) {
error = "Please select a contribution type";
// ✅ FIX: gunakan selectedType sebagai source of truth
if (!selectedType) {
error = "Please select a contribution type";
return;
}

if (formData.notes.length > 1000) {
error = "Notes cannot exceed 1000 characters";
return;
}

for (let i = 0; i < evidenceSlots.length; i++) {
const slot = evidenceSlots[i];
const hasDescription =
slot.description && slot.description.trim().length > 0;
const hasUrl = slot.url && slot.url.trim().length > 0;

if (hasDescription && !hasUrl) {
error = `Evidence ${i + 1}: A URL is required for each evidence item`;
return;
}

if (formData.notes.length > 1000) {
error = "Notes cannot exceed 1000 characters";
if (hasUrl && !hasDescription) {
error = `Evidence ${i + 1}: Please provide a description along with the URL`;
return;
}
}

for (let i = 0; i < evidenceSlots.length; i++) {
const slot = evidenceSlots[i];
const hasDescription =
slot.description && slot.description.trim().length > 0;
const hasUrl = slot.url && slot.url.trim().length > 0;
const filledSlots = evidenceSlots.filter(
(s) => s.description?.trim() && s.url?.trim(),
);

if (hasDescription && !hasUrl) {
error = `Evidence ${i + 1}: A URL is required for each evidence item`;
return;
}
if (hasUrl && !hasDescription) {
error = `Evidence ${i + 1}: Please provide a description along with the URL`;
return;
}
}
if (filledSlots.length === 0) {
error =
"Please add at least one evidence item with a URL to support your contribution";
return;
}

const filledSlots = evidenceSlots.filter(
(s) => s.description?.trim() && s.url?.trim(),
);
if (!recaptchaToken) {
error = "Please complete the reCAPTCHA verification";
return;
}

if (filledSlots.length === 0) {
error =
"Please add at least one evidence item with a URL to support your contribution";
return;
}
submitting = true;
error = "";

if (!recaptchaToken) {
error = "Please complete the reCAPTCHA verification";
return;
}
try {
const submissionData = {
// ✅ FIX: ambil dari selectedType
contribution_type: selectedType?.id,
contribution_date: formData.contribution_date + "T00:00:00Z",
notes: formData.notes,
recaptcha: recaptchaToken,
};

submitting = true;
error = "";
const missionToSubmit = selectedMission || missionId;
if (missionToSubmit) {
submissionData.mission = missionToSubmit;
}

try {
const submissionData = {
contribution_type: formData.contribution_type,
contribution_date: formData.contribution_date + "T00:00:00Z",
notes: formData.notes,
recaptcha: recaptchaToken,
};

// Include mission if selected (from URL param or dropdown selection)
const missionToSubmit = selectedMission || missionId;
if (missionToSubmit) {
submissionData.mission = missionToSubmit;
}
submissionData.evidence_items = filledSlots.map((slot) => ({
description: slot.description,
url: normalizeUrl(slot.url),
}));

// Send evidence inline with the submission (atomic creation)
submissionData.evidence_items = filledSlots.map((slot) => ({
description: slot.description,
url: normalizeUrl(slot.url),
}));
await api.post("/submissions/", submissionData);

await api.post("/submissions/", submissionData);
sessionStorage.setItem(
"submissionUpdateSuccess",
"Your contribution has been submitted successfully and is pending review.",
);

sessionStorage.setItem(
"submissionUpdateSuccess",
"Your contribution has been submitted successfully and is pending review.",
);
push("/my-submissions");
} catch (err) {
if (err.response?.data?.recaptcha) {
error = Array.isArray(err.response.data.recaptcha)
? err.response.data.recaptcha[0]
: err.response.data.recaptcha;
} else {
error =
err.response?.data?.error ||
err.response?.data?.detail ||
"Failed to submit contribution";
}
push("/my-submissions");
} catch (err) {
if (err.response?.data?.recaptcha) {
error = Array.isArray(err.response.data.recaptcha)
? err.response.data.recaptcha[0]
: err.response.data.recaptcha;
} else {
error =
err.response?.data?.error ||
err.response?.data?.detail ||
"Failed to submit contribution";
}

if (recaptchaWidgetId !== null && window.grecaptcha) {
try {
window.grecaptcha.reset(recaptchaWidgetId);
recaptchaToken = "";
} catch (e) {}
}
} finally {
submitting = false;
if (recaptchaWidgetId !== null && window.grecaptcha) {
try {
window.grecaptcha.reset(recaptchaWidgetId);
recaptchaToken = "";
} catch (e) {}
}
} finally {
submitting = false;
}
}

// Click outside listener for dropdown
let dropdownRef;
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/routes/ValidatorWaitlist.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
async function handleJoinWaitlist() {
if (!$authState.isAuthenticated) {
// Show connect wallet prompt
document.querySelector('.auth-button')?.click();
const btn = document.querySelector('.auth-button');
if (btn instanceof HTMLElement) {
btn.click();
}
return;
}

Expand All @@ -48,7 +51,7 @@
sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!');
push(`/participant/${$authState.address}`);
} catch (err) {
error = err.response?.data?.error || 'Failed to join waitlist';
error = 'Failed to join waitlist';
isJoiningWaitlist = false;
}
}
Expand All @@ -59,7 +62,12 @@
<div>
<h1 class="text-2xl font-bold text-gray-900">Validator Waitlist</h1>
<p class="mt-1 text-sm text-gray-500">
Join the waitlist to become a validator on GenLayer Testnets
Join the waitlist to become a validator on GenLayer Testnets
</p>

<p class="text-sm text-gray-500 mt-2">
Joining the validator waitlist does not guarantee immediate activation.
Validator slots are limited and approvals are processed gradually.
</p>
</div>

Expand Down