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:
2026-05-07 20:06:29 -03:00
committed by hueso
parent 5dc630acdf
commit 00390ab0c3
26 changed files with 46 additions and 2349 deletions

904
bun.lock

File diff suppressed because it is too large Load Diff

22
knip.json Normal file
View 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"]
}

View File

@@ -16,19 +16,10 @@
},
"dependencies": {
"@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",
"@wagmi/core": "^3.4.8",
"@wagmi/vue": "^0.5.11",
"@web3-onboard/injected-wallets": "^2.11.3",
"@web3-onboard/vue": "^2.10.0",
"graphql": "^16.13.2",
"marked": "^18.0.3",
"permissionless": "^0.2.57",
"qrcode": "^1.5.4",
"viem": "^2.48.8",
"vite-svg-loader": "^5.1.1",
@@ -36,12 +27,6 @@
"vue-router": "^5.0.6"
},
"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",
"@tailwindcss/vite": "^4.2.4",
"@types/node": "^25.6.0",
@@ -51,15 +36,12 @@
"@vitest/coverage-v8": "^4.1.5",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/test-utils": "^2.4.10",
"@vue/tsconfig": "^0.9.1",
"@wagmi/cli": "^2.10.0",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-vue": "^10.9.0",
"happy-dom": "^20.9.0",
"lighthouse": "^13.2.0",
"playwright-lighthouse": "^4.0.0",
"postcss": "^8.5.8",
"prettier": "^3.5.3",
"tailwindcss": "^4.2.4",

View File

@@ -197,72 +197,6 @@ export const listAllTransactionByWalletAddress = async (
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 user = useUser();
const network = user.network.value;

View File

@@ -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 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
const props = withDefaults(
defineProps<{

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
export type FormCardPadding = 'sm' | 'md' | 'lg';
type FormCardPadding = 'sm' | 'md' | 'lg';
const props = withDefaults(
defineProps<{

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
export type IconButtonSize = 'sm' | 'md' | 'lg';
export type IconPosition = 'left' | 'right';
type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
type IconButtonSize = 'sm' | 'md' | 'lg';
type IconPosition = 'left' | 'right';
const props = withDefaults(
defineProps<{

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -29,6 +29,7 @@ export const Networks: { [key: string]: NetworkConfig } = {
},
};
/** @public */
export const NetworksTestnet: { [key: string]: NetworkConfig } = {
sepolia: {
...sepolia,

View File

@@ -1,5 +0,0 @@
export interface Bank {
COMPE: string;
ISPB: string;
longName: string;
}

View File

@@ -1,11 +1,11 @@
export type Faq = Section[];
export type Section = {
type Section = {
name: string;
items: Question[];
};
export type Question = {
type Question = {
title: string;
content: string;
isOpen?: boolean;

View File

@@ -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;
};

View File

@@ -9,7 +9,7 @@ export interface Participant {
savingsVariation?: string;
}
export interface ParticipantWithID extends Participant {
interface ParticipantWithID extends Participant {
id: string;
}

View File

@@ -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 => {
pixKey = pixKey.replace(/[-.()/]/g, '');
return pixKey;

View File

@@ -19,10 +19,6 @@ export function getLatestVersion(): AppVersion | 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 {
return `https://${ipfsHash}.ipfs.dweb.link`;
}