diff --git a/.github/workflows/CI-CD-k8s-gear-js-stage.yml b/.github/workflows/CI-CD-k8s-gear-js-stage.yml index 61e4db767e..af2bf722e0 100644 --- a/.github/workflows/CI-CD-k8s-gear-js-stage.yml +++ b/.github/workflows/CI-CD-k8s-gear-js-stage.yml @@ -1,4 +1,4 @@ -name: "Gear Idea: deploy to k8s stage" +name: 'Gear Idea: deploy to k8s stage' on: push: @@ -23,9 +23,7 @@ env: AWS_REGION: ${{ secrets.AWS_REGION }} KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} - jobs: - build-frontend-image-staging: runs-on: ubuntu-latest environment: staging @@ -58,6 +56,7 @@ jobs: VITE_NODES_API_URL= ${{ secrets.REACT_APP_DEFAULT_NODES_URL }} VITE_MAINNET_VOUCHERS_API_URL=${{ secrets.VITE_MAINNET_VOUCHERS_API_URL }} VITE_TESTNET_VOUCHERS_API_URL=${{ secrets.VITE_TESTNET_VOUCHERS_API_URL }} + VITE_DNS_API_URL=${{ secrets.VITE_DNS_API_URL }} build-indexer-image-staging: runs-on: ubuntu-latest @@ -161,12 +160,13 @@ jobs: tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-meta-storage:qa deploy-to-k8s-staging: - needs: [ + needs: + [ build-frontend-image-staging, build-indexer-image-staging, build-api-gateway-image-staging, build-test-balance-image-staging, - build-and-push-meta-storage-image + build-and-push-meta-storage-image, ] runs-on: ubuntu-latest diff --git a/.github/workflows/CI-CD-k8s-release-deploy.yml b/.github/workflows/CI-CD-k8s-release-deploy.yml index fd512f0fb9..b0cd6dfd5c 100644 --- a/.github/workflows/CI-CD-k8s-release-deploy.yml +++ b/.github/workflows/CI-CD-k8s-release-deploy.yml @@ -67,6 +67,7 @@ jobs: VITE_GTM_ID=${{ secrets.VITE_GTM_ID}} VITE_MAINNET_VOUCHERS_API_URL=${{ secrets.VITE_MAINNET_VOUCHERS_API_URL }} VITE_TESTNET_VOUCHERS_API_URL=${{ secrets.VITE_TESTNET_VOUCHERS_API_URL }} + VITE_DNS_API_URL=${{ secrets.VITE_DNS_API_URL }} build-indexer-image-prod: runs-on: ubuntu-latest diff --git a/idea/frontend/.env.example b/idea/frontend/.env.example index f7010c3d74..fd694a6516 100644 --- a/idea/frontend/.env.example +++ b/idea/frontend/.env.example @@ -6,3 +6,4 @@ VITE_NODE_ADDRESS= VITE_HCAPTCHA_SITE_KEY= VITE_GTM_ID= VITE_DEFAULT_TRANSFER_BALANCE_VALUE= +VITE_DNS_API_URL= diff --git a/idea/frontend/Dockerfile b/idea/frontend/Dockerfile index 923e7fed6d..99885468f9 100644 --- a/idea/frontend/Dockerfile +++ b/idea/frontend/Dockerfile @@ -21,7 +21,8 @@ ARG VITE_NODE_ADDRESS \ VITE_HCAPTCHA_SITE_KEY \ VITE_GTM_ID \ VITE_MAINNET_VOUCHERS_API_URL \ - VITE_TESTNET_VOUCHERS_API_URL + VITE_TESTNET_VOUCHERS_API_URL \ + VITE_DNS_API_URL ENV VITE_NODE_ADDRESS=${VITE_NODE_ADDRESS} \ VITE_VOUCHERS_API_URL=${VITE_VOUCHERS_API_URL} \ VITE_API_URL=${VITE_API_URL} \ @@ -30,7 +31,8 @@ ENV VITE_NODE_ADDRESS=${VITE_NODE_ADDRESS} \ VITE_HCAPTCHA_SITE_KEY=${VITE_HCAPTCHA_SITE_KEY} \ VITE_GTM_ID=${VITE_GTM_ID} \ VITE_MAINNET_VOUCHERS_API_URL=${VITE_MAINNET_VOUCHERS_API_URL} \ - VITE_TESTNET_VOUCHERS_API_URL=${VITE_TESTNET_VOUCHERS_API_URL} + VITE_TESTNET_VOUCHERS_API_URL=${VITE_TESTNET_VOUCHERS_API_URL} \ + VITE_DNS_API_URL=${VITE_DNS_API_URL} RUN yarn build:frontend diff --git a/idea/frontend/src/features/dns/assets/dns-card-placeholder.svg b/idea/frontend/src/features/dns/assets/dns-card-placeholder.svg new file mode 100644 index 0000000000..d9616f1cad --- /dev/null +++ b/idea/frontend/src/features/dns/assets/dns-card-placeholder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/idea/frontend/src/features/dns/consts/index.ts b/idea/frontend/src/features/dns/consts/index.ts new file mode 100644 index 0000000000..82888bfc50 --- /dev/null +++ b/idea/frontend/src/features/dns/consts/index.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { Values } from '../types'; +import { Program } from './sails'; + +const DNS_API_URL = import.meta.env.VITE_DNS_API_URL as string; + +const DNS_PROGRAM_QUERY_KEY = ['dnsProgram']; + +const FIELD_NAME = { + DNS_NAME: 'name', + DNS_ADDRESS: 'address', +} as const; + +const DEFAULT_VALUES: Values = { + [FIELD_NAME.DNS_ADDRESS]: '', + [FIELD_NAME.DNS_NAME]: '', +}; + +const domainNameRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +const NAME_SCHEMA = z.string().trim().regex(domainNameRegex, 'Invalid domain name'); + +export { DNS_API_URL, DNS_PROGRAM_QUERY_KEY, FIELD_NAME, DEFAULT_VALUES, NAME_SCHEMA, Program }; diff --git a/idea/frontend/src/features/dns/consts/sails.ts b/idea/frontend/src/features/dns/consts/sails.ts new file mode 100644 index 0000000000..53f895107b --- /dev/null +++ b/idea/frontend/src/features/dns/consts/sails.ts @@ -0,0 +1,72 @@ +import { GearApi } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; +import { TransactionBuilder } from 'sails-js'; + +export type ActorId = `0x${string}`; + +export interface ContractInfo { + admin: ActorId; + program_id: ActorId; + registration_time: string; +} + +export class Program { + public readonly registry: TypeRegistry; + public readonly dns: Dns; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types = { + ActorId: '([u8; 32])', + ContractInfo: { admin: 'ActorId', program_id: 'ActorId', registration_time: 'String' }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.dns = new Dns(this); + } +} + +export class Dns { + constructor(private _program: Program) {} + + public addNewProgram(name: string, program_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Dns', 'AddNewProgram', name, program_id], + '(String, String, String, ActorId)', + 'Null', + this._program.programId, + ); + } + + public changeProgramId(name: string, new_program_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Dns', 'ChangeProgramId', name, new_program_id], + '(String, String, String, ActorId)', + 'Null', + this._program.programId, + ); + } + + public deleteProgram(name: string): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Dns', 'DeleteProgram', name], + '(String, String, String)', + 'Null', + this._program.programId, + ); + } +} diff --git a/idea/frontend/src/features/dns/hooks/index.ts b/idea/frontend/src/features/dns/hooks/index.ts new file mode 100644 index 0000000000..3ece77313b --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/index.ts @@ -0,0 +1,8 @@ +import { useDnsSort } from './use-dns-sort'; +import { useDns } from './use-dns'; +import { useDnsFilters } from './use-dns-filters'; +import { useDnsActions } from './use-dns-actions'; +import { useInitDnsProgram } from './use-init-dns-program'; +import { useDnsSchema } from './use-dns-schema'; + +export { useDns, useDnsFilters, useDnsSort, useDnsActions, useInitDnsProgram, useDnsSchema }; diff --git a/idea/frontend/src/features/dns/hooks/use-dns-actions.ts b/idea/frontend/src/features/dns/hooks/use-dns-actions.ts new file mode 100644 index 0000000000..e1cbd7dc6f --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-dns-actions.ts @@ -0,0 +1,85 @@ +import { HexString } from '@gear-js/api'; +import { useAccount } from '@gear-js/react-hooks'; +import { web3FromSource } from '@polkadot/extension-dapp'; +import { TransactionBuilder } from 'sails-js'; + +import { Method } from '@/features/explorer'; +import { useLoading, useModal, useSignAndSend } from '@/hooks'; +import { TransactionName } from '@/shared/config'; + +import { DNS_PROGRAM_QUERY_KEY, Program } from '../consts'; +import { useQueryClient } from '@tanstack/react-query'; + +type ResolveRejectOptions = { + resolve?: () => void; + reject?: () => void; +}; + +type SendDnsMessage = { + getTransactionBuilder: () => TransactionBuilder; + options?: ResolveRejectOptions; +}; + +const useDnsActions = () => { + const [isLoading, enableLoading, disableLoading] = useLoading(); + const { showModal } = useModal(); + const { account } = useAccount(); + const signAndSend = useSignAndSend(); + const queryClient = useQueryClient(); + const state = queryClient.getQueryState(DNS_PROGRAM_QUERY_KEY); + const dnsProgram = state?.data; + + const sendMessage = async ({ getTransactionBuilder, options }: SendDnsMessage) => { + if (!account || !dnsProgram) { + return; + } + const { resolve: onSuccess, reject: onError } = options || {}; + const { signer } = await web3FromSource(account.meta.source); + const transaction = getTransactionBuilder(); + transaction.withAccount(account.address, { signer }); + await transaction.calculateGas(); + + const extrinsic = transaction.extrinsic; + const { partialFee } = await extrinsic.paymentInfo(account.address, { signer }); + + const handleConfirm = () => { + enableLoading(); + signAndSend(extrinsic, Method.MessageQueued, { + onSuccess, + onError, + onFinally: () => disableLoading(), + }); + }; + + showModal('transaction', { + fee: partialFee.toHuman(), + name: TransactionName.SendMessage, + addressFrom: account.address, + addressTo: dnsProgram.programId, + onAbort: onError, + onConfirm: handleConfirm, + }); + }; + + const addNewProgram = (name: string, program_id: HexString, options?: ResolveRejectOptions) => { + if (!dnsProgram) throw new Error('dnsProgram is not initialized'); + const getTransactionBuilder = () => dnsProgram.dns.addNewProgram(name, program_id); + return sendMessage({ getTransactionBuilder, options }); + }; + + const changeProgramId = (name: string, program_id: HexString, options?: ResolveRejectOptions) => { + if (!dnsProgram) throw new Error('dnsProgram is not initialized'); + const getTransactionBuilder = () => dnsProgram.dns.changeProgramId(name, program_id); + return sendMessage({ getTransactionBuilder, options }); + }; + + const deleteProgram = (name: string, options?: ResolveRejectOptions) => { + if (!dnsProgram) throw new Error('dnsProgram is not initialized'); + const getTransactionBuilder = () => dnsProgram.dns.deleteProgram(name); + return sendMessage({ getTransactionBuilder, options }); + }; + + return { isLoading, addNewProgram, changeProgramId, deleteProgram }; +}; + +export { useDnsActions }; diff --git a/idea/frontend/src/features/dns/hooks/use-dns-filters.ts b/idea/frontend/src/features/dns/hooks/use-dns-filters.ts new file mode 100644 index 0000000000..3a5d655604 --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-dns-filters.ts @@ -0,0 +1,26 @@ +import { useAccount } from '@gear-js/react-hooks'; +import { useMemo, useState } from 'react'; + +const DEFAULT_FILTER_VALUES = { + owner: 'all', +}; + +function useDnsFilters() { + const { account } = useAccount(); + const [values, setValues] = useState(DEFAULT_FILTER_VALUES); + + const getOwnerParams = () => { + if (!account) return {}; + + const { decodedAddress } = account; + const { owner } = values; + + return owner === 'all' ? {} : { createdBy: decodedAddress }; + }; + + const params = useMemo(() => getOwnerParams(), [values, account]); + + return [values, params, setValues] as const; +} + +export { useDnsFilters }; diff --git a/idea/frontend/src/features/dns/hooks/use-dns-schema.ts b/idea/frontend/src/features/dns/hooks/use-dns-schema.ts new file mode 100644 index 0000000000..9df0c00a82 --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-dns-schema.ts @@ -0,0 +1,16 @@ +import { useProgramIdSchema } from '@/hooks'; +import { z } from 'zod'; +import { FIELD_NAME, NAME_SCHEMA } from '../consts'; + +const useDnsSchema = () => { + const programIdSchema = useProgramIdSchema([]); + + const dnsSchema = z.object({ + [FIELD_NAME.DNS_ADDRESS]: programIdSchema, + [FIELD_NAME.DNS_NAME]: NAME_SCHEMA, + }); + + return dnsSchema; +}; + +export { useDnsSchema }; diff --git a/idea/frontend/src/features/dns/hooks/use-dns-sort.ts b/idea/frontend/src/features/dns/hooks/use-dns-sort.ts new file mode 100644 index 0000000000..1837e00590 --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-dns-sort.ts @@ -0,0 +1,22 @@ +import { useState } from 'react'; +import { SortDirection } from '../types'; + +const DEFAULT_SORT_VALUES = { + orderByField: 'updatedAt', + orderByDirection: 'DESC' as SortDirection, +}; + +function useDnsSort() { + const [values, setValues] = useState(DEFAULT_SORT_VALUES); + + const toggleDirection = () => { + setValues(({ orderByField, orderByDirection }) => ({ + orderByField, + orderByDirection: orderByDirection === 'ASC' ? 'DESC' : 'ASC', + })); + }; + + return [values, toggleDirection, setValues] as const; +} + +export { useDnsSort }; diff --git a/idea/frontend/src/features/dns/hooks/use-dns.ts b/idea/frontend/src/features/dns/hooks/use-dns.ts new file mode 100644 index 0000000000..8ec8279d37 --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-dns.ts @@ -0,0 +1,23 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { DEFAULT_LIMIT } from '@/shared/config'; + +import { getDns, getNextPageParam } from '../utils'; +import { DnsFilterParams, DnsSortParams } from '../types'; + +function useDns(search: string, filterParams: DnsFilterParams, sortParams: DnsSortParams) { + const { data, isLoading, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({ + queryKey: ['dns', search, filterParams, sortParams], + queryFn: ({ pageParam }) => + getDns({ limit: DEFAULT_LIMIT, offset: pageParam, search, ...filterParams, ...sortParams }), + initialPageParam: 0, + getNextPageParam, + }); + + const dns = data?.pages.flatMap((page) => page.data) || []; + const dnsCount = data?.pages[0].count || 0; + + return [dns, dnsCount, isLoading, hasNextPage, fetchNextPage, refetch] as const; +} + +export { useDns }; diff --git a/idea/frontend/src/features/dns/hooks/use-init-dns-program.ts b/idea/frontend/src/features/dns/hooks/use-init-dns-program.ts new file mode 100644 index 0000000000..d692c7d31a --- /dev/null +++ b/idea/frontend/src/features/dns/hooks/use-init-dns-program.ts @@ -0,0 +1,30 @@ +import { useApi } from '@gear-js/react-hooks'; + +import { DNS_API_URL, DNS_PROGRAM_QUERY_KEY, Program } from '../consts'; +import { useQuery } from '@tanstack/react-query'; + +const useInitDnsProgram = () => { + const { isApiReady, api } = useApi(); + + const getDnsProgram = () => + fetch(`${DNS_API_URL}/dns/contract`).then((response) => { + return response.json().then(({ contract }) => { + const programId = contract; + if (isApiReady) { + return new Program(api, programId); + } + }); + }); + + const { data, isPending } = useQuery({ + queryKey: DNS_PROGRAM_QUERY_KEY, + queryFn: getDnsProgram, + enabled: isApiReady, + }); + + const isLoading = isPending; + + return { data, isLoading }; +}; + +export { useInitDnsProgram }; diff --git a/idea/frontend/src/features/dns/index.ts b/idea/frontend/src/features/dns/index.ts new file mode 100644 index 0000000000..ab00d95ef4 --- /dev/null +++ b/idea/frontend/src/features/dns/index.ts @@ -0,0 +1,8 @@ +import { DnsCard, CreateDns } from './ui'; +import DnsCardPlaceholder from './assets/dns-card-placeholder.svg?react'; +import { useDns, useDnsFilters, useDnsSort, useInitDnsProgram } from './hooks'; +import { Dns } from './types'; + +export { DnsCard, DnsCardPlaceholder, useDns, useDnsFilters, CreateDns, useDnsSort, useInitDnsProgram }; + +export type { Dns }; diff --git a/idea/frontend/src/features/dns/types.ts b/idea/frontend/src/features/dns/types.ts new file mode 100644 index 0000000000..8019b1b161 --- /dev/null +++ b/idea/frontend/src/features/dns/types.ts @@ -0,0 +1,36 @@ +import { HexString } from '@gear-js/api'; +import { FIELD_NAME } from './consts'; + +type Values = { + [FIELD_NAME.DNS_ADDRESS]: string; + [FIELD_NAME.DNS_NAME]: string; +}; + +type Dns = { + id: string; // same as 'name' at contract + name: string; + address: HexString; // same as 'program_id' at contract + createdBy: HexString; + createdAt: string; + updatedAt: string; +}; + +type SortDirection = 'ASC' | 'DESC'; +type DnsParams = { + limit: number; + offset: number; + createdBy?: string; + search?: string; + orderByField: string; + orderByDirection: SortDirection; +}; + +type DnsFilterParams = Pick; +type DnsSortParams = Pick; + +type DnsResponse = { + data: Dns[]; + count: number; +}; + +export type { Values, Dns, DnsParams, DnsResponse, DnsFilterParams, DnsSortParams, SortDirection }; diff --git a/idea/frontend/src/features/dns/ui/create-dns/create-dns.tsx b/idea/frontend/src/features/dns/ui/create-dns/create-dns.tsx new file mode 100644 index 0000000000..e5498cc4c5 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/create-dns/create-dns.tsx @@ -0,0 +1,25 @@ +import { Button } from '@gear-js/ui'; + +import { useModalState } from '@/hooks'; +import { withAccount } from '@/shared/ui'; +import EditSVG from '@/shared/assets/images/actions/edit.svg?react'; + +import { DnsModal } from '../dns-modal'; + +type Props = { + onSuccess: () => void; +}; + +const CreateDns = withAccount(({ onSuccess }: Props) => { + const [isModalOpen, openModal, closeModal] = useModalState(); + + return ( + <> + + + {isModalOpen && } + > + ); +}); + +export { CreateDns }; diff --git a/idea/frontend/src/features/dns/ui/create-dns/index.ts b/idea/frontend/src/features/dns/ui/create-dns/index.ts new file mode 100644 index 0000000000..9ae59d1cea --- /dev/null +++ b/idea/frontend/src/features/dns/ui/create-dns/index.ts @@ -0,0 +1,3 @@ +import { CreateDns } from './create-dns'; + +export { CreateDns }; diff --git a/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.module.scss b/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.module.scss new file mode 100644 index 0000000000..4947e777c0 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.module.scss @@ -0,0 +1,5 @@ +@use '@/shared/assets/styles/variables' as *; + +.link { + color: $gray800; +} diff --git a/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.tsx b/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.tsx new file mode 100644 index 0000000000..68a0340853 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/delete-dns/delete-dns.tsx @@ -0,0 +1,53 @@ +import { Button } from '@gear-js/ui'; + +import { useModalState } from '@/hooks'; +import TrashSVG from '@/shared/assets/images/actions/trash.svg?react'; +import { ConfirmModal } from '@/shared/ui/confirm-modal'; + +import { useDnsActions } from '../../hooks/use-dns-actions'; +import styles from './delete-dns.module.scss'; + +type Props = { + name: string; + onSuccess: () => void; +}; + +const DeleteDns = ({ name, onSuccess }: Props) => { + const [isModalOpen, openModal, closeModal] = useModalState(); + const { isLoading, deleteProgram } = useDnsActions(); + const text = `DNS '${name}' will be deleted. Are you sure?`; + + const onConfirm = async () => { + const resolve = () => { + onSuccess(); + closeModal(); + }; + deleteProgram(name, { resolve }); + }; + + return ( + <> + + {isModalOpen && ( + + )} + > + ); +}; + +export { DeleteDns }; diff --git a/idea/frontend/src/features/dns/ui/delete-dns/index.ts b/idea/frontend/src/features/dns/ui/delete-dns/index.ts new file mode 100644 index 0000000000..d062a826b2 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/delete-dns/index.ts @@ -0,0 +1,3 @@ +import { DeleteDns } from './delete-dns'; + +export { DeleteDns }; diff --git a/idea/frontend/src/features/dns/ui/dns-card/dns-card.module.scss b/idea/frontend/src/features/dns/ui/dns-card/dns-card.module.scss new file mode 100644 index 0000000000..37c6e9b0e2 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-card/dns-card.module.scss @@ -0,0 +1,77 @@ +@use '@/shared/assets/styles/shared' as *; +@use '@/shared/assets/styles/mixins' as *; +@use '@/shared/assets/styles/variables' as *; + +.card { + display: grid; + grid-template-columns: 3fr 1fr; + + background-color: #222225; + border-radius: 16px; + + position: relative; + overflow: hidden; + + button { + position: relative; + z-index: 1; + } + + &::after { + content: ''; + + width: 100%; + height: 100%; + + position: absolute; + top: 0; + left: 0; + + opacity: 0; + transition: opacity 0.25s; + + background-image: radial-gradient(75% 75% at 0% 100%, rgba(43, 208, 113, 0.1) 0, rgba(24, 24, 27, 0) 100%); + } + + &:hover { + &::after { + opacity: 1; + } + } +} + +.content { + display: grid; + grid-template-columns: 2fr 2fr 2fr; + gap: 16px; + padding: toRem(24); + overflow: hidden; + position: relative; + + &::after { + content: ''; + position: absolute; + right: -40%; + bottom: -100%; + width: 100%; + height: 200%; + background: radial-gradient(50% 50% at 50% 50%, rgba($successColor, 0.23) 0%, rgba(24, 24, 27, 0) 100%); + } +} + +.heading { + font-size: 16px; + font-weight: 600; + + font-family: Kanit; +} + +.actions { + @include childrenMargin(12px); + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + flex: 0.3 1; + padding: toRem(12) toRem(24); +} diff --git a/idea/frontend/src/features/dns/ui/dns-card/dns-card.tsx b/idea/frontend/src/features/dns/ui/dns-card/dns-card.tsx new file mode 100644 index 0000000000..d342b4166a --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-card/dns-card.tsx @@ -0,0 +1,38 @@ +import { IdBlock } from '@/shared/ui/idBlock'; +import { TimestampBlock } from '@/shared/ui/timestampBlock'; +import { Dns } from '../../types'; +import { useAccount } from '@gear-js/react-hooks'; +import { EditDns } from '../edit-dns'; +import { DeleteDns } from '../delete-dns'; +import styles from './dns-card.module.scss'; + +type Props = { + dns: Dns; + onSuccess: () => void; +}; + +function DnsCard({ dns, onSuccess }: Props) { + const { name, address, updatedAt, createdBy } = dns; + const { account } = useAccount(); + + const isOwner = createdBy === account?.decodedAddress; + + return ( + + + {name} + + + + + {isOwner && ( + + + + + )} + + ); +} + +export { DnsCard }; diff --git a/idea/frontend/src/features/dns/ui/dns-card/index.ts b/idea/frontend/src/features/dns/ui/dns-card/index.ts new file mode 100644 index 0000000000..f34c8d15df --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-card/index.ts @@ -0,0 +1,3 @@ +import { DnsCard } from './dns-card'; + +export { DnsCard }; diff --git a/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.module.scss b/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.module.scss new file mode 100644 index 0000000000..19bef7dcec --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.module.scss @@ -0,0 +1,19 @@ +@use '@/shared/assets/styles/mixins' as *; +@use '@/shared/assets/styles/variables' as *; + +.inputs { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 32px; + + :disabled { + color: $gray600; + } +} + +.buttons { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; +} diff --git a/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.tsx b/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.tsx new file mode 100644 index 0000000000..2ccf0160c6 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-modal/dns-modal.tsx @@ -0,0 +1,66 @@ +import { Button, Modal } from '@gear-js/ui'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; +import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; +import { Input } from '@/shared/ui'; + +import { DEFAULT_VALUES, FIELD_NAME } from '../../consts'; +import { Values } from '../../types'; +import { useDnsActions, useDnsSchema } from '../../hooks'; + +import styles from './dns-modal.module.scss'; + +type Props = { + close: () => void; + onSuccess: () => void; + heading: string; + submitText: string; + initialValues?: Values; +}; + +const DnsModal = ({ heading, submitText, close, onSuccess, initialValues }: Props) => { + const dnsSchema = useDnsSchema(); + const isEditMode = Boolean(initialValues); + + type DnsSchema = z.infer; + + const form = useForm({ + defaultValues: initialValues || DEFAULT_VALUES, + resolver: zodResolver(dnsSchema), + }); + + const { isLoading, addNewProgram, changeProgramId } = useDnsActions(); + + const handleSubmit = ({ name, address }: DnsSchema) => { + const resolve = () => { + onSuccess(); + close(); + }; + + const handler = isEditMode ? changeProgramId : addNewProgram; + handler(name, address, { resolve }); + }; + + return ( + + + + + + + + + + + + + + + + ); +}; + +export { DnsModal }; diff --git a/idea/frontend/src/features/dns/ui/dns-modal/index.tsx b/idea/frontend/src/features/dns/ui/dns-modal/index.tsx new file mode 100644 index 0000000000..bf52385d06 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/dns-modal/index.tsx @@ -0,0 +1,3 @@ +import { DnsModal } from './dns-modal'; + +export { DnsModal }; diff --git a/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.module.scss b/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.module.scss new file mode 100644 index 0000000000..4947e777c0 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.module.scss @@ -0,0 +1,5 @@ +@use '@/shared/assets/styles/variables' as *; + +.link { + color: $gray800; +} diff --git a/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.tsx b/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.tsx new file mode 100644 index 0000000000..8d28a8023e --- /dev/null +++ b/idea/frontend/src/features/dns/ui/edit-dns/edit-dns.tsx @@ -0,0 +1,43 @@ +import { Button } from '@gear-js/ui'; + +import { useModalState } from '@/hooks'; +import EditSVG from '@/shared/assets/images/actions/edit.svg?react'; + +import { Values } from '../../types'; +import { DnsModal } from '../dns-modal'; +import styles from './edit-dns.module.scss'; + +type Props = { + initialValues: Values; + onSuccess: () => void; +}; + +const EditDns = ({ onSuccess, initialValues }: Props) => { + const [isModalOpen, openModal, closeModal] = useModalState(); + + return ( + <> + + + {isModalOpen && ( + + )} + > + ); +}; + +export { EditDns }; diff --git a/idea/frontend/src/features/dns/ui/edit-dns/index.ts b/idea/frontend/src/features/dns/ui/edit-dns/index.ts new file mode 100644 index 0000000000..eac3748c10 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/edit-dns/index.ts @@ -0,0 +1,3 @@ +import { EditDns } from './edit-dns'; + +export { EditDns }; diff --git a/idea/frontend/src/features/dns/ui/index.ts b/idea/frontend/src/features/dns/ui/index.ts new file mode 100644 index 0000000000..4410566307 --- /dev/null +++ b/idea/frontend/src/features/dns/ui/index.ts @@ -0,0 +1,4 @@ +import { DnsCard } from './dns-card'; +import { CreateDns } from './create-dns'; + +export { DnsCard, CreateDns }; diff --git a/idea/frontend/src/features/dns/utils.ts b/idea/frontend/src/features/dns/utils.ts new file mode 100644 index 0000000000..2e93cbf8d8 --- /dev/null +++ b/idea/frontend/src/features/dns/utils.ts @@ -0,0 +1,22 @@ +import { DEFAULT_LIMIT } from '@/shared/config'; +import { fetchWithGuard } from '@/shared/helpers'; + +import { DnsParams, DnsResponse } from './types'; +import { DNS_API_URL } from './consts'; + +const getDns = (params: DnsParams) => { + const method = 'GET'; + const url = new URL(`${DNS_API_URL}/dns`); + Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, String(value))); + return fetchWithGuard(url.toString(), { method }); +}; + +const getNextPageParam = (lastPage: DnsResponse, allPages: DnsResponse[]) => { + const totalCount = lastPage.count; + const lastPageCount = lastPage.data.length; + const fetchedCount = (allPages.length - 1) * DEFAULT_LIMIT + lastPageCount; + + return fetchedCount < totalCount ? fetchedCount : undefined; +}; + +export { getDns, getNextPageParam }; diff --git a/idea/frontend/src/features/nodesSwitch/ui/node/Node.tsx b/idea/frontend/src/features/nodesSwitch/ui/node/Node.tsx index 7874a6c1fa..77876a7cbc 100644 --- a/idea/frontend/src/features/nodesSwitch/ui/node/Node.tsx +++ b/idea/frontend/src/features/nodesSwitch/ui/node/Node.tsx @@ -5,9 +5,9 @@ import clsx from 'clsx'; import { Node as NodeType } from '@/entities/node'; import { copyToClipboard } from '@/shared/helpers'; import CopyGreenSVG from '@/shared/assets/images/actions/copyGreen.svg?react'; +import TrashSVG from '@/shared/assets/images/actions/trash.svg?react'; import { ICON } from '@/widgets/menu/model/consts'; -import TrashSVG from '../../assets/trash.svg?react'; import styles from './Node.module.scss'; type Props = NodeType & { diff --git a/idea/frontend/src/features/voucher/hooks/index.ts b/idea/frontend/src/features/voucher/hooks/index.ts index 77e2f340af..86ec3e7db0 100644 --- a/idea/frontend/src/features/voucher/hooks/index.ts +++ b/idea/frontend/src/features/voucher/hooks/index.ts @@ -1,9 +1,6 @@ import { useDurationSchema } from './use-duration-schema'; -import { useProgramIdSchema } from './use-program-id-schema'; import { useVoucherType } from './use-voucher-type'; -import { useModal } from './use-modal'; -import { useLoading } from './use-loading'; import { useVouchers } from './use-vouchers'; import { useVoucherFilters } from './use-voucher-filters'; -export { useDurationSchema, useProgramIdSchema, useVoucherType, useModal, useLoading, useVouchers, useVoucherFilters }; +export { useDurationSchema, useVoucherType, useVouchers, useVoucherFilters }; diff --git a/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.tsx b/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.tsx index 600286f591..0f96daedf6 100644 --- a/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.tsx +++ b/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.tsx @@ -1,13 +1,10 @@ import { HexString } from '@gear-js/api'; import { useApi } from '@gear-js/react-hooks'; -import { Button, Modal } from '@gear-js/ui'; +import { Button } from '@gear-js/ui'; -import { useSignAndSend } from '@/hooks'; -import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; - -import RemoveSVG from '../../assets/remove.svg?react'; -import { useModal } from '../../hooks'; -import styles from './decline-voucher.module.scss'; +import { useModalState, useSignAndSend } from '@/hooks'; +import RemoveSVG from '@/shared/assets/images/actions/remove.svg?react'; +import { ConfirmModal } from '@/shared/ui/confirm-modal'; type Props = { id: HexString; @@ -18,7 +15,7 @@ const DeclineVoucher = ({ id, onSubmit }: Props) => { const { isApiReady, api } = useApi(); const signAndSend = useSignAndSend(); - const [isModalOpen, openModal, closeModal] = useModal(); + const [isModalOpen, openModal, closeModal] = useModalState(); const handleSubmitClick = () => { if (!isApiReady) throw new Error('API is not initialized'); @@ -35,17 +32,14 @@ const DeclineVoucher = ({ id, onSubmit }: Props) => { {isModalOpen && ( - - - This action cannot be undone. If you change your mind, voucher's owner will have to issue a new voucher - manually. - - - - - - - + )} > ); diff --git a/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx b/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx index 711fa92100..3da3c1efc1 100644 --- a/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx +++ b/idea/frontend/src/features/voucher/ui/issue-voucher-modal/issue-voucher-modal.tsx @@ -6,13 +6,13 @@ import { useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; -import { useBalanceSchema, useSignAndSend } from '@/hooks'; +import { useLoading, useBalanceSchema, useSignAndSend } from '@/hooks'; import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; import { Input, ValueField } from '@/shared/ui'; import { ADDRESS_SCHEMA, DEFAULT_VALUES, FIELD_NAME, VOUCHER_TYPE } from '../../consts'; -import { useDurationSchema, useVoucherType, useLoading } from '../../hooks'; +import { useDurationSchema, useVoucherType } from '../../hooks'; import { Values } from '../../types'; import { DurationForm } from '../duration-form'; import { ProgramsForm } from '../programs-form'; diff --git a/idea/frontend/src/features/voucher/ui/issue-voucher/issue-voucher.tsx b/idea/frontend/src/features/voucher/ui/issue-voucher/issue-voucher.tsx index 5bb45f7bd2..b8ab2e32f6 100644 --- a/idea/frontend/src/features/voucher/ui/issue-voucher/issue-voucher.tsx +++ b/idea/frontend/src/features/voucher/ui/issue-voucher/issue-voucher.tsx @@ -2,11 +2,11 @@ import { HexString } from '@gear-js/api'; import { Button } from '@gear-js/ui'; import cx from 'clsx'; +import { useModalState } from '@/hooks'; import { withAccount } from '@/shared/ui'; import CouponSVG from '../../assets/coupon.svg?react'; import { IssueVoucherModal } from '../issue-voucher-modal'; -import { useModal } from '../../hooks'; import styles from './issue-voucher.module.scss'; type Props = { @@ -17,7 +17,7 @@ type Props = { }; const IssueVoucher = withAccount(({ programId, buttonColor = 'light', buttonSize = 'medium', onSubmit }: Props) => { - const [isModalOpen, openModal, closeModal] = useModal(); + const [isModalOpen, openModal, closeModal] = useModalState(); return ( <> diff --git a/idea/frontend/src/features/voucher/ui/programs-form/programs-form.tsx b/idea/frontend/src/features/voucher/ui/programs-form/programs-form.tsx index 211ab9126b..0bf0503636 100644 --- a/idea/frontend/src/features/voucher/ui/programs-form/programs-form.tsx +++ b/idea/frontend/src/features/voucher/ui/programs-form/programs-form.tsx @@ -5,10 +5,10 @@ import clsx from 'clsx'; import { FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; +import { useProgramIdSchema } from '@/hooks'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; import { Input } from '@/shared/ui'; -import { useProgramIdSchema } from '../../hooks'; import styles from './programs-form.module.scss'; const FIELD_NAME = { diff --git a/idea/frontend/src/features/voucher/ui/revoke-voucher/revoke-voucher.tsx b/idea/frontend/src/features/voucher/ui/revoke-voucher/revoke-voucher.tsx index d76681a3cf..73c2e0cc32 100644 --- a/idea/frontend/src/features/voucher/ui/revoke-voucher/revoke-voucher.tsx +++ b/idea/frontend/src/features/voucher/ui/revoke-voucher/revoke-voucher.tsx @@ -2,11 +2,10 @@ import { HexString } from '@gear-js/api'; import { useApi } from '@gear-js/react-hooks'; import { Button, Modal } from '@gear-js/ui'; -import { useSignAndSend } from '@/hooks'; +import { useModalState, useSignAndSend } from '@/hooks'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; +import RemoveSVG from '@/shared/assets/images/actions/remove.svg?react'; -import RemoveSVG from '../../assets/remove.svg?react'; -import { useModal } from '../../hooks'; import styles from './revoke-voucher.module.scss'; type Props = { @@ -19,7 +18,7 @@ const RevokeVoucher = ({ spender, id, onSubmit }: Props) => { const { isApiReady, api } = useApi(); const signAndSend = useSignAndSend(); - const [isModalOpen, openModal, closeModal] = useModal(); + const [isModalOpen, openModal, closeModal] = useModalState(); const handleSubmitClick = () => { if (!isApiReady) throw new Error('API is not initialized'); diff --git a/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx b/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx index 9fe1b7fd0e..8fe3305c10 100644 --- a/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx +++ b/idea/frontend/src/features/voucher/ui/update-voucher-modal/update-voucher-modal.tsx @@ -6,14 +6,14 @@ import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; -import { useBalanceSchema, useSignAndSend } from '@/hooks'; +import { useLoading, useBalanceSchema, useSignAndSend } from '@/hooks'; import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; import { asOptionalField } from '@/shared/helpers'; import { Input, ValueField } from '@/shared/ui'; import { ADDRESS_SCHEMA, DEFAULT_VALUES, FIELD_NAME } from '../../consts'; -import { useDurationSchema, useLoading } from '../../hooks'; +import { useDurationSchema } from '../../hooks'; import { Values, Voucher } from '../../types'; import { DurationForm } from '../duration-form'; import { ProgramsForm } from '../programs-form'; diff --git a/idea/frontend/src/features/voucher/ui/update-voucher/update-voucher.tsx b/idea/frontend/src/features/voucher/ui/update-voucher/update-voucher.tsx index 57a0acd210..b324c5e1c9 100644 --- a/idea/frontend/src/features/voucher/ui/update-voucher/update-voucher.tsx +++ b/idea/frontend/src/features/voucher/ui/update-voucher/update-voucher.tsx @@ -1,8 +1,8 @@ import { Button } from '@gear-js/ui'; -import EditSVG from '../../assets/edit.svg?react'; +import EditSVG from '@/shared/assets/images/actions/edit.svg?react'; +import { useModalState } from '@/hooks'; import { UpdateVoucherModal } from '../update-voucher-modal'; -import { useModal } from '../../hooks'; import { Voucher } from '../../types'; import styles from './update-voucher.module.scss'; @@ -12,7 +12,7 @@ type Props = { }; const UpdateVoucher = ({ voucher, onSubmit }: Props) => { - const [isModalOpen, openModal, closeModal] = useModal(); + const [isModalOpen, openModal, closeModal] = useModalState(); return ( <> diff --git a/idea/frontend/src/features/voucher/utils.ts b/idea/frontend/src/features/voucher/utils.ts index e16931f90e..cff94f852d 100644 --- a/idea/frontend/src/features/voucher/utils.ts +++ b/idea/frontend/src/features/voucher/utils.ts @@ -1,4 +1,5 @@ import { DEFAULT_LIMIT } from '@/shared/config'; +import { fetchWithGuard } from '@/shared/helpers'; import { VouchersResponse } from './types'; @@ -32,14 +33,6 @@ function getTime(ms: number) { return result.trim(); } -const fetchWithGuard = async (...args: Parameters) => { - const response = await fetch(...args); - - if (!response.ok) throw new Error(response.statusText); - - return response.json() as T; -}; - const getVouchers = (url: string, params: { offset: number; limit: number; id: string }) => { const method = 'POST'; const body = JSON.stringify(params); diff --git a/idea/frontend/src/hooks/index.ts b/idea/frontend/src/hooks/index.ts index c4598db5c8..6d9351e1d6 100644 --- a/idea/frontend/src/hooks/index.ts +++ b/idea/frontend/src/hooks/index.ts @@ -1,10 +1,12 @@ import { useModal, useBlocks, useEvents, useChain, useOnboarding } from './context'; +import { useLoading } from './useLoading'; import { useMessage } from './useMessage'; import { useOutsideClick } from './useOutsideClick'; import { useChangeEffect } from './useChangeEffect'; import { useProgram } from './useProgram'; import { useMessageActions } from './useMessageActions'; import { usePrograms } from './usePrograms'; +import { useProgramIdSchema } from './useProgramIdSchema'; import { useCodeUpload } from './useCodeUpload'; import { useMessageClaim } from './useMessageClaim'; import { useProgramActions } from './useProgramActions'; @@ -25,14 +27,17 @@ import { useNetworkIcon } from './useNetworkIcon'; import { useValidationSchema, useBalanceSchema, useGasLimitSchema } from './schemas'; import { useSignAndSend } from './use-sign-and-send'; import { useContractApiWithFile } from './use-contract-api-with-file'; +import { useModalState } from './use-modal-state'; export { useModal, + useLoading, useBlocks, useEvents, useChain, useProgram, usePrograms, + useProgramIdSchema, useMessage, useMessages, useWaitlist, @@ -60,4 +65,5 @@ export { useContractApiWithFile, useBalanceSchema, useGasLimitSchema, + useModalState, }; diff --git a/idea/frontend/src/features/voucher/hooks/use-modal.ts b/idea/frontend/src/hooks/use-modal-state.ts similarity index 79% rename from idea/frontend/src/features/voucher/hooks/use-modal.ts rename to idea/frontend/src/hooks/use-modal-state.ts index 82e4b40cfb..6e90a073ed 100644 --- a/idea/frontend/src/features/voucher/hooks/use-modal.ts +++ b/idea/frontend/src/hooks/use-modal-state.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -function useModal() { +function useModalState() { const [isOpen, setIsOpen] = useState(false); const open = () => setIsOpen(true); @@ -9,4 +9,4 @@ function useModal() { return [isOpen, open, close] as const; } -export { useModal }; +export { useModalState }; diff --git a/idea/frontend/src/features/voucher/hooks/use-loading.ts b/idea/frontend/src/hooks/useLoading.ts similarity index 100% rename from idea/frontend/src/features/voucher/hooks/use-loading.ts rename to idea/frontend/src/hooks/useLoading.ts diff --git a/idea/frontend/src/features/voucher/hooks/use-program-id-schema.ts b/idea/frontend/src/hooks/useProgramIdSchema.ts similarity index 100% rename from idea/frontend/src/features/voucher/hooks/use-program-id-schema.ts rename to idea/frontend/src/hooks/useProgramIdSchema.ts diff --git a/idea/frontend/src/pages/dns/dns.module.scss b/idea/frontend/src/pages/dns/dns.module.scss new file mode 100644 index 0000000000..73fadcb724 --- /dev/null +++ b/idea/frontend/src/pages/dns/dns.module.scss @@ -0,0 +1,30 @@ +@use '@gear-js/ui/headings' as *; +@use '@/shared/assets/styles/variables' as *; + +.dns { + display: grid; + grid-template-columns: 7.75fr 2.25fr; + gap: 16px 24px; + align-items: flex-start; +} + +.header { + display: flex; + justify-content: space-between; + & > div:first-child { + width: 100%; + } +} + +.heading { + @include heading(24px, 31px, 500); + color: $gray800; +} + +.placeholder { + display: flex; + flex-direction: column; + gap: 12px; + + position: relative; +} diff --git a/idea/frontend/src/pages/dns/dns.tsx b/idea/frontend/src/pages/dns/dns.tsx new file mode 100644 index 0000000000..901b708f1c --- /dev/null +++ b/idea/frontend/src/pages/dns/dns.tsx @@ -0,0 +1,64 @@ +import { useAccount } from '@gear-js/react-hooks'; +import { useState } from 'react'; + +import { Placeholder } from '@/entities/placeholder'; +import { FilterGroup, Filters, Radio } from '@/features/filters'; +import { SortBy } from '@/features/sortBy'; +import { + Dns as DnsType, + DnsCard, + DnsCardPlaceholder, + useDnsFilters, + useDns, + CreateDns, + useDnsSort, + useInitDnsProgram, +} from '@/features/dns'; +import { List, SearchForm, Skeleton } from '@/shared/ui'; + +import styles from './dns.module.scss'; + +const Dns = () => { + const { account } = useAccount(); + useInitDnsProgram(); + + const [searchQuery, setSearchQuery] = useState(''); + const [filterValues, filterParams, handleFiltersSubmit] = useDnsFilters(); + const [sortValues, toggleDirection] = useDnsSort(); + + const [dns, count, isLoading, hasMore, fetchMore, refetch] = useDns(searchQuery, filterParams, sortValues); + const isEmpty = !(isLoading || count); + const isLoaderVisible = isEmpty || (!count && isLoading); + + const renderDns = (dnsItem: DnsType) => ; + const renderSkeleton = () => ; + + return ( + + + toggleDirection()} /> + + + + + setSearchQuery(query)} /> + + {isLoaderVisible ? ( + + + + ) : ( + + )} + + + + + {account && } + + + + ); +}; + +export { Dns }; diff --git a/idea/frontend/src/pages/dns/index.ts b/idea/frontend/src/pages/dns/index.ts new file mode 100644 index 0000000000..751e42bdef --- /dev/null +++ b/idea/frontend/src/pages/dns/index.ts @@ -0,0 +1,3 @@ +import { Dns } from './dns'; + +export { Dns }; diff --git a/idea/frontend/src/pages/index.tsx b/idea/frontend/src/pages/index.tsx index 5080d65079..466b2f5399 100644 --- a/idea/frontend/src/pages/index.tsx +++ b/idea/frontend/src/pages/index.tsx @@ -19,6 +19,7 @@ import { Explorer } from './explorer'; import { Code } from './code'; import { UploadCode } from './uploadCode'; import { Vouchers } from './vouchers'; +import { Dns } from './dns'; const Routing = () => { const events = useEvents(); @@ -64,6 +65,7 @@ const Routing = () => { } /> } /> + } /> } /> } /> diff --git a/idea/frontend/src/features/voucher/assets/edit.svg b/idea/frontend/src/shared/assets/images/actions/edit.svg similarity index 100% rename from idea/frontend/src/features/voucher/assets/edit.svg rename to idea/frontend/src/shared/assets/images/actions/edit.svg diff --git a/idea/frontend/src/features/voucher/assets/remove.svg b/idea/frontend/src/shared/assets/images/actions/remove.svg similarity index 100% rename from idea/frontend/src/features/voucher/assets/remove.svg rename to idea/frontend/src/shared/assets/images/actions/remove.svg diff --git a/idea/frontend/src/features/nodesSwitch/assets/trash.svg b/idea/frontend/src/shared/assets/images/actions/trash.svg similarity index 100% rename from idea/frontend/src/features/nodesSwitch/assets/trash.svg rename to idea/frontend/src/shared/assets/images/actions/trash.svg diff --git a/idea/frontend/src/shared/config/consts.ts b/idea/frontend/src/shared/config/consts.ts index 2f9f457d27..b8742fa7a9 100644 --- a/idea/frontend/src/shared/config/consts.ts +++ b/idea/frontend/src/shared/config/consts.ts @@ -1,3 +1,5 @@ +import { HexString } from '@gear-js/api'; + const API_URL = import.meta.env.VITE_API_URL as string; const NODES_API_URL = import.meta.env.VITE_NODES_API_URL as string; const NODE_ADDRESS = import.meta.env.VITE_NODE_ADDRESS as string; diff --git a/idea/frontend/src/shared/config/routes.ts b/idea/frontend/src/shared/config/routes.ts index eeaafdacea..fcb324653d 100644 --- a/idea/frontend/src/shared/config/routes.ts +++ b/idea/frontend/src/shared/config/routes.ts @@ -18,6 +18,7 @@ const routes = { reply: 'reply/:messageId', block: ':blockId', vouchers: '/vouchers', + dns: '/dns', }; const absoluteRoutes = { diff --git a/idea/frontend/src/shared/helpers/index.ts b/idea/frontend/src/shared/helpers/index.ts index 0814026fbe..c738aaa5a7 100644 --- a/idea/frontend/src/shared/helpers/index.ts +++ b/idea/frontend/src/shared/helpers/index.ts @@ -125,6 +125,14 @@ const isHex = (value: unknown): value is HexString => { return isString(value) && (value === '0x' || (HEX_REGEX.test(value) && value.length % 2 === 0)); }; +const fetchWithGuard = async (...args: Parameters) => { + const response = await fetch(...args); + + if (!response.ok) throw new Error(response.statusText); + + return response.json() as T; +}; + export { checkWallet, formatDate, @@ -150,4 +158,5 @@ export { isString, isUndefined, isHex, + fetchWithGuard, }; diff --git a/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.module.scss b/idea/frontend/src/shared/ui/confirm-modal/confirm-modal.module.scss similarity index 50% rename from idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.module.scss rename to idea/frontend/src/shared/ui/confirm-modal/confirm-modal.module.scss index c70f9769d7..7672a32f59 100644 --- a/idea/frontend/src/features/voucher/ui/decline-voucher/decline-voucher.module.scss +++ b/idea/frontend/src/shared/ui/confirm-modal/confirm-modal.module.scss @@ -1,15 +1,11 @@ +@use '@/shared/assets/styles/mixins' as *; + .text { - color: rgba(255, 255, 255, 0.7); + margin-bottom: 32px; } .buttons { - margin-top: 24px; - display: grid; grid-template-columns: 1fr 1fr; gap: 32px; - - svg path { - fill: #fff; - } } diff --git a/idea/frontend/src/shared/ui/confirm-modal/confirm-modal.tsx b/idea/frontend/src/shared/ui/confirm-modal/confirm-modal.tsx new file mode 100644 index 0000000000..a868a15c7e --- /dev/null +++ b/idea/frontend/src/shared/ui/confirm-modal/confirm-modal.tsx @@ -0,0 +1,39 @@ +import { FC, SVGProps } from 'react'; +import { Button, Modal } from '@gear-js/ui'; + +import ApplySVG from '@/shared/assets/images/actions/apply.svg?react'; +import CloseSVG from '@/shared/assets/images/actions/close.svg?react'; + +import styles from './confirm-modal.module.scss'; + +type Props = { + title: string; + text: string; + confirmText: string; + cancelText?: string; + close: () => void; + onSubmit: () => void; + confirmIcon?: FC>; + isLoading?: boolean; +}; + +const ConfirmModal = ({ title, text, close, onSubmit, confirmText, confirmIcon, isLoading }: Props) => { + return ( + + {text} + + + + + + + ); +}; + +export { ConfirmModal }; diff --git a/idea/frontend/src/shared/ui/confirm-modal/index.tsx b/idea/frontend/src/shared/ui/confirm-modal/index.tsx new file mode 100644 index 0000000000..89cfd53fb9 --- /dev/null +++ b/idea/frontend/src/shared/ui/confirm-modal/index.tsx @@ -0,0 +1,3 @@ +import { ConfirmModal } from './confirm-modal'; + +export { ConfirmModal }; diff --git a/idea/frontend/src/widgets/menu/ui/Menu.module.scss b/idea/frontend/src/widgets/menu/ui/Menu.module.scss index eb61b812be..c19186ee6d 100644 --- a/idea/frontend/src/widgets/menu/ui/Menu.module.scss +++ b/idea/frontend/src/widgets/menu/ui/Menu.module.scss @@ -157,7 +157,6 @@ font-weight: 600; line-height: toRem(21); letter-spacing: 0.08em; - text-transform: capitalize; } } diff --git a/idea/frontend/src/widgets/menu/ui/navigation/Navigation.tsx b/idea/frontend/src/widgets/menu/ui/navigation/Navigation.tsx index 8f5389fa86..508105ba53 100644 --- a/idea/frontend/src/widgets/menu/ui/navigation/Navigation.tsx +++ b/idea/frontend/src/widgets/menu/ui/navigation/Navigation.tsx @@ -38,6 +38,8 @@ const Navigation = ({ isOpen }: Props) => { } text="Vouchers" isFullWidth={isOpen} /> + } text="dDNS" isFullWidth={isOpen} /> +
- This action cannot be undone. If you change your mind, voucher's owner will have to issue a new voucher - manually. -
{text}