From 163fc9945cccc9ee4fda819fd4f6204692470a85 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Tue, 25 Feb 2025 10:37:28 +0100
Subject: [PATCH 1/8] Add new Form component to Proposal form

---
 locale/cnr/translation.toml                   |   9 +
 locale/de/translation.toml                    |   9 +
 locale/en/translation.toml                    |   9 +
 locale/es-mx/translation.toml                 |   9 +
 locale/fr/translation.toml                    |   9 +
 locale/hi/translation.toml                    |   9 +
 locale/it/translation.toml                    |   9 +
 locale/nl/translation.toml                    |   9 +
 locale/ph/translation.toml                    |   9 +
 locale/pl/translation.toml                    |   9 +
 locale/pt-br/translation.toml                 |   9 +
 locale/pt-pt/translation.toml                 |   9 +
 locale/template/translation.toml              |   9 +
 locale/uwu/translation.toml                   |   9 +
 scripts/form/Form.vue                         |  48 ++++
 scripts/form/Input.vue                        | 102 +++++++
 scripts/form/NumericInput.vue                 |  38 +++
 scripts/governance/ProposalCreateModal.vue    | 252 ++++++++++--------
 .../governance/ProposalCreateModal.spec.js    |  32 ++-
 19 files changed, 480 insertions(+), 118 deletions(-)
 create mode 100644 scripts/form/Form.vue
 create mode 100644 scripts/form/Input.vue
 create mode 100644 scripts/form/NumericInput.vue

diff --git a/locale/cnr/translation.toml b/locale/cnr/translation.toml
index 561a216e6..020374ba1 100644
--- a/locale/cnr/translation.toml
+++ b/locale/cnr/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Interna greška, molimo pokušajte ponovo kasnije" # Internal error, please try again later
diff --git a/locale/de/translation.toml b/locale/de/translation.toml
index 44407ab80..50640e862 100644
--- a/locale/de/translation.toml
+++ b/locale/de/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Interner Fehler, bitte versuche es später erneut" # Internal error, please try again later
diff --git a/locale/en/translation.toml b/locale/en/translation.toml
index 33a01e76c..d757e354e 100644
--- a/locale/en/translation.toml
+++ b/locale/en/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "Creating SHIELD transaction..." # Creating SHIELD t
 deleteProposal = "Delete Proposal" # Delete Proposal
 deleteProposalBody = "Are you sure you want to delete your proposal? Your collateral will be permanently lost." # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "Part of your staked balance may be locked for 100 blocks as your reward matures." # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "Maximum length allowed: {length}" # Maximum length allowed: {length}
+formValidationMinLength = "Minimum length allowed: {length}" # Minimum length allowed: {length}
+formValidationOther = "Form validation error" # Form validation error
+formValidationString = "The string contains invalid characters" # The string contains invalid characters
+formValidationUrl = "The URL is invalid" # The URL is invalid
+formValidationAddress = "The address provided is invalid" # The address provided is invalid
+formValidationMin = "The minimum value is: {value}" # The minimum value is: {value}
+formValidationMax = "The maximum value is: {value}" # The maximum value is: {value}
+formValidationInvalidNumber = "Invalid Number" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, please try again later" # Internal error, please try again later
diff --git a/locale/es-mx/translation.toml b/locale/es-mx/translation.toml
index 3b0f178f4..b68e61c6c 100644
--- a/locale/es-mx/translation.toml
+++ b/locale/es-mx/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Error interno, vuelve a intentarlo más tarde" # Internal error, please try again later
diff --git a/locale/fr/translation.toml b/locale/fr/translation.toml
index e39ee4690..c51d05c44 100644
--- a/locale/fr/translation.toml
+++ b/locale/fr/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Erreur interne, veuillez réessayer plus tard" # Internal error, please try again later
diff --git a/locale/hi/translation.toml b/locale/hi/translation.toml
index 31eb6b829..762f7eb52 100644
--- a/locale/hi/translation.toml
+++ b/locale/hi/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "क्रिएटिंग शील्ड ट्
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "आंतरिक त्रुटि, कृपया कुछ समय बाद पुनः प्रयास करें" # Internal error, please try again later
diff --git a/locale/it/translation.toml b/locale/it/translation.toml
index fa2fca9d5..72a29c519 100644
--- a/locale/it/translation.toml
+++ b/locale/it/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Errore interno, rirova più tardi" # Internal error, please try again later
diff --git a/locale/nl/translation.toml b/locale/nl/translation.toml
index 48084ec04..3a44635ef 100644
--- a/locale/nl/translation.toml
+++ b/locale/nl/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Interne fout, probeer het later opnieuw" # Internal error, please try again later
diff --git a/locale/ph/translation.toml b/locale/ph/translation.toml
index b317d4b1d..4189e7fcc 100644
--- a/locale/ph/translation.toml
+++ b/locale/ph/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, Pakiusap uliting muli" # Internal error, please try again later
diff --git a/locale/pl/translation.toml b/locale/pl/translation.toml
index cb7aaff4c..7bb85efef 100644
--- a/locale/pl/translation.toml
+++ b/locale/pl/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Błąd wewnętrzny, spróbuj ponownie później" # Internal error, please try again later
diff --git a/locale/pt-br/translation.toml b/locale/pt-br/translation.toml
index b5a31527e..28cf35c7e 100644
--- a/locale/pt-br/translation.toml
+++ b/locale/pt-br/translation.toml
@@ -87,6 +87,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro este endereço irá ser usado." # <b>Cold Address set!</b><br>Future stakes will use this address.
diff --git a/locale/pt-pt/translation.toml b/locale/pt-pt/translation.toml
index aa5fc1df1..21ede79bd 100644
--- a/locale/pt-pt/translation.toml
+++ b/locale/pt-pt/translation.toml
@@ -87,6 +87,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro irá ser usado este endereço." # <b>Cold Address set!</b><br>Future stakes will use this address.
diff --git a/locale/template/translation.toml b/locale/template/translation.toml
index c3fc3ebf1..43dc12e07 100644
--- a/locale/template/translation.toml
+++ b/locale/template/translation.toml
@@ -235,6 +235,15 @@ creatingShieldTransaction = "Creating SHIELD transaction..."
 deleteProposal = "Delete Proposal"
 deleteProposalBody = "Are you sure you want to delete your proposal? Your collateral will be permanently lost."
 immatureRewards = "Part of your staked balance may be locked for 100 blocks as your reward matures."
