chore(knip): add config and remove dead code/deps
- knip.json: scope to src, ignore submodule/worktrees, mark
generated abi.ts as unresolved-allowed, honor @public JSDoc tag
- drop 14 orphaned files (12 ui components, model/Bank, model/Pix)
- drop 18 unused deps (urql, tanstack, wagmi/{core,vue}, graphql,
permissionless, graphql-codegen suite, axe-core, lighthouse,
vue/test-utils)
- drop 4 unused exports and de-export 9 internal-only types
- mark NetworksTestnet as @public (in-flight testnet support)
This commit is contained in:
22
knip.json
Normal file
22
knip.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/knip@latest/schema.json",
|
||||||
|
"entry": [
|
||||||
|
"src/router/index.ts",
|
||||||
|
"index.html",
|
||||||
|
"wagmi.config.ts"
|
||||||
|
],
|
||||||
|
"project": [
|
||||||
|
"src/**/*.{ts,vue}",
|
||||||
|
"*.config.{ts,js}"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"p2pix-smart-contracts/**",
|
||||||
|
".claude/**",
|
||||||
|
"babel.config.js"
|
||||||
|
],
|
||||||
|
"ignoreUnresolved": [
|
||||||
|
"./abi",
|
||||||
|
"@/blockchain/abi"
|
||||||
|
],
|
||||||
|
"tags": ["public"]
|
||||||
|
}
|
||||||
18
package.json
18
package.json
@@ -16,19 +16,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.1.11",
|
"@floating-ui/vue": "^1.1.11",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
|
||||||
"@tanstack/query-core": "^5.100.8",
|
|
||||||
"@tanstack/vue-query": "^5.100.8",
|
|
||||||
"@urql/core": "^6.0.1",
|
|
||||||
"@urql/vue": "^2.1.0",
|
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"@wagmi/core": "^3.4.8",
|
|
||||||
"@wagmi/vue": "^0.5.11",
|
|
||||||
"@web3-onboard/injected-wallets": "^2.11.3",
|
"@web3-onboard/injected-wallets": "^2.11.3",
|
||||||
"@web3-onboard/vue": "^2.10.0",
|
"@web3-onboard/vue": "^2.10.0",
|
||||||
"graphql": "^16.13.2",
|
|
||||||
"marked": "^18.0.3",
|
"marked": "^18.0.3",
|
||||||
"permissionless": "^0.2.57",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"viem": "^2.48.8",
|
"viem": "^2.48.8",
|
||||||
"vite-svg-loader": "^5.1.1",
|
"vite-svg-loader": "^5.1.1",
|
||||||
@@ -36,12 +27,6 @@
|
|||||||
"vue-router": "^5.0.6"
|
"vue-router": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.11.3",
|
|
||||||
"@graphql-codegen/cli": "^7.0.0",
|
|
||||||
"@graphql-codegen/client-preset": "^6.0.0",
|
|
||||||
"@graphql-codegen/typed-document-node": "^7.0.0",
|
|
||||||
"@graphql-codegen/typescript": "^6.0.0",
|
|
||||||
"@graphql-codegen/typescript-operations": "^6.0.0",
|
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
@@ -51,15 +36,12 @@
|
|||||||
"@vitest/coverage-v8": "^4.1.5",
|
"@vitest/coverage-v8": "^4.1.5",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.7.0",
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
"@vue/test-utils": "^2.4.10",
|
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"@wagmi/cli": "^2.10.0",
|
"@wagmi/cli": "^2.10.0",
|
||||||
"autoprefixer": "^10.5.0",
|
"autoprefixer": "^10.5.0",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-plugin-vue": "^10.9.0",
|
"eslint-plugin-vue": "^10.9.0",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.9.0",
|
||||||
"lighthouse": "^13.2.0",
|
|
||||||
"playwright-lighthouse": "^4.0.0",
|
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
|
|||||||
@@ -197,72 +197,6 @@ export const listAllTransactionByWalletAddress = async (
|
|||||||
return transactions.sort((a, b) => b.blockNumber - a.blockNumber);
|
return transactions.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
// get wallet's release transactions
|
|
||||||
export const listReleaseTransactionByWalletAddress = async (
|
|
||||||
walletAddress: Address,
|
|
||||||
) => {
|
|
||||||
const user = useUser();
|
|
||||||
const network = user.network.value;
|
|
||||||
|
|
||||||
// Query subgraph for release transactions
|
|
||||||
const subgraphQuery = {
|
|
||||||
query: `
|
|
||||||
{
|
|
||||||
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
|
||||||
buyer
|
|
||||||
lockId
|
|
||||||
e2eId
|
|
||||||
blockTimestamp
|
|
||||||
blockNumber
|
|
||||||
transactionHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch data from subgraph
|
|
||||||
const response = await fetch(network.subgraphUrls[0], {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subgraphQuery),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Process the subgraph response into the same format as the previous implementation
|
|
||||||
if (!data.data?.lockReleaseds) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform the subgraph data to match the event log decode format
|
|
||||||
return data.data.lockReleaseds
|
|
||||||
.sort((a: any, b: any) => {
|
|
||||||
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
|
|
||||||
})
|
|
||||||
.map((release: any) => {
|
|
||||||
try {
|
|
||||||
// Create a structure similar to the decoded event log
|
|
||||||
return {
|
|
||||||
eventName: 'LockReleased',
|
|
||||||
args: {
|
|
||||||
buyer: release.buyer,
|
|
||||||
lockID: BigInt(release.lockId),
|
|
||||||
e2eId: release.e2eId,
|
|
||||||
},
|
|
||||||
// Add any other necessary fields to match the original return format
|
|
||||||
blockNumber: BigInt(release.blockNumber),
|
|
||||||
transactionHash: release.transactionHash,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing subgraph data', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((decoded: any) => decoded !== null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
const network = user.network.value;
|
const network = user.network.value;
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useUser } from '@/composables/useUser';
|
|
||||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
|
||||||
import { debounce } from '@/utils/debounce';
|
|
||||||
import { decimalCount } from '@/utils/decimalCount';
|
|
||||||
import { getTokenImage } from '@/utils/imagesPath';
|
|
||||||
import { useOnboard } from '@web3-onboard/vue';
|
|
||||||
|
|
||||||
// Store
|
|
||||||
const user = useUser();
|
|
||||||
const { walletAddress } = user;
|
|
||||||
|
|
||||||
// Reactive state
|
|
||||||
const tokenValue = ref<number>(0);
|
|
||||||
const enableSelectButton = ref<boolean>(false);
|
|
||||||
const hasLiquidity = ref<boolean>(true);
|
|
||||||
const validDecimals = ref<boolean>(true);
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits(['tokenBuy']);
|
|
||||||
|
|
||||||
// Blockchain methods
|
|
||||||
const connectAccount = async (): Promise<void> => {
|
|
||||||
const { connectWallet } = useOnboard();
|
|
||||||
await connectWallet();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Debounce methods
|
|
||||||
const handleInputEvent = (event: any): void => {
|
|
||||||
const { value } = event.target;
|
|
||||||
|
|
||||||
tokenValue.value = Number(value);
|
|
||||||
|
|
||||||
if (decimalCount(String(tokenValue.value)) > 2) {
|
|
||||||
validDecimals.value = false;
|
|
||||||
enableSelectButton.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validDecimals.value = true;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="page">
|
|
||||||
<div class="text-container">
|
|
||||||
<span class="text font-extrabold text-5xl max-w-[29rem]"
|
|
||||||
>Adquira cripto com apenas um Pix</span
|
|
||||||
>
|
|
||||||
<span class="text font-medium text-base max-w-[28rem]"
|
|
||||||
>Digite um valor, confira a oferta, conecte sua carteira e receba os
|
|
||||||
tokens após realizar o Pix</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="main-container">
|
|
||||||
<div
|
|
||||||
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between w-full items-center">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="border-none outline-none text-lg text-gray-900 w-fit"
|
|
||||||
v-bind:class="{
|
|
||||||
'font-semibold': tokenValue != undefined,
|
|
||||||
'text-xl': tokenValue != undefined,
|
|
||||||
}"
|
|
||||||
@input="debounce(handleInputEvent, 500)($event)"
|
|
||||||
placeholder="0 "
|
|
||||||
step=".01"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Token image"
|
|
||||||
class="w-fit"
|
|
||||||
:src="getTokenImage(user.selectedToken.value)"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 text-lg w-fit" id="token">{{
|
|
||||||
user.selectedToken
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="custom-divide py-2"></div>
|
|
||||||
<div class="flex justify-between pt-2" v-if="hasLiquidity">
|
|
||||||
<p class="text-gray-500 font-normal text-sm w-auto">
|
|
||||||
~ R$ {{ tokenValue.toFixed(2) }}
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<img
|
|
||||||
alt="Polygon image"
|
|
||||||
src="@/assets/networks/polygon.svg?url"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt="Ethereum image"
|
|
||||||
src="@/assets/networks/ethereum.svg?url"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex pt-2 justify-center" v-if="!validDecimals">
|
|
||||||
<span class="text-red-500 font-normal text-sm"
|
|
||||||
>Por favor utilize no máximo 2 casas decimais</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex pt-2 justify-center" v-else-if="!hasLiquidity">
|
|
||||||
<span class="text-red-500 font-normal text-sm"
|
|
||||||
>Atualmente não há liquidez nas redes para sua demanda</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomButton
|
|
||||||
v-if="walletAddress"
|
|
||||||
:text="'Conectar carteira'"
|
|
||||||
@buttonClicked="emit('tokenBuy')"
|
|
||||||
/>
|
|
||||||
<CustomButton
|
|
||||||
v-if="!walletAddress"
|
|
||||||
:text="'Conectar carteira'"
|
|
||||||
@buttonClicked="connectAccount()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.custom-divide {
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid #d1d5db;
|
|
||||||
}
|
|
||||||
.bottom-position {
|
|
||||||
top: -20px;
|
|
||||||
right: 50%;
|
|
||||||
transform: translateX(50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-container {
|
|
||||||
@apply flex flex-col items-center justify-center gap-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
@apply text-white text-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='number']::-webkit-inner-spin-button,
|
|
||||||
input[type='number']::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch, computed } from 'vue';
|
|
||||||
import { TokenEnum } from '@/model/NetworkEnum';
|
|
||||||
import { decimalCount } from '@/utils/decimalCount';
|
|
||||||
import { debounce } from '@/utils/debounce';
|
|
||||||
import TokenSelector from './TokenSelector.vue';
|
|
||||||
import ErrorMessage from './ErrorMessage.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
modelValue: number;
|
|
||||||
selectedToken: TokenEnum;
|
|
||||||
placeholder?: string;
|
|
||||||
showTokenSelector?: boolean;
|
|
||||||
showConversion?: boolean;
|
|
||||||
conversionRate?: number;
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placeholder: '0',
|
|
||||||
showTokenSelector: true,
|
|
||||||
showConversion: true,
|
|
||||||
conversionRate: 1,
|
|
||||||
minValue: 0,
|
|
||||||
disabled: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: number];
|
|
||||||
'update:selectedToken': [token: TokenEnum];
|
|
||||||
error: [message: string | null];
|
|
||||||
valid: [isValid: boolean];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const inputValue = ref<string>(String(props.modelValue || ''));
|
|
||||||
const validDecimals = ref(true);
|
|
||||||
const validRange = ref(true);
|
|
||||||
|
|
||||||
const convertedValue = computed(() => {
|
|
||||||
return (props.modelValue * props.conversionRate).toFixed(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMessage = computed(() => {
|
|
||||||
if (!validDecimals.value) {
|
|
||||||
return 'Por favor utilize no máximo 2 casas decimais';
|
|
||||||
}
|
|
||||||
if (!validRange.value) {
|
|
||||||
if (props.minValue && props.modelValue < props.minValue) {
|
|
||||||
return `Valor mínimo: ${props.minValue}`;
|
|
||||||
}
|
|
||||||
if (props.maxValue && props.modelValue > props.maxValue) {
|
|
||||||
return `Valor máximo: ${props.maxValue}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isValid = computed(() => {
|
|
||||||
return validDecimals.value && validRange.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInput = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const value = target.value;
|
|
||||||
inputValue.value = value;
|
|
||||||
|
|
||||||
const numValue = Number(value);
|
|
||||||
|
|
||||||
// Validar decimais
|
|
||||||
if (decimalCount(value) > 2) {
|
|
||||||
validDecimals.value = false;
|
|
||||||
emit('error', 'Por favor utilize no máximo 2 casas decimais');
|
|
||||||
emit('valid', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validDecimals.value = true;
|
|
||||||
|
|
||||||
// Validar range
|
|
||||||
if (props.minValue !== undefined && numValue < props.minValue) {
|
|
||||||
validRange.value = false;
|
|
||||||
emit('error', `Valor mínimo: ${props.minValue}`);
|
|
||||||
emit('valid', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.maxValue !== undefined && numValue > props.maxValue) {
|
|
||||||
validRange.value = false;
|
|
||||||
emit('error', `Valor máximo: ${props.maxValue}`);
|
|
||||||
emit('valid', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validRange.value = true;
|
|
||||||
|
|
||||||
emit('update:modelValue', numValue);
|
|
||||||
emit('error', null);
|
|
||||||
emit('valid', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedHandleInput = debounce(handleInput, 500);
|
|
||||||
|
|
||||||
const handleTokenChange = (token: TokenEnum) => {
|
|
||||||
emit('update:selectedToken', token);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal !== Number(inputValue.value)) {
|
|
||||||
inputValue.value = String(newVal || '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="amount-input-container">
|
|
||||||
<div class="input-row">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="inputValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="required"
|
|
||||||
class="amount-input"
|
|
||||||
:class="{
|
|
||||||
'font-semibold text-xl': modelValue > 0,
|
|
||||||
'has-error': !isValid,
|
|
||||||
}"
|
|
||||||
step="0.01"
|
|
||||||
@input="debouncedHandleInput"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TokenSelector
|
|
||||||
v-if="showTokenSelector"
|
|
||||||
:model-value="selectedToken"
|
|
||||||
:disabled="disabled"
|
|
||||||
size="md"
|
|
||||||
@update:model-value="handleTokenChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else class="token-display">
|
|
||||||
{{ selectedToken }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<p v-if="showConversion" class="conversion-text">
|
|
||||||
~ R$ {{ convertedValue }}
|
|
||||||
</p>
|
|
||||||
<slot name="extra-info"></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ErrorMessage v-if="errorMessage" :message="errorMessage" type="error" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.amount-input-container {
|
|
||||||
@apply flex flex-col w-full gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row {
|
|
||||||
@apply flex justify-between items-center w-full gap-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-input {
|
|
||||||
@apply border-none outline-none text-lg text-gray-900 flex-1 bg-transparent;
|
|
||||||
appearance: textfield;
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-input::-webkit-inner-spin-button,
|
|
||||||
.amount-input::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-input:disabled {
|
|
||||||
@apply opacity-50 cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-input.has-error {
|
|
||||||
@apply text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-display {
|
|
||||||
@apply flex items-center px-3 py-2 bg-gray-300 rounded-3xl min-w-fit text-gray-900 font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
@apply w-full border-b border-gray-300 my-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
@apply flex justify-between items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversion-text {
|
|
||||||
@apply text-gray-500 font-normal text-sm;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import bankList from '@/utils/files/isbpList.json';
|
|
||||||
|
|
||||||
export interface Bank {
|
|
||||||
ISPB: string;
|
|
||||||
longName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
modelValue: string | null;
|
|
||||||
disabled?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
placeholder: 'Busque e selecione seu banco',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: string];
|
|
||||||
change: [bank: Bank];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const bankItems = computed(() => {
|
|
||||||
return bankList.map((bank) => ({
|
|
||||||
value: bank.ISPB,
|
|
||||||
label: bank.longName,
|
|
||||||
bank: bank,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedItem = computed(() => {
|
|
||||||
if (!props.modelValue) return null;
|
|
||||||
return bankItems.value.find((item) => item.value === props.modelValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchQuery = computed({
|
|
||||||
get: () => selectedItem.value?.label || '',
|
|
||||||
set: (value: string) => {
|
|
||||||
// Handled by input
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredBanks = computed(() => {
|
|
||||||
if (!searchQuery.value) return [];
|
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
|
||||||
return bankList
|
|
||||||
.filter((bank) => bank.longName.toLowerCase().includes(query))
|
|
||||||
.slice(0, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showBankList = computed(() => {
|
|
||||||
return filteredBanks.value.length > 0 && searchQuery.value.length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectBank = (bank: Bank) => {
|
|
||||||
emit('update:modelValue', bank.ISPB);
|
|
||||||
emit('change', bank);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bank-selector">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="searchQuery"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:disabled="disabled"
|
|
||||||
class="bank-input"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<transition name="dropdown-fade">
|
|
||||||
<div v-if="showBankList" class="bank-list">
|
|
||||||
<div
|
|
||||||
v-for="bank in filteredBanks"
|
|
||||||
:key="bank.ISPB"
|
|
||||||
class="bank-item"
|
|
||||||
@click="selectBank(bank)"
|
|
||||||
>
|
|
||||||
<span class="bank-name">{{ bank.longName }}</span>
|
|
||||||
<span class="bank-ispb">{{ bank.ISPB }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.bank-selector {
|
|
||||||
@apply relative w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-input {
|
|
||||||
@apply w-full px-4 py-3 border-none outline-none rounded-lg bg-white text-gray-900 text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-input:focus {
|
|
||||||
@apply ring-2 ring-indigo-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-input:disabled {
|
|
||||||
@apply opacity-50 cursor-not-allowed bg-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-list {
|
|
||||||
@apply absolute top-full left-0 right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 max-h-64 overflow-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-item {
|
|
||||||
@apply flex justify-between items-center px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-name {
|
|
||||||
@apply text-gray-900 font-medium text-sm flex-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bank-ispb {
|
|
||||||
@apply text-gray-500 text-xs ml-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animação */
|
|
||||||
.dropdown-fade-enter-active,
|
|
||||||
.dropdown-fade-leave-active {
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-fade-enter-from,
|
|
||||||
.dropdown-fade-leave-to {
|
|
||||||
@apply opacity-0 -translate-y-2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||||
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
<script setup lang="ts" generic="T">
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { onClickOutside } from '@vueuse/core';
|
|
||||||
import ChevronDown from '@/assets/chevronDown.svg';
|
|
||||||
|
|
||||||
defineOptions({ name: 'UiDropdown' });
|
|
||||||
|
|
||||||
export interface DropdownItem<T = any> {
|
|
||||||
value: T;
|
|
||||||
label: string;
|
|
||||||
icon?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
items: DropdownItem<T>[];
|
|
||||||
modelValue: T;
|
|
||||||
placeholder?: string;
|
|
||||||
searchable?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
showIcon?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placeholder: 'Selecione...',
|
|
||||||
searchable: false,
|
|
||||||
disabled: false,
|
|
||||||
size: 'md',
|
|
||||||
showIcon: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: T];
|
|
||||||
change: [value: T];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isOpen = ref(false);
|
|
||||||
const searchQuery = ref('');
|
|
||||||
const dropdownRef = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const selectedItem = computed(() => {
|
|
||||||
return props.items.find((item) => item.value === props.modelValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
|
||||||
if (!props.searchable || !searchQuery.value) {
|
|
||||||
return props.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
|
||||||
return props.items.filter((item) => item.label.toLowerCase().includes(query));
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
|
||||||
if (!props.disabled) {
|
|
||||||
isOpen.value = !isOpen.value;
|
|
||||||
if (!isOpen.value) {
|
|
||||||
searchQuery.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectItem = (item: DropdownItem<T>) => {
|
|
||||||
if (!item.disabled) {
|
|
||||||
emit('update:modelValue', item.value);
|
|
||||||
emit('change', item.value);
|
|
||||||
isOpen.value = false;
|
|
||||||
searchQuery.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickOutside(dropdownRef, () => {
|
|
||||||
isOpen.value = false;
|
|
||||||
searchQuery.value = '';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="dropdownRef" class="dropdown-container">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="[
|
|
||||||
'dropdown-trigger',
|
|
||||||
`size-${size}`,
|
|
||||||
{ disabled: disabled, open: isOpen },
|
|
||||||
]"
|
|
||||||
@click="toggleDropdown"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="selectedItem?.icon && showIcon"
|
|
||||||
:src="selectedItem.icon"
|
|
||||||
:alt="selectedItem.label"
|
|
||||||
class="item-icon"
|
|
||||||
/>
|
|
||||||
<span class="selected-text">
|
|
||||||
{{ selectedItem?.label || placeholder }}
|
|
||||||
</span>
|
|
||||||
<ChevronDown class="chevron" :class="{ rotated: isOpen }" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<transition name="dropdown-fade">
|
|
||||||
<div v-if="isOpen" class="dropdown-menu">
|
|
||||||
<input
|
|
||||||
v-if="searchable"
|
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="Buscar..."
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="items-container">
|
|
||||||
<div
|
|
||||||
v-for="item in filteredItems"
|
|
||||||
:key="String(item.value)"
|
|
||||||
:class="[
|
|
||||||
'dropdown-item',
|
|
||||||
{
|
|
||||||
selected: item.value === modelValue,
|
|
||||||
disabled: item.disabled,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
@click="selectItem(item)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="item.icon && showIcon"
|
|
||||||
:src="item.icon"
|
|
||||||
:alt="item.label"
|
|
||||||
class="item-icon"
|
|
||||||
/>
|
|
||||||
<span class="item-label">{{ item.label }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="filteredItems.length === 0" class="no-results">
|
|
||||||
Nenhum resultado encontrado
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.dropdown-container {
|
|
||||||
@apply relative inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-trigger {
|
|
||||||
@apply flex items-center gap-2 bg-gray-300 hover:bg-gray-200 rounded-3xl transition-colors cursor-pointer border-none outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-trigger:focus {
|
|
||||||
@apply outline-2 outline-indigo-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-trigger.disabled {
|
|
||||||
@apply opacity-50 cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-trigger.disabled:hover {
|
|
||||||
@apply bg-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-sm {
|
|
||||||
@apply px-2 py-1 text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-md {
|
|
||||||
@apply px-3 py-2 text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-lg {
|
|
||||||
@apply px-4 py-3 text-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-icon {
|
|
||||||
@apply sm:w-fit w-4 flex-shrink-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-text {
|
|
||||||
@apply text-gray-900 font-medium min-w-fit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron {
|
|
||||||
@apply transition-transform duration-300 invert pr-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron.rotated {
|
|
||||||
@apply rotate-180;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
@apply absolute right-0 mt-2 bg-white rounded-xl border border-gray-300 shadow-md z-50 min-w-max w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
@apply w-full px-4 py-3 border-b border-gray-200 outline-none text-gray-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
@apply border-indigo-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-container {
|
|
||||||
@apply max-h-64 overflow-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
@apply flex items-center gap-2 px-4 py-4 cursor-pointer hover:bg-gray-300 transition-colors text-gray-900 font-semibold text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.selected {
|
|
||||||
@apply bg-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.disabled {
|
|
||||||
@apply opacity-50 cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.disabled:hover {
|
|
||||||
@apply bg-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-label {
|
|
||||||
@apply text-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results {
|
|
||||||
@apply px-4 py-6 text-center text-gray-500 text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animação */
|
|
||||||
.dropdown-fade-enter-active,
|
|
||||||
.dropdown-fade-leave-active {
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-fade-enter-from,
|
|
||||||
.dropdown-fade-leave-to {
|
|
||||||
@apply opacity-0 -translate-y-2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
export type ErrorType = 'error' | 'warning' | 'info';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
message: string;
|
|
||||||
type?: ErrorType;
|
|
||||||
centered?: boolean;
|
|
||||||
icon?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
type: 'error',
|
|
||||||
centered: true,
|
|
||||||
icon: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const colorClasses = {
|
|
||||||
error: 'text-red-500',
|
|
||||||
warning: 'text-amber-500',
|
|
||||||
info: 'text-blue-500',
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="['error-message-container', { centered: centered }]">
|
|
||||||
<div :class="['error-message', colorClasses[type]]">
|
|
||||||
<span v-if="icon" class="icon">⚠</span>
|
|
||||||
<span class="message">{{ message }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.error-message-container {
|
|
||||||
@apply flex w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message-container.centered {
|
|
||||||
@apply justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
@apply font-normal text-sm flex items-center gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
@apply text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
@apply leading-tight;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export type FormCardPadding = 'sm' | 'md' | 'lg';
|
type FormCardPadding = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||||
export type IconButtonSize = 'sm' | 'md' | 'lg';
|
type IconButtonSize = 'sm' | 'md' | 'lg';
|
||||||
export type IconPosition = 'left' | 'right';
|
type IconPosition = 'left' | 'right';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useFloating, arrow, offset, flip, shift } from '@floating-ui/vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
text: string;
|
|
||||||
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
||||||
iconSrc?: string;
|
|
||||||
showOnHover?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placement: 'right',
|
|
||||||
iconSrc: '',
|
|
||||||
showOnHover: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const showTooltip = ref<boolean>(false);
|
|
||||||
const reference = ref<HTMLElement | null>(null);
|
|
||||||
const floating = ref<HTMLElement | null>(null);
|
|
||||||
const floatingArrow = ref(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
useFloating(reference, floating, {
|
|
||||||
placement: props.placement,
|
|
||||||
middleware: [
|
|
||||||
offset(10),
|
|
||||||
flip(),
|
|
||||||
shift(),
|
|
||||||
arrow({ element: floatingArrow }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
|
||||||
if (props.showOnHover) {
|
|
||||||
showTooltip.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseOut = () => {
|
|
||||||
if (props.showOnHover) {
|
|
||||||
showTooltip.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTooltip = () => {
|
|
||||||
if (!props.showOnHover) {
|
|
||||||
showTooltip.value = !showTooltip.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="info-tooltip-container">
|
|
||||||
<img
|
|
||||||
:src="iconSrc || '/src/assets/info.svg'"
|
|
||||||
alt="info icon"
|
|
||||||
class="info-icon"
|
|
||||||
ref="reference"
|
|
||||||
@mouseover="handleMouseOver"
|
|
||||||
@mouseout="handleMouseOut"
|
|
||||||
@click="toggleTooltip"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="showTooltip"
|
|
||||||
role="tooltip"
|
|
||||||
ref="floating"
|
|
||||||
class="tooltip-content"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.info-tooltip-container {
|
|
||||||
@apply relative inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-icon {
|
|
||||||
@apply cursor-pointer transition-opacity hover:opacity-70;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-content {
|
|
||||||
@apply bg-white text-gray-900 font-medium text-xs md:text-sm px-3 py-2 rounded border-2 border-emerald-500 z-50 max-w-xs shadow-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.tooltip-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import SpinnerComponent from './SpinnerComponent.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
message?: string;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
centered?: boolean;
|
|
||||||
inline?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
message: 'Carregando...',
|
|
||||||
size: 'md',
|
|
||||||
centered: true,
|
|
||||||
inline: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const sizeMap = {
|
|
||||||
sm: { spinner: '4', text: 'text-sm' },
|
|
||||||
md: { spinner: '6', text: 'text-base' },
|
|
||||||
lg: { spinner: '8', text: 'text-lg' },
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="['loading-state', { centered: centered, inline: inline }]">
|
|
||||||
<span v-if="message" :class="['loading-message', sizeMap[size].text]">
|
|
||||||
{{ message }}
|
|
||||||
</span>
|
|
||||||
<SpinnerComponent
|
|
||||||
:width="sizeMap[size].spinner"
|
|
||||||
:height="sizeMap[size].spinner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.loading-state {
|
|
||||||
@apply flex items-center gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state.centered {
|
|
||||||
@apply justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state.inline {
|
|
||||||
@apply inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-message {
|
|
||||||
@apply text-gray-900 font-normal;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { getNetworkImage } from '@/utils/imagesPath';
|
|
||||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
networks: NetworkConfig[];
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
showLabel?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
size: 'md',
|
|
||||||
showLabel: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const sizeMap = {
|
|
||||||
sm: 16,
|
|
||||||
md: 24,
|
|
||||||
lg: 32,
|
|
||||||
};
|
|
||||||
|
|
||||||
const networkData = computed(() => {
|
|
||||||
return props.networks.map((network) => ({
|
|
||||||
network,
|
|
||||||
image: getNetworkImage(network.name),
|
|
||||||
name: network.name,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="network-badges">
|
|
||||||
<div
|
|
||||||
v-for="data in networkData"
|
|
||||||
:key="data.network.id"
|
|
||||||
class="network-badge"
|
|
||||||
:title="data.name"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:alt="`${data.name} logo`"
|
|
||||||
:src="data.image"
|
|
||||||
:width="sizeMap[size]"
|
|
||||||
:height="sizeMap[size]"
|
|
||||||
class="network-icon"
|
|
||||||
/>
|
|
||||||
<span v-if="showLabel" class="network-label">
|
|
||||||
{{ data.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.network-badges {
|
|
||||||
@apply flex gap-2 items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-badge {
|
|
||||||
@apply flex items-center gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-icon {
|
|
||||||
@apply flex-shrink-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-label {
|
|
||||||
@apply text-sm font-medium text-gray-900;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { Networks } from '@/config/networks';
|
|
||||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
|
||||||
import { getNetworkImage } from '@/utils/imagesPath';
|
|
||||||
import Dropdown, { type DropdownItem } from './Dropdown.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
modelValue: NetworkConfig;
|
|
||||||
disabled?: boolean;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
availableNetworks?: NetworkConfig[];
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
size: 'md',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: NetworkConfig];
|
|
||||||
change: [value: NetworkConfig];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const networkItems = computed((): DropdownItem<NetworkConfig>[] => {
|
|
||||||
return Object.values(Networks).map((network) => ({
|
|
||||||
value: network,
|
|
||||||
label: network.name,
|
|
||||||
icon: getNetworkImage(network.name),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange = (value: NetworkConfig) => {
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
emit('change', value);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Dropdown
|
|
||||||
:model-value="modelValue"
|
|
||||||
:items="networkItems"
|
|
||||||
:disabled="disabled"
|
|
||||||
:size="size"
|
|
||||||
:show-icon="true"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
export type HeaderSize = 'sm' | 'md' | 'lg';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
size?: HeaderSize;
|
|
||||||
centered?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
size: 'lg',
|
|
||||||
centered: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="['page-header', `size-${size}`, { centered: centered }]">
|
|
||||||
<h1 class="title text-white font-extrabold">
|
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
<p v-if="subtitle" class="subtitle text-white font-medium">
|
|
||||||
{{ subtitle }}
|
|
||||||
</p>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.page-header {
|
|
||||||
@apply flex flex-col gap-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header.centered {
|
|
||||||
@apply items-center justify-center text-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tamanhos */
|
|
||||||
.size-sm .title {
|
|
||||||
@apply sm:text-2xl text-xl sm:max-w-[20rem] max-w-[16rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-sm .subtitle {
|
|
||||||
@apply sm:text-sm text-xs sm:max-w-[18rem] max-w-[14rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-md .title {
|
|
||||||
@apply sm:text-4xl text-2xl sm:max-w-[28rem] max-w-[22rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-md .subtitle {
|
|
||||||
@apply sm:text-base text-sm sm:max-w-[26rem] max-w-[20rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-lg .title {
|
|
||||||
@apply sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-lg .subtitle {
|
|
||||||
@apply sm:text-base text-sm sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { TokenEnum } from '@/model/NetworkEnum';
|
|
||||||
import { getTokenImage } from '@/utils/imagesPath';
|
|
||||||
import Dropdown, { type DropdownItem } from './Dropdown.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
modelValue: TokenEnum;
|
|
||||||
disabled?: boolean;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
size: 'md',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: TokenEnum];
|
|
||||||
change: [value: TokenEnum];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const tokenItems = computed((): DropdownItem<TokenEnum>[] => {
|
|
||||||
return Object.values(TokenEnum).map((token) => ({
|
|
||||||
value: token,
|
|
||||||
label: token,
|
|
||||||
icon: getTokenImage(token),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange = (value: TokenEnum) => {
|
|
||||||
emit('update:modelValue', value);
|
|
||||||
emit('change', value);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Dropdown
|
|
||||||
:model-value="modelValue"
|
|
||||||
:items="tokenItems"
|
|
||||||
:disabled="disabled"
|
|
||||||
:size="size"
|
|
||||||
:show-icon="true"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { onClickOutside } from '@vueuse/core';
|
|
||||||
import CustomButton from './CustomButton.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
walletAddress: string | null;
|
|
||||||
variant?: 'primary' | 'secondary' | 'outline';
|
|
||||||
showMenu?: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
variant: 'primary',
|
|
||||||
showMenu: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
connect: [];
|
|
||||||
disconnect: [];
|
|
||||||
viewTransactions: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const menuOpen = ref(false);
|
|
||||||
const menuRef = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const isConnected = computed(() => {
|
|
||||||
return !!props.walletAddress;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedAddress = computed(() => {
|
|
||||||
if (!props.walletAddress) return '';
|
|
||||||
|
|
||||||
const address = props.walletAddress;
|
|
||||||
const length = address.length;
|
|
||||||
const start = address.substring(0, 5);
|
|
||||||
const end = address.substring(length - 4, length);
|
|
||||||
|
|
||||||
return `${start}...${end}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleConnect = () => {
|
|
||||||
emit('connect');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
menuOpen.value = false;
|
|
||||||
emit('disconnect');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewTransactions = () => {
|
|
||||||
menuOpen.value = false;
|
|
||||||
emit('viewTransactions');
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
if (isConnected.value && props.showMenu) {
|
|
||||||
menuOpen.value = !menuOpen.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickOutside(menuRef, () => {
|
|
||||||
menuOpen.value = false;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="wallet-connect-container">
|
|
||||||
<CustomButton
|
|
||||||
v-if="!isConnected"
|
|
||||||
text="Conectar carteira"
|
|
||||||
:variant="variant"
|
|
||||||
@button-clicked="handleConnect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else ref="menuRef" class="wallet-connected">
|
|
||||||
<button type="button" class="wallet-button" @click="toggleMenu">
|
|
||||||
<span class="wallet-address">{{ formattedAddress }}</span>
|
|
||||||
<div class="wallet-indicator"></div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<transition name="menu-fade">
|
|
||||||
<div v-if="menuOpen && showMenu" class="wallet-menu">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="menu-item"
|
|
||||||
@click="handleViewTransactions"
|
|
||||||
>
|
|
||||||
<span>Ver transações</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="menu-item disconnect"
|
|
||||||
@click="handleDisconnect"
|
|
||||||
>
|
|
||||||
<span>Desconectar</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "tailwindcss";
|
|
||||||
.wallet-connect-container {
|
|
||||||
@apply relative inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-connected {
|
|
||||||
@apply relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-button {
|
|
||||||
@apply flex items-center gap-3 px-4 py-2 bg-white border-2 border-amber-400 rounded-lg hover:bg-amber-50 transition-colors cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-address {
|
|
||||||
@apply text-gray-900 font-semibold text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-indicator {
|
|
||||||
@apply w-2 h-2 bg-emerald-500 rounded-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-menu {
|
|
||||||
@apply absolute top-full right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 min-w-[200px] overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
@apply w-full px-4 py-3 text-left text-gray-900 font-medium text-sm hover:bg-gray-100 transition-colors cursor-pointer border-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item.disconnect {
|
|
||||||
@apply text-red-500 hover:bg-red-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animação */
|
|
||||||
.menu-fade-enter-active,
|
|
||||||
.menu-fade-leave-active {
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-fade-enter-from,
|
|
||||||
.menu-fade-leave-to {
|
|
||||||
@apply opacity-0 -translate-y-2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -29,6 +29,7 @@ export const Networks: { [key: string]: NetworkConfig } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export const NetworksTestnet: { [key: string]: NetworkConfig } = {
|
export const NetworksTestnet: { [key: string]: NetworkConfig } = {
|
||||||
sepolia: {
|
sepolia: {
|
||||||
...sepolia,
|
...sepolia,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface Bank {
|
|
||||||
COMPE: string;
|
|
||||||
ISPB: string;
|
|
||||||
longName: string;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
export type Faq = Section[];
|
export type Faq = Section[];
|
||||||
|
|
||||||
export type Section = {
|
type Section = {
|
||||||
name: string;
|
name: string;
|
||||||
items: Question[];
|
items: Question[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Question = {
|
type Question = {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export type Pix = {
|
|
||||||
pixKey: string;
|
|
||||||
merchantCity?: string;
|
|
||||||
merchantName?: string;
|
|
||||||
value?: number;
|
|
||||||
transactionId?: string;
|
|
||||||
message?: string;
|
|
||||||
cep?: string;
|
|
||||||
currency?: number;
|
|
||||||
countryCode?: string;
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,7 @@ export interface Participant {
|
|||||||
savingsVariation?: string;
|
savingsVariation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParticipantWithID extends Participant {
|
interface ParticipantWithID extends Participant {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
export const pixFormatValidation = (pixKey: string): boolean => {
|
|
||||||
const cpf = /(^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$)/g;
|
|
||||||
const cnpj = /(^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$)/g;
|
|
||||||
const telefone = /(^[0-9]{2})?(\s|-)?(9?[0-9]{4})-?([0-9]{4}$)/g;
|
|
||||||
|
|
||||||
if (pixKey.match(cpf) || pixKey.match(cnpj) || pixKey.match(telefone)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postProcessKey = (pixKey: string): string => {
|
export const postProcessKey = (pixKey: string): string => {
|
||||||
pixKey = pixKey.replace(/[-.()/]/g, '');
|
pixKey = pixKey.replace(/[-.()/]/g, '');
|
||||||
return pixKey;
|
return pixKey;
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ export function getLatestVersion(): AppVersion | null {
|
|||||||
return appVersions.length > 0 ? appVersions[0] : null;
|
return appVersions.length > 0 ? appVersions[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVersionByTag(tag: string): AppVersion | null {
|
|
||||||
return appVersions.find((v) => v.tag === tag) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getIpfsUrl(ipfsHash: string): string {
|
export function getIpfsUrl(ipfsHash: string): string {
|
||||||
return `https://${ipfsHash}.ipfs.dweb.link`;
|
return `https://${ipfsHash}.ipfs.dweb.link`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user