diff --git a/dns/frontend/.eslintrc.cjs b/dns/frontend/.eslintrc.cjs
new file mode 100644
index 00000000..d6c95379
--- /dev/null
+++ b/dns/frontend/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/dns/frontend/.gitignore b/dns/frontend/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/dns/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/dns/frontend/README.md b/dns/frontend/README.md
new file mode 100644
index 00000000..6c867152
--- /dev/null
+++ b/dns/frontend/README.md
@@ -0,0 +1,20 @@
+# ink! Frontend Example
+
+This is a vanilla [vite + typescript](https://vitejs.dev/) project to showcase the use of [`useinkathon`](https://github.com/scio-labs/use-inkathon).
+
+## Getting Started
+
+You can use the package manager of your choice to install the dependencies and start the project in development mode. We like `pnpm` right now. But this example should work with `npm` & `yarn` as well.
+
+```sh
+pnpm install
+pnpm dev
+```
+
+## Change the Code
+
+The actual interaction with the contract is all contained in the `./src/App.tsx` file. Every other file in the folder is only relevant for styling and bundling.
+
+## Demo
+
+
\ No newline at end of file
diff --git a/dns/frontend/demo.gif b/dns/frontend/demo.gif
new file mode 100644
index 00000000..6a264718
Binary files /dev/null and b/dns/frontend/demo.gif differ
diff --git a/dns/frontend/index.html b/dns/frontend/index.html
new file mode 100644
index 00000000..7094f9d5
--- /dev/null
+++ b/dns/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ink! Frontend Example
+
+
+
+
+
+
diff --git a/dns/frontend/package.json b/dns/frontend/package.json
new file mode 100644
index 00000000..0b645829
--- /dev/null
+++ b/dns/frontend/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "dns-frontend-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@polkadot/util-crypto": "^12.6.2",
+ "@scio-labs/use-inkathon": "^0.6.3",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.48",
+ "@types/react-dom": "^18.2.18",
+ "@typescript-eslint/eslint-plugin": "^6.19.1",
+ "@typescript-eslint/parser": "^6.19.1",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "autoprefixer": "^10.4.17",
+ "eslint": "^8.56.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "postcss": "^8.4.33",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.3.3",
+ "vite": "^5.0.12"
+ }
+}
diff --git a/dns/frontend/postcss.config.js b/dns/frontend/postcss.config.js
new file mode 100644
index 00000000..2e7af2b7
--- /dev/null
+++ b/dns/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/dns/frontend/src/App.tsx b/dns/frontend/src/App.tsx
new file mode 100644
index 00000000..bd0e05c0
--- /dev/null
+++ b/dns/frontend/src/App.tsx
@@ -0,0 +1,370 @@
+import { blake2AsHex } from "@polkadot/util-crypto";
+import {
+ SubstrateDeployment,
+ UseInkathonProvider,
+ contractQuery,
+ contractTx,
+ decodeOutput,
+ rococo,
+ useBalance,
+ useInkathon,
+ useRegisteredContract,
+} from "@scio-labs/use-inkathon";
+import { useRef, useState } from "react";
+
+import CONTRACT_METADATA from "./dns.json";
+const CONTRACT_NAME = "dns";
+
+const getDeployments = async (): Promise => {
+ return [
+ {
+ contractId: CONTRACT_NAME,
+ networkId: rococo.network,
+ abi: CONTRACT_METADATA,
+ address: "5GWCUiApMhV3QYK4RedaLpbhcCBWLeGVT2wtZPfCHhnHxoud",
+ },
+ ];
+};
+
+export default function WrappedApp() {
+ return (
+
+
+
+ );
+}
+
+function App() {
+ const { isConnected } = useInkathon();
+ return (
+
+
+
+ {isConnected && (
+ <>
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+const ConnectionState = () => {
+ const {
+ connect,
+ disconnect,
+ isConnected,
+ activeChain,
+ activeAccount,
+ setActiveAccount,
+ accounts,
+ } = useInkathon();
+ const { contract } = useRegisteredContract(CONTRACT_NAME);
+ const balance = useBalance(activeAccount?.address, true);
+
+ if (!isConnected) {
+ return (
+
+ (connect ? connect() : undefined)}>
+ Connect
+
+
+ );
+ }
+
+ return (
+
+
+ {activeChain && (
+
+
Chain
+
{activeChain.name}
+
+ )}
+
+ {activeAccount && accounts && (
+
+
Active Account
+
+
{
+ const selectedAccount = accounts.find(
+ (account) => account.address === v.target.value
+ );
+
+ if (selectedAccount && setActiveAccount)
+ setActiveAccount(selectedAccount);
+ }}
+ >
+ {accounts.map((account) => (
+
+ {account.name ? account.name : account.address}
+
+ ))}
+
+
+ {activeAccount.address}
+
+
+ )}
+
+ {balance && (
+
+
Account Balance
+
+ {balance.balanceFormatted}
+
+
+
+ )}
+
+ {contract && (
+
+
Contract
+
+ {contract?.address.toHex()}
+
+
+ )}
+
+
+ Disconnect
+
+
+ );
+};
+
+const RegisterName = () => {
+ const { api, activeAccount } = useInkathon();
+
+ const [name, setName] = useState("");
+ const nameRef = useRef(null);
+
+ const { contract } = useRegisteredContract("dns");
+
+ const {
+ loading,
+ error,
+ result,
+ execute: register,
+ } = usePromise(async () => {
+ if (!contract || !api || !activeAccount || !nameRef.current) return;
+ setName(nameRef.current.value);
+ return contractTx(api, activeAccount.address, contract, "register", {}, [
+ blake2AsHex(nameRef.current.value),
+ ]);
+ });
+
+ return (
+
+
+
Step 1: Register a New Name
+
+ Claim ownership of the given name
+
+
+
+
+
+ {error && (
+ <>
+
+
{error}
+ >
+ )}
+
+ {result && !!result.successEvent && (
+ <>
+
+
+ Name {name}
registered!
+
+ >
+ )}
+
+ );
+};
+
+const SetAddress = () => {
+ const { api, activeAccount } = useInkathon();
+ const [name, setName] = useState("");
+ const [accountId, setAccountId] = useState("");
+
+ const { contract } = useRegisteredContract("dns");
+
+ const { execute, loading, result, error } = usePromise(async () => {
+ if (!contract || !api || !activeAccount || !name) return;
+
+ return contractTx(api, activeAccount.address, contract, "setAddress", {}, [
+ blake2AsHex(name),
+ accountId,
+ ]);
+ });
+
+ return (
+
+
+
+ Step 2: Set AccountId For Name
+
+
+ Set the AccountId
which the given name
{" "}
+ should resolve to
+
+
+
+
+
+ {error && (
+ <>
+
+
{error}
+ >
+ )}
+ {result && !!result.successEvent && (
+ <>
+
+
+
+ Name {name}
now resolves to:
+
+
+ {accountId}
+
+
+ >
+ )}
+
+ );
+};
+
+const GetAddress = () => {
+ const { api } = useInkathon();
+ const inputRef = useRef(null);
+ const [name, setName] = useState("");
+ const [result, setResult] = useState>(
+ null
+ );
+ const { contract } = useRegisteredContract("dns");
+
+ const getAddress = async (name: string) => {
+ if (!contract || !api || !name) return;
+
+ const result = await contractQuery(
+ api,
+ "",
+ contract,
+ "getAddress",
+ undefined,
+ [blake2AsHex(name)]
+ );
+
+ setResult(decodeOutput(result, contract, "getAddress"));
+ setName(name);
+ };
+
+ return (
+
+
+
+ Step 3: Lookup Name
+
+
+ Resolves Address
for a given Name
+
+
+
+
+ {result && (
+ <>
+
+
+ {name}
resolves to:
+
+
{result.decodedOutput}
+ >
+ )}
+
+ );
+};
+
+const usePromise = (promise: () => Promise) => {
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const execute = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const result = await promise();
+ setResult(result);
+ } catch (error) {
+ if (
+ typeof error === "object" &&
+ error &&
+ "errorMessage" in error &&
+ typeof error.errorMessage === "string"
+ )
+ setError(error.errorMessage);
+ else {
+ console.error(error);
+ setError("Unknown error, check console");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { result, error, loading, execute };
+};
diff --git a/dns/frontend/src/dns.json b/dns/frontend/src/dns.json
new file mode 100644
index 00000000..e41a57d6
--- /dev/null
+++ b/dns/frontend/src/dns.json
@@ -0,0 +1,787 @@
+{
+ "source": {
+ "hash": "0x5450904551b4922e90662a0d278ca50988780bffc17ac871fceda420d85f8d79",
+ "language": "ink! 4.3.0",
+ "compiler": "rustc 1.72.1",
+ "build_info": {
+ "build_mode": "Debug",
+ "cargo_contract_version": "3.2.0",
+ "rust_toolchain": "stable-aarch64-apple-darwin",
+ "wasm_opt_settings": {
+ "keep_debug_symbols": false,
+ "optimization_passes": "Z"
+ }
+ }
+ },
+ "contract": {
+ "name": "dns",
+ "version": "4.3.0",
+ "authors": [
+ "Parity Technologies "
+ ]
+ },
+ "spec": {
+ "constructors": [
+ {
+ "args": [],
+ "default": false,
+ "docs": [
+ "Creates a new domain name service contract."
+ ],
+ "label": "new",
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink_primitives",
+ "ConstructorResult"
+ ],
+ "type": 3
+ },
+ "selector": "0x9bae9d5e"
+ }
+ ],
+ "docs": [],
+ "environment": {
+ "accountId": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ },
+ "balance": {
+ "displayName": [
+ "Balance"
+ ],
+ "type": 12
+ },
+ "blockNumber": {
+ "displayName": [
+ "BlockNumber"
+ ],
+ "type": 14
+ },
+ "chainExtension": {
+ "displayName": [
+ "ChainExtension"
+ ],
+ "type": 15
+ },
+ "hash": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ },
+ "maxEventTopics": 4,
+ "timestamp": {
+ "displayName": [
+ "Timestamp"
+ ],
+ "type": 13
+ }
+ },
+ "events": [
+ {
+ "args": [
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ },
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "from",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ }
+ ],
+ "docs": [
+ "Emitted whenever a new name is being registered."
+ ],
+ "label": "Register"
+ },
+ {
+ "args": [
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ },
+ {
+ "docs": [],
+ "indexed": false,
+ "label": "from",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ },
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "old_address",
+ "type": {
+ "displayName": [
+ "Option"
+ ],
+ "type": 11
+ }
+ },
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "new_address",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ }
+ ],
+ "docs": [
+ "Emitted whenever an address changes."
+ ],
+ "label": "SetAddress"
+ },
+ {
+ "args": [
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ },
+ {
+ "docs": [],
+ "indexed": false,
+ "label": "from",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ },
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "old_owner",
+ "type": {
+ "displayName": [
+ "Option"
+ ],
+ "type": 11
+ }
+ },
+ {
+ "docs": [],
+ "indexed": true,
+ "label": "new_owner",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ }
+ ],
+ "docs": [
+ "Emitted whenever a name is being transferred."
+ ],
+ "label": "Transfer"
+ }
+ ],
+ "lang_error": {
+ "displayName": [
+ "ink",
+ "LangError"
+ ],
+ "type": 5
+ },
+ "messages": [
+ {
+ "args": [
+ {
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ }
+ ],
+ "default": false,
+ "docs": [
+ " Register specific name with caller as owner."
+ ],
+ "label": "register",
+ "mutates": true,
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink",
+ "MessageResult"
+ ],
+ "type": 7
+ },
+ "selector": "0x229b553f"
+ },
+ {
+ "args": [
+ {
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ },
+ {
+ "label": "new_address",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ }
+ ],
+ "default": false,
+ "docs": [
+ " Set address for specific name."
+ ],
+ "label": "set_address",
+ "mutates": true,
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink",
+ "MessageResult"
+ ],
+ "type": 7
+ },
+ "selector": "0xb8a4d3d9"
+ },
+ {
+ "args": [
+ {
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ },
+ {
+ "label": "to",
+ "type": {
+ "displayName": [
+ "AccountId"
+ ],
+ "type": 0
+ }
+ }
+ ],
+ "default": false,
+ "docs": [
+ " Transfer owner to another address."
+ ],
+ "label": "transfer",
+ "mutates": true,
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink",
+ "MessageResult"
+ ],
+ "type": 7
+ },
+ "selector": "0x84a15da1"
+ },
+ {
+ "args": [
+ {
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ }
+ ],
+ "default": false,
+ "docs": [
+ " Get address for specific name."
+ ],
+ "label": "get_address",
+ "mutates": false,
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink",
+ "MessageResult"
+ ],
+ "type": 10
+ },
+ "selector": "0xd259f7ba"
+ },
+ {
+ "args": [
+ {
+ "label": "name",
+ "type": {
+ "displayName": [
+ "Hash"
+ ],
+ "type": 6
+ }
+ }
+ ],
+ "default": false,
+ "docs": [
+ " Get owner of specific name."
+ ],
+ "label": "get_owner",
+ "mutates": false,
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "ink",
+ "MessageResult"
+ ],
+ "type": 10
+ },
+ "selector": "0x07fcd0b1"
+ }
+ ]
+ },
+ "storage": {
+ "root": {
+ "layout": {
+ "struct": {
+ "fields": [
+ {
+ "layout": {
+ "root": {
+ "layout": {
+ "leaf": {
+ "key": "0x9891106e",
+ "ty": 0
+ }
+ },
+ "root_key": "0x9891106e"
+ }
+ },
+ "name": "name_to_address"
+ },
+ {
+ "layout": {
+ "root": {
+ "layout": {
+ "leaf": {
+ "key": "0xd3cd158e",
+ "ty": 0
+ }
+ },
+ "root_key": "0xd3cd158e"
+ }
+ },
+ "name": "name_to_owner"
+ },
+ {
+ "layout": {
+ "leaf": {
+ "key": "0x00000000",
+ "ty": 0
+ }
+ },
+ "name": "default_address"
+ }
+ ],
+ "name": "DomainNameService"
+ }
+ },
+ "root_key": "0x00000000"
+ }
+ },
+ "types": [
+ {
+ "id": 0,
+ "type": {
+ "def": {
+ "composite": {
+ "fields": [
+ {
+ "type": 1,
+ "typeName": "[u8; 32]"
+ }
+ ]
+ }
+ },
+ "path": [
+ "ink_primitives",
+ "types",
+ "AccountId"
+ ]
+ }
+ },
+ {
+ "id": 1,
+ "type": {
+ "def": {
+ "array": {
+ "len": 32,
+ "type": 2
+ }
+ }
+ }
+ },
+ {
+ "id": 2,
+ "type": {
+ "def": {
+ "primitive": "u8"
+ }
+ }
+ },
+ {
+ "id": 3,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "fields": [
+ {
+ "type": 4
+ }
+ ],
+ "index": 0,
+ "name": "Ok"
+ },
+ {
+ "fields": [
+ {
+ "type": 5
+ }
+ ],
+ "index": 1,
+ "name": "Err"
+ }
+ ]
+ }
+ },
+ "params": [
+ {
+ "name": "T",
+ "type": 4
+ },
+ {
+ "name": "E",
+ "type": 5
+ }
+ ],
+ "path": [
+ "Result"
+ ]
+ }
+ },
+ {
+ "id": 4,
+ "type": {
+ "def": {
+ "tuple": []
+ }
+ }
+ },
+ {
+ "id": 5,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "index": 1,
+ "name": "CouldNotReadInput"
+ }
+ ]
+ }
+ },
+ "path": [
+ "ink_primitives",
+ "LangError"
+ ]
+ }
+ },
+ {
+ "id": 6,
+ "type": {
+ "def": {
+ "composite": {
+ "fields": [
+ {
+ "type": 1,
+ "typeName": "[u8; 32]"
+ }
+ ]
+ }
+ },
+ "path": [
+ "ink_primitives",
+ "types",
+ "Hash"
+ ]
+ }
+ },
+ {
+ "id": 7,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "fields": [
+ {
+ "type": 8
+ }
+ ],
+ "index": 0,
+ "name": "Ok"
+ },
+ {
+ "fields": [
+ {
+ "type": 5
+ }
+ ],
+ "index": 1,
+ "name": "Err"
+ }
+ ]
+ }
+ },
+ "params": [
+ {
+ "name": "T",
+ "type": 8
+ },
+ {
+ "name": "E",
+ "type": 5
+ }
+ ],
+ "path": [
+ "Result"
+ ]
+ }
+ },
+ {
+ "id": 8,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "fields": [
+ {
+ "type": 4
+ }
+ ],
+ "index": 0,
+ "name": "Ok"
+ },
+ {
+ "fields": [
+ {
+ "type": 9
+ }
+ ],
+ "index": 1,
+ "name": "Err"
+ }
+ ]
+ }
+ },
+ "params": [
+ {
+ "name": "T",
+ "type": 4
+ },
+ {
+ "name": "E",
+ "type": 9
+ }
+ ],
+ "path": [
+ "Result"
+ ]
+ }
+ },
+ {
+ "id": 9,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "index": 0,
+ "name": "NameAlreadyExists"
+ },
+ {
+ "index": 1,
+ "name": "CallerIsNotOwner"
+ }
+ ]
+ }
+ },
+ "path": [
+ "dns",
+ "dns",
+ "Error"
+ ]
+ }
+ },
+ {
+ "id": 10,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "fields": [
+ {
+ "type": 0
+ }
+ ],
+ "index": 0,
+ "name": "Ok"
+ },
+ {
+ "fields": [
+ {
+ "type": 5
+ }
+ ],
+ "index": 1,
+ "name": "Err"
+ }
+ ]
+ }
+ },
+ "params": [
+ {
+ "name": "T",
+ "type": 0
+ },
+ {
+ "name": "E",
+ "type": 5
+ }
+ ],
+ "path": [
+ "Result"
+ ]
+ }
+ },
+ {
+ "id": 11,
+ "type": {
+ "def": {
+ "variant": {
+ "variants": [
+ {
+ "index": 0,
+ "name": "None"
+ },
+ {
+ "fields": [
+ {
+ "type": 0
+ }
+ ],
+ "index": 1,
+ "name": "Some"
+ }
+ ]
+ }
+ },
+ "params": [
+ {
+ "name": "T",
+ "type": 0
+ }
+ ],
+ "path": [
+ "Option"
+ ]
+ }
+ },
+ {
+ "id": 12,
+ "type": {
+ "def": {
+ "primitive": "u128"
+ }
+ }
+ },
+ {
+ "id": 13,
+ "type": {
+ "def": {
+ "primitive": "u64"
+ }
+ }
+ },
+ {
+ "id": 14,
+ "type": {
+ "def": {
+ "primitive": "u32"
+ }
+ }
+ },
+ {
+ "id": 15,
+ "type": {
+ "def": {
+ "variant": {}
+ },
+ "path": [
+ "ink_env",
+ "types",
+ "NoChainExtension"
+ ]
+ }
+ }
+ ],
+ "version": "4"
+}
\ No newline at end of file
diff --git a/dns/frontend/src/index.css b/dns/frontend/src/index.css
new file mode 100644
index 00000000..04e892ed
--- /dev/null
+++ b/dns/frontend/src/index.css
@@ -0,0 +1,42 @@
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+
+ body {
+ @apply font-sans text-base antialiased leading-6;
+ }
+
+ button, [type='submit'], [type='button'] {
+ @apply disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-50 bg-purple-500 hover:bg-purple-400 text-white font-bold py-2 px-4 border-b-4 border-purple-700 hover:border-purple-500 disabled:border-gray-500 rounded active:border-none active:mt-1;
+ }
+
+ a{
+ @apply text-purple-500 hover:text-purple-700 active:text-purple-700;
+ }
+
+ select {
+ @apply bg-purple-500 px-4 my-1 py-2 rounded text-white font-bold border-b-4 border-purple-700;
+ }
+
+ input {
+ @apply bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500;
+ }
+
+ #root{
+ }
+
+ .card {
+ @apply flex flex-col gap-4 py-4 px-4 w-full rounded-lg overflow-hidden border-2 border-slate-300 ;
+ }
+
+ .error {
+ @apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative;
+ }
+
+ .success {
+ @apply bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative;
+ }
+}
diff --git a/dns/frontend/src/main.tsx b/dns/frontend/src/main.tsx
new file mode 100644
index 00000000..7432d465
--- /dev/null
+++ b/dns/frontend/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
+
+// biome-ignore lint/style/noNonNullAssertion:
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/dns/frontend/src/vite-env.d.ts b/dns/frontend/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/dns/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dns/frontend/tailwind.config.js b/dns/frontend/tailwind.config.js
new file mode 100644
index 00000000..614c86b4
--- /dev/null
+++ b/dns/frontend/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/dns/frontend/tsconfig.json b/dns/frontend/tsconfig.json
new file mode 100644
index 00000000..a7fc6fbf
--- /dev/null
+++ b/dns/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/dns/frontend/tsconfig.node.json b/dns/frontend/tsconfig.node.json
new file mode 100644
index 00000000..42872c59
--- /dev/null
+++ b/dns/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/dns/frontend/vite.config.ts b/dns/frontend/vite.config.ts
new file mode 100644
index 00000000..861b04b3
--- /dev/null
+++ b/dns/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})