+formValidationMaxLength = "Maximum length allowed: {length}"
+formValidationMinLength = "Minimum length allowed: {length}"
+formValidationMin = "The minimum value is: {value}"
+formValidationMax = "The maximum value is: {value}"
+formValidationOther = "Form validation error"
+formValidationString = "The string contains invalid characters"
+formValidationUrl = "The URL is invalid"
+formValidationAddress = "The address provided is invalid"
+formValidationInvalidNumber = "Invalid Number"
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, please try again later"
diff --git a/locale/uwu/translation.toml b/locale/uwu/translation.toml
index d37966328..1b13f31f4 100644
--- a/locale/uwu/translation.toml
+++ b/locale/uwu/translation.toml
@@ -211,6 +211,15 @@ creatingShieldTransaction = "" # Creating SHIELD transaction...
 deleteProposal = "" # Delete Proposal
 deleteProposalBody = "" # Are you sure you want to delete your proposal? Your collateral will be permanently lost.
 immatureRewards = "" # Part of your staked balance may be locked for 100 blocks as your reward matures.
+formValidationMaxLength = "" # Maximum length allowed: {length}
+formValidationMinLength = "" # Minimum length allowed: {length}
+formValidationOther = "" # Form validation error
+formValidationString = "" # The string contains invalid characters
+formValidationUrl = "" # The URL is invalid
+formValidationAddress = "" # The address provided is invalid
+formValidationMin = "" # The minimum value is: {value}
+formValidationMax = "" # The maximum value is: {value}
+formValidationInvalidNumber = "" # Invalid Number
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, pwease try again later" # Internal error, please try again later
diff --git a/scripts/form/Form.vue b/scripts/form/Form.vue
new file mode 100644
index 000000000..e404dc26a
--- /dev/null
+++ b/scripts/form/Form.vue
@@ -0,0 +1,48 @@
+<script setup>
+import { provide, reactive, ref } from 'vue';
+import { translation } from '../i18n.js';
+
+const props = defineProps({
+    validationFunction: Function,
+});
+const formData = reactive({});
+const error = ref('');
+provide('formData', formData);
+
+const emit = defineEmits(['submit']);
+const submitForm = () => {
+    const res = {};
+    error.value = '';
+    for (const key in formData) {
+        // true is the only success
+        // A string rapresents are error, and is relayed to the user
+        if (formData[key].validationFunction() !== true) return;
+        res[key] = formData[key].value;
+    }
+    if (props.validationFunction) {
+        const validation = props.validationFunction(res);
+        if (validation !== true) {
+            error.value = validation;
+            return;
+        }
+    }
+    emit('submit', res);
+};
+</script>
+
+<template>
+    <form @submit.prevent="submitForm">
+        {{ error }}
+        <slot> </slot>
+        <slot name="button" :onSubmit="() => submitForm()">
+            <button
+                type="button"
+                class="pivx-button-big"
+                style="float: right"
+                @click="submitForm()"
+            >
+                {{ translation.popupConfirm }}
+            </button>
+        </slot>
+    </form>
+</template>
diff --git a/scripts/form/Input.vue b/scripts/form/Input.vue
new file mode 100644
index 000000000..a33de1fb8
--- /dev/null
+++ b/scripts/form/Input.vue
@@ -0,0 +1,102 @@
+<script setup>
+import { ref, inject, watch, toRaw } from 'vue';
+import { tr, translation } from '../i18n.js';
+
+const props = defineProps({
+    name: String,
+    validationFunction: Function,
+    maxLength: Number,
+    minLength: Number,
+    placeholder: String,
+    type: String,
+    max: Number,
+    min: Number,
+    disabled: Boolean,
+    'data-testid': String,
+});
+
+const value = ref('');
+const error = ref('');
+const validationFunction = () => {
+    if (props.maxLength && value.value.length > props.maxLength)
+        return tr(translation.formValidationMaxLength, [
+            { length: props.maxLength },
+        ]);
+    if (props.minLength && value.value.length < props.minLength)
+        return tr(translation.formValidationMinLength, [
+            { length: props.minLength },
+        ]);
+    if (props.validationFunction) return props.validationFunction(value.value);
+    return true;
+};
+/**
+ * @type{import('vue').Reactive<{[key: string]: string}>}
+ */
+const formData = inject('formData');
+watch(
+    [
+        value,
+        () => props.validationFunction,
+        () => props.name,
+        () => props.disabled,
+    ],
+    () => {
+        if (!props.name) return;
+        if (props.disabled) {
+            delete formData[props.name];
+            return;
+        }
+        error.value = '';
+
+        formData[props.name] = {
+            value: value.value,
+            validationFunction: () => {
+                const res = validationFunction();
+                if (res !== true) error.value = res;
+                return res;
+            },
+        };
+    },
+    { immediate: true }
+);
+
+watch(
+    () => props.disabled,
+    () => {
+        if (props.disabled) {
+            value.value = '';
+            error.value = '';
+        }
+    }
+);
+watch(
+    () => props['data-testid'],
+    () => console.log(props['data-testid'])
+);
+</script>
+
+<template>
+    <input
+        :class="{ 'input-error': error?.length }"
+        v-model="value"
+        :placeholder="placeholder"
+        :type="props.type"
+        :max="props.max"
+        :min="props.min"
+        :disabled="props.disabled"
+        :data-testid="props['dataTestid']"
+    />
+    <div class="validation-summary">
+        {{ error }}
+    </div>
+</template>
+
+<style>
+.input-error {
+    border: 2px solid #ff1d1d;
+    margin-bottom: 0;
+}
+.validation-summary {
+    color: #ff1d1d;
+}
+</style>
diff --git a/scripts/form/NumericInput.vue b/scripts/form/NumericInput.vue
new file mode 100644
index 000000000..baf6f3544
--- /dev/null
+++ b/scripts/form/NumericInput.vue
@@ -0,0 +1,38 @@
+<script setup>
+import { tr, translation } from '../i18n.js';
+import Input from './Input.vue';
+
+const props = defineProps([
+    'name',
+    'validationFunction',
+    'max',
+    'min',
+    'placeholder',
+    'dataTestid',
+]);
+
+const validationFunction = (value) => {
+    if (!Number.isInteger(value))
+        return translation.formValidationInvalidNumber;
+    if (props.max && value > props.max)
+        return tr(translation.formValidationMax, [{ value: props.max }]);
+    if (props.min && value < props.min)
+        return tr(translation.formValidationMin, [{ value: props.min }]);
+    if (props.validationFunction) return props.validationFunction(value);
+    return true;
+};
+</script>
+
+<template>
+    {{ error }}
+    <Input
+        type="number"
+        v-model="value"
+        :name="name"
+        :placeholder="placeholder"
+        :max="props.max"
+        :min="props.min"
+        :validation-function="validationFunction"
+        :data-testid="props.dataTestid"
+    />
+</template>
diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index 3b7a5fa17..70790954d 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -1,30 +1,31 @@
 <script setup>
 import Modal from '../Modal.vue';
+import Form from '../form/Form.vue';
+import Input from '../form/Input.vue';
+import NumericInput from '../form/NumericInput.vue';
 import { translation } from '../i18n.js';
 import { COIN, cChainParams } from '../chain_params';
-import { toRefs, ref, watch } from 'vue';
+import { toRefs } from 'vue';
+import { isStandardAddress } from '../misc';
 
 const props = defineProps({
     advancedMode: Boolean,
+    isTest: Boolean,
 });
-const name = ref('');
-const url = ref('');
-const payments = ref(0);
-const monthlyPayment = ref('');
-const address = ref('');
 const { advancedMode } = toRefs(props);
 const emit = defineEmits(['close', 'create']);
-function submit() {
+function submit(data) {
     emit(
         'create',
-        name.value,
-        url.value,
-        payments.value,
-        monthlyPayment.value,
-        address.value
+        data.proposalTitle,
+        data.proposalUrl,
+        data.proposalCycles,
+        data.proposalPayment,
+        data.proposalAddress
     );
 }
-watch(advancedMode, () => (address.value = ''));
+
+const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
 </script>
 
 <template>
@@ -46,88 +47,78 @@ watch(advancedMode, () => (address.value = ''));
             >
         </template>
         <template #body>
-            <div style="padding-left: 10px; padding-right: 10px">
-                <p
-                    style="
-                        margin-bottom: 12px;
-                        color: #af9cc6;
-                        font-size: 1rem;
-                        font-weight: 500;
-                    "
-                >
-                    Proposal name
-                </p>
-                <input
-                    data-testid="proposalTitle"
-                    maxlength="20"
-                    :placeholder="translation.popupProposalName"
-                    style="text-align: start; margin-bottom: 25px"
-                    v-model="name"
-                /><br />
-
-                <p
-                    style="
-                        margin-bottom: 12px;
-                        color: #af9cc6;
-                        font-size: 1rem;
-                        font-weight: 500;
-                    "
-                >
-                    URL
-                </p>
-                <input
-                    data-testid="proposalUrl"
-                    maxlength="64"
-                    placeholder="https://forum.pivx.org/..."
-                    style="margin-bottom: 25px; text-align: start"
-                    v-model="url"
-                /><br />
-
-                <p
-                    style="
-                        margin-bottom: 12px;
-                        color: #af9cc6;
-                        font-size: 1rem;
-                        font-weight: 500;
-                    "
-                >
-                    Duration in cycles
-                </p>
-                <input
-                    type="number"
-                    data-testid="proposalCycles"
-                    min="1"
-                    :max="cChainParams.current.maxPaymentCycles"
-                    :placeholder="translation.popupProposalDuration"
-                    style="margin-bottom: 25px; text-align: start"
-                    v-model="payments"
-                /><br />
+            <Form @submit="submit">
+                <template #default>
+                    <p
+                        style="
+                            margin-bottom: 12px;
+                            color: #af9cc6;
+                            font-size: 1rem;
+                            font-weight: 500;
+                        "
+                    >
+                        Proposal name
+                    </p>
+                    <Input
+                        name="proposalTitle"
+                        data-testid="proposalTitle"
+                        :max-length="20"
+                        :placeholder="translation.popupProposalName"
+                        :validation-function="
+                            (value) => {
+                                if (!isSafeStr.test(value))
+                                    return translation.formValidationString;
+                                return true;
+                            }
+                        "
+                    />
+                    <p
+                        style="
+                            margin-bottom: 12px;
+                            color: #af9cc6;
+                            font-size: 1rem;
+                            font-weight: 500;
+                        "
+                    >
+                        URL
+                    </p>
+                    <Input
+                        name="proposalUrl"
+                        data-testid="proposalUrl"
+                        :max-length="64"
+                        placeholder="https://forum.pivx.org/..."
+                        :validation-function="
+                            (value) => {
+                                if (!isSafeStr.test(value))
+                                    return translation.formValidationString;
+                                if (
+                                    !/^(https?):\/\/[^\s/$.?#][^\s]*[^\s/.]\.[^\s/.][^\s]*[^\s.]$/.test(
+                                        value
+                                    )
+                                )
+                                    return translation.formValidationUrl;
+                                return true;
+                            }
+                        "
+                    />
+                    <p
+                        style="
+                            margin-bottom: 12px;
+                            color: #af9cc6;
+                            font-size: 1rem;
+                            font-weight: 500;
+                        "
+                    >
+                        Duration in cycles
+                    </p>
+                    <NumericInput
+                        name="proposalCycles"
+                        data-testid="proposalCycles"
+                        :min="1"
+                        :max="cChainParams.current.maxPaymentCycles"
+                        :placeholder="translation.popupProposalDuration"
+                    />
 
-                <p
-                    style="
-                        margin-bottom: 12px;
-                        color: #af9cc6;
-                        font-size: 1rem;
-                        font-weight: 500;
-                    "
-                >
-                    {{ cChainParams.current.TICKER }} per cycle
-                </p>
-                <input
-                    type="number"
-                    data-testid="proposalPayment"
-                    min="10"
-                    :max="cChainParams.current.maxPayment / COIN"
-                    :placeholder="
-                        cChainParams.current.TICKER +
-                        ' ' +
-                        translation.popupProposalPerCycle
-                    "
-                    style="margin-bottom: 25px; text-align: start"
-                    v-model="monthlyPayment"
-                />
-                <br v-if="!advancedMode" />
-                <span v-if="advancedMode">
                     <p
                         style="
                             margin-bottom: 12px;
@@ -136,19 +127,65 @@ watch(advancedMode, () => (address.value = ''));
                             font-weight: 500;
                         "
                     >
-                        Proposal Address
+                        {{ cChainParams.current.TICKER }} per cycle
                     </p>
-                    <input
-                        data-testid="proposalAddress"
-                        maxlength="34"
-                        :placeholder="translation.popupProposalAddress"
-                        style="margin-bottom: 25px; text-align: start"
-                        v-model="address"
+                    <NumericInput
+                        name="proposalPayment"
+                        data-testid="proposalPayment"
+                        min="10"
+                        :max="cChainParams.current.maxPayment / COIN"
+                        :placeholder="`${cChainParams.current.TICKER} ${translation.popupProposalPerCycle}`"
                     />
-                </span>
-            </div>
+                    <span v-show="advancedMode">
+                        <p
+                            style="
+                                margin-bottom: 12px;
+                                color: #af9cc6;
+                                font-size: 1rem;
+                                font-weight: 500;
+                            "
+                        >
+                            Proposal Address
+                        </p>
+                        <Input
+                            name="proposalAddress"
+                            data-testid="proposalAddress"
+                            :validation-function="
+                                (value) => {
+                                    if (
+                                        value.length !== 0 &&
+                                        !isStandardAddress(value)
+                                    )
+                                        return translation.formValidationAddress;
+                                    return true;
+                                }
+                            "
+                            :disabled="!advancedMode"
+                        />
+                    </span>
+                </template>
+
+                <template #button="slotProps">
+                    <Teleport
+                        :disabled="props.isTest"
+                        defer
+                        to=".create-proposal-button-container"
+                    >
+                        <button
+                            type="button"
+                            class="pivx-button-big"
+                            style="float: right"
+                            data-testid="proposalSubmit"
+                            @click="slotProps.onSubmit()"
+                        >
+                            {{ translation.popupConfirm }}
+                        </button>
+                    </Teleport>
+                </template>
+            </Form>
         </template>
         <template #footer>
+            <div class="create-proposal-button-container"></div>
             <button
                 type="button"
                 class="pivx-button-big-cancel"
@@ -158,15 +195,6 @@ watch(advancedMode, () => (address.value = ''));
             >
                 {{ translation.popupCancel }}
             </button>
-            <button
-                type="button"
-                class="pivx-button-big"
-                style="float: right"
-                data-testid="proposalSubmit"
-                @click="submit()"
-            >
-                {{ translation.popupConfirm }}
-            </button>
         </template>
     </Modal>
 </template>
diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js
index d820501ca..d5f5ba7fc 100644
--- a/tests/components/governance/ProposalCreateModal.spec.js
+++ b/tests/components/governance/ProposalCreateModal.spec.js
@@ -1,23 +1,40 @@
 import { describe, it, expect, vi } from 'vitest';
 import { mount } from '@vue/test-utils';
 import ProposalCreateModal from '../../../scripts/governance/ProposalCreateModal.vue';
+import { vi } from 'vitest';
+import { defineComponent, h } from 'vue';
+
+vi.stubGlobal(
+    'Teleport',
+    defineComponent({
+        render() {
+            return h('div', this.$slots.default ? this.$slots.default() : []);
+        },
+    })
+);
 
 describe('ProposalCreateModal component tests', () => {
     it('hides address input when advanced mode is false', async () => {
         const wrapper = mount(ProposalCreateModal, {
             props: { advancedMode: true },
+            global: {
+                stubs: {
+                    teleport: true, // Stubs out <teleport> completely
+                },
+            },
         });
         let address = wrapper.find('[data-testid="proposalAddress"]');
         // Address input
+        expect(address.attributes().disabled).toBe(undefined);
         expect(address.isVisible()).toBe(true);
         await wrapper.setProps({ advancedMode: false });
         address = wrapper.find('[data-testid="proposalAddress"]');
-        expect(address.exists()).toBe(false);
+        expect(address.attributes().disabled).toBe('');
     });
 
     it('submits correctly', async () => {
         const wrapper = mount(ProposalCreateModal, {
-            props: { advancedMode: true },
+            props: { advancedMode: true, isTest: true },
         });
         const proposalTitle = wrapper.find('[data-testid="proposalTitle"]');
         await proposalTitle.setValue('Proposal Title');
@@ -28,17 +45,20 @@ describe('ProposalCreateModal component tests', () => {
         const proposalPayment = wrapper.find('[data-testid="proposalPayment"]');
         await proposalPayment.setValue(20);
         const address = wrapper.find('[data-testid="proposalAddress"]');
-        await address.setValue('DLabSomethingSomething');
-
+        await address.setValue('DLabsOops');
         const proposalSubmit = wrapper.find('[data-testid="proposalSubmit"]');
         await proposalSubmit.trigger('click');
+        // Nothing should be emitted because address is wrong
+        expect(wrapper.emitted().create).toBeUndefined();
+        await address.setValue('DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb');
+        await proposalSubmit.trigger('click');
         expect(wrapper.emitted().create).toStrictEqual([
             [
                 'Proposal Title',
                 'https://proposal.com/',
                 3,
                 20,
-                'DLabSomethingSomething',
+                'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb',
             ],
         ]);
         await wrapper.setProps({ advancedMode: false });
@@ -50,7 +70,7 @@ describe('ProposalCreateModal component tests', () => {
             'https://proposal.com/',
             3,
             20,
-            '',
+            undefined,
         ]);
     });
 });

From 03ed84e9c3b0345bb13a95b3b4bffcaf6d9b0e21 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Mon, 3 Mar 2025 15:00:16 +0100
Subject: [PATCH 2/8] Add proposal confirmation screen

---
 locale/cnr/translation.toml                |  2 +
 locale/de/translation.toml                 |  2 +
 locale/en/translation.toml                 |  2 +
 locale/es-mx/translation.toml              |  2 +
 locale/fr/translation.toml                 |  2 +
 locale/hi/translation.toml                 |  2 +
 locale/it/translation.toml                 |  2 +
 locale/nl/translation.toml                 |  2 +
 locale/ph/translation.toml                 |  2 +
 locale/pl/translation.toml                 |  2 +
 locale/pt-br/translation.toml              |  2 +
 locale/pt-pt/translation.toml              |  2 +
 locale/template/translation.toml           |  2 +
 locale/uwu/translation.toml                |  2 +
 scripts/governance/ProposalCreateModal.vue | 99 +++++++++++++++++++++-
 15 files changed, 124 insertions(+), 3 deletions(-)

diff --git a/locale/cnr/translation.toml b/locale/cnr/translation.toml
index 020374ba1..1d1f803dd 100644
--- a/locale/cnr/translation.toml
+++ b/locale/cnr/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Interna greška, molimo pokušajte ponovo kasnije" # Internal error, please try again later
diff --git a/locale/de/translation.toml b/locale/de/translation.toml
index 50640e862..248af3448 100644
--- a/locale/de/translation.toml
+++ b/locale/de/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Interner Fehler, bitte versuche es später erneut" # Internal error, please try again later
diff --git a/locale/en/translation.toml b/locale/en/translation.toml
index d757e354e..817647307 100644
--- a/locale/en/translation.toml
+++ b/locale/en/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "The address provided is invalid" # The address provided
 formValidationMin = "The minimum value is: {value}" # The minimum value is: {value}
 formValidationMax = "The maximum value is: {value}" # The maximum value is: {value}
 formValidationInvalidNumber = "Invalid Number" # Invalid Number
+proposalTotal = "For a total of" # For a total of
+proposalConfirm = "Confirm your proposal" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, please try again later" # Internal error, please try again later
diff --git a/locale/es-mx/translation.toml b/locale/es-mx/translation.toml
index b68e61c6c..7aaf2c11a 100644
--- a/locale/es-mx/translation.toml
+++ b/locale/es-mx/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Error interno, vuelve a intentarlo más tarde" # Internal error, please try again later
diff --git a/locale/fr/translation.toml b/locale/fr/translation.toml
index c51d05c44..765ad360a 100644
--- a/locale/fr/translation.toml
+++ b/locale/fr/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Erreur interne, veuillez réessayer plus tard" # Internal error, please try again later
diff --git a/locale/hi/translation.toml b/locale/hi/translation.toml
index 762f7eb52..4f13df81e 100644
--- a/locale/hi/translation.toml
+++ b/locale/hi/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "आंतरिक त्रुटि, कृपया कुछ समय बाद पुनः प्रयास करें" # Internal error, please try again later
diff --git a/locale/it/translation.toml b/locale/it/translation.toml
index 72a29c519..16a94583b 100644
--- a/locale/it/translation.toml
+++ b/locale/it/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Errore interno, rirova più tardi" # Internal error, please try again later
diff --git a/locale/nl/translation.toml b/locale/nl/translation.toml
index 3a44635ef..c5b7aa8bb 100644
--- a/locale/nl/translation.toml
+++ b/locale/nl/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Interne fout, probeer het later opnieuw" # Internal error, please try again later
diff --git a/locale/ph/translation.toml b/locale/ph/translation.toml
index 4189e7fcc..0ce962788 100644
--- a/locale/ph/translation.toml
+++ b/locale/ph/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, Pakiusap uliting muli" # Internal error, please try again later
diff --git a/locale/pl/translation.toml b/locale/pl/translation.toml
index 7bb85efef..c5627e035 100644
--- a/locale/pl/translation.toml
+++ b/locale/pl/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Błąd wewnętrzny, spróbuj ponownie później" # Internal error, please try again later
diff --git a/locale/pt-br/translation.toml b/locale/pt-br/translation.toml
index 28cf35c7e..471019353 100644
--- a/locale/pt-br/translation.toml
+++ b/locale/pt-br/translation.toml
@@ -96,6 +96,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro este endereço irá ser usado." # <b>Cold Address set!</b><br>Future stakes will use this address.
diff --git a/locale/pt-pt/translation.toml b/locale/pt-pt/translation.toml
index 21ede79bd..db4997831 100644
--- a/locale/pt-pt/translation.toml
+++ b/locale/pt-pt/translation.toml
@@ -96,6 +96,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 STAKE_ADDR_SET = "<b>Endereço de Cold Staking definido!</b><br>Ao fazer Stake no futuro irá ser usado este endereço." # <b>Cold Address set!</b><br>Future stakes will use this address.
diff --git a/locale/template/translation.toml b/locale/template/translation.toml
index 43dc12e07..3d371f1e6 100644
--- a/locale/template/translation.toml
+++ b/locale/template/translation.toml
@@ -244,6 +244,8 @@ formValidationString = "The string contains invalid characters"
 formValidationUrl = "The URL is invalid"
 formValidationAddress = "The address provided is invalid"
 formValidationInvalidNumber = "Invalid Number"
+proposalTotal = "For a total of"
+proposalConfirm = "Confirm your proposal"
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, please try again later"
diff --git a/locale/uwu/translation.toml b/locale/uwu/translation.toml
index 1b13f31f4..c5adb1a41 100644
--- a/locale/uwu/translation.toml
+++ b/locale/uwu/translation.toml
@@ -220,6 +220,8 @@ formValidationAddress = "" # The address provided is invalid
 formValidationMin = "" # The minimum value is: {value}
 formValidationMax = "" # The maximum value is: {value}
 formValidationInvalidNumber = "" # Invalid Number
+proposalTotal = "" # For a total of
+proposalConfirm = "" # Confirm your proposal
 
 [ALERTS]
 INTERNAL_ERROR = "Internal error, pwease try again later" # Internal error, please try again later
diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index 70790954d..007ad2960 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -5,7 +5,7 @@ import Input from '../form/Input.vue';
 import NumericInput from '../form/NumericInput.vue';
 import { translation } from '../i18n.js';
 import { COIN, cChainParams } from '../chain_params';
-import { toRefs } from 'vue';
+import { toRefs, ref, reactive } from 'vue';
 import { isStandardAddress } from '../misc';
 
 const props = defineProps({
@@ -14,7 +14,10 @@ const props = defineProps({
 });
 const { advancedMode } = toRefs(props);
 const emit = defineEmits(['close', 'create']);
-function submit(data) {
+ const data = reactive({});
+const showConfirmation = ref(false);
+function submit() {
+	showConfirmation.value = false;
     emit(
         'create',
         data.proposalTitle,
@@ -25,6 +28,17 @@ function submit(data) {
     );
 }
 
+function createConfirmationScreen(d) {
+    data.proposalTitle = d.proposalTitle;
+    data.proposalUrl = d.proposalUrl;
+    data.proposalCycles = d.proposalCycles;
+    data.proposalPayment = d.proposalPayment;
+    data.proposalAddress = d.proposalAddress;
+    console.log(d);
+    console.log(data);
+    showConfirmation.value = true;
+}
+
 const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
 </script>
 
@@ -47,7 +61,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
             >
         </template>
         <template #body>
-            <Form @submit="submit">
+            <Form @submit="createConfirmationScreen">
                 <template #default>
                     <p
                         style="
@@ -197,4 +211,83 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
             </button>
         </template>
     </Modal>
+
+    <Modal :show="showConfirmation">
+        <template #header>
+            <h4>{{ translation.proposalConfirm }}</h4>
+        </template>
+        <template #body>
+            <div class="proposalConfirmContainer">
+                <p class="proposalConfirmLabel">Proposal name</p>
+                <code class="proposalConfirmText">{{
+                    data.proposalTitle
+                }}</code>
+            </div>
+            <div class="proposalConfirmContainer">
+                <p class="proposalConfirmLabel">URL</p>
+                <code class="proposalConfirmText">{{ data.proposalUrl }}</code>
+            </div>
+            <div>
+                <p class="proposalConfirmLabel">Duration in cycles</p>
+                <code class="proposalConfirmText">{{
+                    data.proposalCycles
+                }}</code>
+            </div>
+
+            <div class="proposalConfirmContainer">
+                <p class="proposalConfirmLabel">
+                    {{ cChainParams.current.TICKER }} per cycle
+                </p>
+                <code class="proposalConfirmText"
+                    >{{ data.proposalPayment }}
+                </code>
+            </div>
+            <div class="proposalConfirmContainer">
+                <p class="proposalConfirmLabel">
+                    {{ translation.proposalTotal }}
+                </p>
+                <code class="proposalConfirmText"
+                    >{{ data.proposalPayment * data.proposalCycles }}
+                </code>
+            </div>
+            <div v-if="data.proposalAddress" class="proposalConfirmContainer">
+                <p class="proposalConfirmLabel">Proposal Address</p>
+                <code class="proposalConfirmText"
+                    >{{ data.proposalAddress }}
+                </code>
+            </div>
+        </template>
+        <template #footer>
+            <button
+                type="button"
+                class="pivx-button-big"
+                style="float: right"
+                data-testid="proposalSubmit"
+                @click="submit()"
+            >
+                {{ translation.popupConfirm }}
+            </button>
+
+            <button
+                type="button"
+                class="pivx-button-big-cancel"
+                style="float: left"
+                data-testid="proposalCancel"
+                @click="showConfirmation = false"
+            >
+                {{ translation.popupCancel }}
+            </button>
+        </template>
+    </Modal>
 </template>
+<style>
+.proposalConfirmLabel {
+    margin-bottom: 0px;
+    color: #af9cc6;
+    font-size: 1rem;
+    font-weight: 500;
+}
+ .proposalConfirmContainer {
+     margin-bottom: 10px;
+ }
+</style>

From a16c1f5327bf20b5d24d4afd806bebd316262044 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Mon, 3 Mar 2025 15:21:47 +0100
Subject: [PATCH 3/8] Run prettier

---
 scripts/governance/ProposalCreateModal.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index 007ad2960..9a7987bb0 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -14,10 +14,10 @@ const props = defineProps({
 });
 const { advancedMode } = toRefs(props);
 const emit = defineEmits(['close', 'create']);
- const data = reactive({});
+const data = reactive({});
 const showConfirmation = ref(false);
 function submit() {
-	showConfirmation.value = false;
+    showConfirmation.value = false;
     emit(
         'create',
         data.proposalTitle,
@@ -287,7 +287,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
     font-size: 1rem;
     font-weight: 500;
 }
- .proposalConfirmContainer {
-     margin-bottom: 10px;
- }
+.proposalConfirmContainer {
+    margin-bottom: 10px;
+}
 </style>

From f1baf23138abe78413e66cef5c4cbd409934aef9 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Tue, 11 Mar 2025 15:33:10 +0100
Subject: [PATCH 4/8] Fix tests

---
 scripts/governance/ProposalCreateModal.vue            | 10 +++++-----
 .../components/governance/ProposalCreateModal.spec.js | 11 ++++++++++-
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index 9a7987bb0..f867a5f97 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -5,7 +5,7 @@ import Input from '../form/Input.vue';
 import NumericInput from '../form/NumericInput.vue';
 import { translation } from '../i18n.js';
 import { COIN, cChainParams } from '../chain_params';
-import { toRefs, ref, reactive } from 'vue';
+import { toRefs, ref, reactive, watch } from 'vue';
 import { isStandardAddress } from '../misc';
 
 const props = defineProps({
@@ -16,15 +16,17 @@ const { advancedMode } = toRefs(props);
 const emit = defineEmits(['close', 'create']);
 const data = reactive({});
 const showConfirmation = ref(false);
+
 function submit() {
     showConfirmation.value = false;
+
     emit(
         'create',
         data.proposalTitle,
         data.proposalUrl,
         data.proposalCycles,
         data.proposalPayment,
-        data.proposalAddress
+        advancedMode.value ? data.proposalAddress : undefined
     );
 }
 
@@ -34,8 +36,6 @@ function createConfirmationScreen(d) {
     data.proposalCycles = d.proposalCycles;
     data.proposalPayment = d.proposalPayment;
     data.proposalAddress = d.proposalAddress;
-    console.log(d);
-    console.log(data);
     showConfirmation.value = true;
 }
 
@@ -262,7 +262,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                 type="button"
                 class="pivx-button-big"
                 style="float: right"
-                data-testid="proposalSubmit"
+                data-testid="proposalConfirmSubmit"
                 @click="submit()"
             >
                 {{ translation.popupConfirm }}
diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js
index d5f5ba7fc..edb582b2e 100644
--- a/tests/components/governance/ProposalCreateModal.spec.js
+++ b/tests/components/governance/ProposalCreateModal.spec.js
@@ -14,7 +14,7 @@ vi.stubGlobal(
 );
 
 describe('ProposalCreateModal component tests', () => {
-    it('hides address input when advanced mode is false', async () => {
+    it.todo('hides address input when advanced mode is false', async () => {
         const wrapper = mount(ProposalCreateModal, {
             props: { advancedMode: true },
             global: {
@@ -52,6 +52,11 @@ describe('ProposalCreateModal component tests', () => {
         expect(wrapper.emitted().create).toBeUndefined();
         await address.setValue('DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb');
         await proposalSubmit.trigger('click');
+        // Confirm the submission
+        await wrapper
+            .find('[data-testid="proposalConfirmSubmit"]')
+            .trigger('click');
+
         expect(wrapper.emitted().create).toStrictEqual([
             [
                 'Proposal Title',
@@ -64,6 +69,10 @@ describe('ProposalCreateModal component tests', () => {
         await wrapper.setProps({ advancedMode: false });
 
         await proposalSubmit.trigger('click');
+        // Confirm the submission
+        await wrapper
+            .find('[data-testid="proposalConfirmSubmit"]')
+            .trigger('click');
         // When advanced mode is toggled off, address should reset
         expect(wrapper.emitted().create.at(-1)).toStrictEqual([
             'Proposal Title',

From f2f78526acfa15843da48f1f77b2c87d53df29cc Mon Sep 17 00:00:00 2001
From: BreadJS <83626012+BreadJS@users.noreply.github.com>
Date: Wed, 19 Mar 2025 16:49:29 +0100
Subject: [PATCH 5/8] UI Redesign

---
 scripts/Modal.vue                          |   4 +-
 scripts/governance/ProposalCreateModal.vue | 123 ++++++++++++++-------
 2 files changed, 88 insertions(+), 39 deletions(-)

diff --git a/scripts/Modal.vue b/scripts/Modal.vue
index d59c10402..c3d7d2438 100644
--- a/scripts/Modal.vue
+++ b/scripts/Modal.vue
@@ -2,6 +2,7 @@
 const props = defineProps({
     show: Boolean,
     modalClass: String,
+    centered: Boolean
 });
 </script>
 
@@ -25,7 +26,8 @@ const props = defineProps({
                         <slot name="header"></slot>
                     </div>
                     <div
-                        class="modal-body center-text"
+                        class="modal-body"
+                        :class="{ 'center-text': !centered }"
                         style="padding-bottom: 8px; overflow: auto"
                     >
                         <slot name="body"></slot>
diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index f867a5f97..1677481ac 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -212,50 +212,69 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
         </template>
     </Modal>
 
-    <Modal :show="showConfirmation">
+    <Modal :show="showConfirmation" :centered="true">
         <template #header>
             <h4>{{ translation.proposalConfirm }}</h4>
         </template>
         <template #body>
-            <div class="proposalConfirmContainer">
-                <p class="proposalConfirmLabel">Proposal name</p>
-                <code class="proposalConfirmText">{{
-                    data.proposalTitle
-                }}</code>
-            </div>
-            <div class="proposalConfirmContainer">
-                <p class="proposalConfirmLabel">URL</p>
-                <code class="proposalConfirmText">{{ data.proposalUrl }}</code>
-            </div>
-            <div>
-                <p class="proposalConfirmLabel">Duration in cycles</p>
-                <code class="proposalConfirmText">{{
-                    data.proposalCycles
-                }}</code>
-            </div>
+            <div class="row">
+                <div class="col-6">
+                    <div class="proposalConfirmContainer">
+                        <p class="proposalConfirmLabel">Proposal name</p>
+                        <code class="proposalConfirmText">{{ data.proposalTitle }}</code>
+                    </div>
+                </div>
 
-            <div class="proposalConfirmContainer">
-                <p class="proposalConfirmLabel">
-                    {{ cChainParams.current.TICKER }} per cycle
-                </p>
-                <code class="proposalConfirmText"
-                    >{{ data.proposalPayment }}
-                </code>
-            </div>
-            <div class="proposalConfirmContainer">
-                <p class="proposalConfirmLabel">
-                    {{ translation.proposalTotal }}
-                </p>
-                <code class="proposalConfirmText"
-                    >{{ data.proposalPayment * data.proposalCycles }}
-                </code>
-            </div>
-            <div v-if="data.proposalAddress" class="proposalConfirmContainer">
-                <p class="proposalConfirmLabel">Proposal Address</p>
-                <code class="proposalConfirmText"
-                    >{{ data.proposalAddress }}
-                </code>
+                <div class="col-6">
+                    <div>
+                        <p class="proposalConfirmLabel">Duration in cycles</p>
+                        <code class="proposalConfirmText">{{
+                            data.proposalCycles
+                        }}</code>
+                    </div>
+                </div>
+
+                <div class="col-6">
+                    <div class="proposalConfirmContainer">
+                        <p class="proposalConfirmLabel">
+                            {{ cChainParams.current.TICKER }} per cycle
+                        </p>
+                        <code class="proposalConfirmText"
+                            >{{ data.proposalPayment }} {{ cChainParams.current.TICKER }}
+                        </code>
+                    </div>
+                </div>
+                <div class="col-6">
+                    <div class="proposalConfirmContainer">
+                        <p class="proposalConfirmLabel">
+                            {{ translation.proposalTotal }}
+                        </p>
+                        <code class="proposalConfirmText"
+                            >{{ data.proposalPayment * data.proposalCycles }} {{ cChainParams.current.TICKER }}
+                        </code>
+                    </div>
+                </div>
+
+                <div class="col-12">
+                    <div class="proposalConfirmContainer">
+                        <p class="proposalConfirmLabel">URL</p>
+                        <div class="proposalConfirmText link"><a :href="data.proposalUrl">{{ data.proposalUrl }}</a></div>
+                    </div>
+                </div>
+                
+                
+
+                <div class="col-12">
+                    <div v-if="data.proposalAddress" class="proposalConfirmContainer">
+                        <p class="proposalConfirmLabel">Proposal Address</p>
+                        <code class="proposalConfirmText"
+                            >{{ data.proposalAddress }}
+                        </code>
+                    </div>
+                </div>
             </div>
+
+            
         </template>
         <template #footer>
             <button
@@ -290,4 +309,32 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
 .proposalConfirmContainer {
     margin-bottom: 10px;
 }
+
+.proposalConfirmText {
+    background-color: #0000003d;
+    padding: 1px 5px 2px 5px;
+    border-radius: 5px;
+}
+
+.proposalConfirmText.link {
+    background-color: #0000003d;
+    padding: 1px 5px 2px 5px;
+    border-radius: 5px;
+    width: fit-content;
+    word-break: break-all;
+}
+
+.proposalConfirmText.link a {
+    color:#9221ff;
+    font-size: 87.5%;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace!important;
+}
+
+.proposalConfirmText.link a:hover {
+    text-decoration:underline!important;
+}
+
+code {
+    color:#E9DEFF;
+}
 </style>

From 71f51e97b618195aa0c2f3c13675d50810189e92 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Thu, 20 Mar 2025 09:51:32 +0100
Subject: [PATCH 6/8] Run pretteir

---
 scripts/Modal.vue                          |  2 +-
 scripts/governance/ProposalCreateModal.vue | 34 +++++++++++++---------
 2 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/scripts/Modal.vue b/scripts/Modal.vue
index c3d7d2438..c0355f6c4 100644
--- a/scripts/Modal.vue
+++ b/scripts/Modal.vue
@@ -2,7 +2,7 @@
 const props = defineProps({
     show: Boolean,
     modalClass: String,
-    centered: Boolean
+    centered: Boolean,
 });
 </script>
 
diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index 1677481ac..f2d0f9e68 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -221,7 +221,9 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                 <div class="col-6">
                     <div class="proposalConfirmContainer">
                         <p class="proposalConfirmLabel">Proposal name</p>
-                        <code class="proposalConfirmText">{{ data.proposalTitle }}</code>
+                        <code class="proposalConfirmText">{{
+                            data.proposalTitle
+                        }}</code>
                     </div>
                 </div>
 
@@ -240,7 +242,8 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                             {{ cChainParams.current.TICKER }} per cycle
                         </p>
                         <code class="proposalConfirmText"
-                            >{{ data.proposalPayment }} {{ cChainParams.current.TICKER }}
+                            >{{ data.proposalPayment }}
+                            {{ cChainParams.current.TICKER }}
                         </code>
                     </div>
                 </div>
@@ -250,7 +253,8 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                             {{ translation.proposalTotal }}
                         </p>
                         <code class="proposalConfirmText"
-                            >{{ data.proposalPayment * data.proposalCycles }} {{ cChainParams.current.TICKER }}
+                            >{{ data.proposalPayment * data.proposalCycles }}
+                            {{ cChainParams.current.TICKER }}
                         </code>
                     </div>
                 </div>
@@ -258,14 +262,19 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                 <div class="col-12">
                     <div class="proposalConfirmContainer">
                         <p class="proposalConfirmLabel">URL</p>
-                        <div class="proposalConfirmText link"><a :href="data.proposalUrl">{{ data.proposalUrl }}</a></div>
+                        <div class="proposalConfirmText link">
+                            <a :href="data.proposalUrl">{{
+                                data.proposalUrl
+                            }}</a>
+                        </div>
                     </div>
                 </div>
-                
-                
 
                 <div class="col-12">
-                    <div v-if="data.proposalAddress" class="proposalConfirmContainer">
+                    <div
+                        v-if="data.proposalAddress"
+                        class="proposalConfirmContainer"
+                    >
                         <p class="proposalConfirmLabel">Proposal Address</p>
                         <code class="proposalConfirmText"
                             >{{ data.proposalAddress }}
@@ -273,8 +282,6 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
                     </div>
                 </div>
             </div>
-
-            
         </template>
         <template #footer>
             <button
@@ -325,16 +332,17 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
 }
 
 .proposalConfirmText.link a {
-    color:#9221ff;
+    color: #9221ff;
     font-size: 87.5%;
-    font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace!important;
+    font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
+        'Courier New', monospace !important;
 }
 
 .proposalConfirmText.link a:hover {
-    text-decoration:underline!important;
+    text-decoration: underline !important;
 }
 
 code {
-    color:#E9DEFF;
+    color: #e9deff;
 }
 </style>

From 21dac525919116aeea99364bb1ae5bebfb0fae62 Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Thu, 20 Mar 2025 10:20:58 +0100
Subject: [PATCH 7/8] Fix modal always being visible

---
 scripts/governance/Governance.vue          | 4 ++--
 scripts/governance/ProposalCreateModal.vue | 5 +++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/scripts/governance/Governance.vue b/scripts/governance/Governance.vue
index 6d7531abb..85ff27da1 100644
--- a/scripts/governance/Governance.vue
+++ b/scripts/governance/Governance.vue
@@ -20,7 +20,7 @@ import { useMasternode } from '../composables/use_masternode';
 import { useAlerts } from '../composables/use_alerts.js';
 const { createAlert } = useAlerts();
 
-const showCreateProposalModal = ref(true);
+const showCreateProposalModal = ref(false);
 
 const wallet = useWallet();
 const settings = useSettings();
@@ -236,7 +236,7 @@ async function vote(proposal, voteCode) {
 
 <template>
     <ProposalCreateModal
-        v-show="showCreateProposalModal"
+        :show="showCreateProposalModal"
         :advancedMode="advancedMode"
         @close="showCreateProposalModal = false"
         @create="createProposal"
diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue
index f2d0f9e68..118063e6e 100644
--- a/scripts/governance/ProposalCreateModal.vue
+++ b/scripts/governance/ProposalCreateModal.vue
@@ -11,8 +11,9 @@ import { isStandardAddress } from '../misc';
 const props = defineProps({
     advancedMode: Boolean,
     isTest: Boolean,
+    show: Boolean,
 });
-const { advancedMode } = toRefs(props);
+const { advancedMode, show } = toRefs(props);
 const emit = defineEmits(['close', 'create']);
 const data = reactive({});
 const showConfirmation = ref(false);
@@ -43,7 +44,7 @@ const isSafeStr = /^[a-z0-9 .,;\-_/:?@()]+$/i;
 </script>
 
 <template>
-    <Modal :show="true">
+    <Modal :show="show">
         <template #header>
             <h4>{{ translation.popupCreateProposal }}</h4>
             <span

From 9a24b3df048a5daf9900ac59cccb57d5c27bb67e Mon Sep 17 00:00:00 2001
From: Duddino <duddino@duddino.com>
Date: Thu, 20 Mar 2025 10:24:38 +0100
Subject: [PATCH 8/8] Fix tests and reenable one

---
 tests/components/governance/ProposalCreateModal.spec.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js
index edb582b2e..edf3f46df 100644
--- a/tests/components/governance/ProposalCreateModal.spec.js
+++ b/tests/components/governance/ProposalCreateModal.spec.js
@@ -14,9 +14,9 @@ vi.stubGlobal(
 );
 
 describe('ProposalCreateModal component tests', () => {
-    it.todo('hides address input when advanced mode is false', async () => {
+    it('hides address input when advanced mode is false', async () => {
         const wrapper = mount(ProposalCreateModal, {
-            props: { advancedMode: true },
+            props: { advancedMode: true, show: true },
             global: {
                 stubs: {
                     teleport: true, // Stubs out <teleport> completely
@@ -34,7 +34,7 @@ describe('ProposalCreateModal component tests', () => {
 
     it('submits correctly', async () => {
         const wrapper = mount(ProposalCreateModal, {
-            props: { advancedMode: true, isTest: true },
+            props: { advancedMode: true, isTest: true, show: true },
         });
         const proposalTitle = wrapper.find('[data-testid="proposalTitle"]');
         await proposalTitle.setValue('Proposal Title');