Compare commits

..

No commits in common. 'main' and 'master' have entirely different histories.
main ... master

  1. 1
      .babelrc
  2. 1
      .idea/frontend.iml
  3. 2
      .idea/modules.xml
  4. 325
      .idea/workspace.xml
  5. 56
      README.md
  6. 1
      env.d.ts
  7. 30
      index.html
  8. 36096
      package-lock.json
  9. 68
      package.json
  10. BIN
      public/favicon.ico
  11. BIN
      public/favicon.png
  12. 33
      public/index.html
  13. BIN
      public/logo192.png
  14. BIN
      public/logo512.png
  15. BIN
      public/logo_mono.png
  16. 25
      public/manifest.json
  17. 3
      public/robots.txt
  18. 5
      public/tonconnect-manifest.json
  19. 44
      src/App.css
  20. 9
      src/App.test.tsx
  21. 15
      src/App.tsx
  22. 16
      src/App.vue
  23. 71
      src/api.ts
  24. 74
      src/assets/base.css
  25. 81
      src/assets/headers/getting_ton_domain.svg
  26. 81
      src/assets/headers/i-know.svg
  27. 55
      src/assets/icons/buy.svg
  28. 3
      src/assets/icons/globe.svg
  29. 58
      src/assets/icons/instant_buy.svg
  30. 3
      src/assets/icons/link.svg
  31. 24
      src/assets/icons/logout.svg
  32. 45
      src/assets/icons/ton_bottom.svg
  33. 49
      src/assets/icons/ton_left.svg
  34. 45
      src/assets/icons/ton_right.svg
  35. 45
      src/assets/icons/ton_top.svg
  36. BIN
      src/assets/images/anton_ton.png
  37. BIN
      src/assets/images/my_name_is.png
  38. BIN
      src/assets/images/personal_page.png
  39. BIN
      src/assets/images/send_by_name.png
  40. BIN
      src/assets/images/tg_nft.png
  41. BIN
      src/assets/images/tondns_nft.png
  42. BIN
      src/assets/images/tonweb_duck.png
  43. BIN
      src/assets/images/usage.png
  44. BIN
      src/assets/logo.png
  45. 1
      src/assets/logo.svg
  46. BIN
      src/assets/logo_landing.png
  47. BIN
      src/assets/logo_single.png
  48. 196
      src/assets/main.css
  49. 84
      src/assets/tondns.svg
  50. 115
      src/assets/tonweb.svg
  51. 11
      src/collection.ts
  52. 9
      src/components/Button/button.module.css
  53. 18
      src/components/Button/index.tsx
  54. 48
      src/components/DarkLayout.vue
  55. 98
      src/components/DnD.vue
  56. 148
      src/components/DomainBar.vue
  57. 116
      src/components/DomainResult.vue
  58. 198
      src/components/DomainTable.vue
  59. 91
      src/components/GetDomainBtn.vue
  60. 177
      src/components/Header.vue
  61. 28
      src/components/Header/header.module.css
  62. 22
      src/components/Header/index.tsx
  63. 179
      src/components/LoginModal.vue
  64. 28
      src/components/Preview/index.tsx
  65. 48
      src/components/Preview/preview.module.css
  66. 88
      src/components/RotateSquare2.vue
  67. 400
      src/components/SiteSettings.vue
  68. 539
      src/components/Socket.vue
  69. 56
      src/components/Switcher.vue
  70. 294
      src/components/TemplatesList.vue
  71. 37
      src/components/TonButton.vue
  72. 78
      src/components/Tooltip.vue
  73. 68
      src/components/WhiteLayout.vue
  74. 128
      src/components/ZonePricing.vue
  75. 143
      src/components/ZoneTable.vue
  76. 13
      src/index.css
  77. 19
      src/index.tsx
  78. 1
      src/logo.svg
  79. 112
      src/main.ts
  80. 18
      src/pages/main.tsx
  81. 1
      src/react-app-env.d.ts
  82. 15
      src/reportWebVitals.ts
  83. 269
      src/result.ts
  84. 96
      src/router/index.ts
  85. 3
      src/router/routes.ts
  86. 5
      src/setupTests.ts
  87. 24
      src/types.ts
  88. 126
      src/utils.ts
  89. 134
      src/views/AddTemplate.vue
  90. 112
      src/views/Checkout.vue
  91. 496
      src/views/Explore.vue
  92. 34
      src/views/Find.vue
  93. 118
      src/views/FindQ.vue
  94. 64
      src/views/Get.vue
  95. 88
      src/views/IHave.vue
  96. 78
      src/views/IKnow.vue
  97. 57
      src/views/Landing.vue
  98. 139
      src/views/MintCollection.vue
  99. 18
      src/views/MyDomains.vue
  100. 53
      src/views/TonDns.vue
  101. Some files were not shown because too many files have changed in this diff Show More

1
.babelrc

@ -0,0 +1 @@
{ "presets":["@babel/env"] }

1
.idea/agorata.iml → .idea/frontend.iml

@ -5,7 +5,6 @@
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

2
.idea/modules.xml

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/agorata.iml" filepath="$PROJECT_DIR$/.idea/agorata.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
</modules>
</component>
</project>

325
.idea/workspace.xml

@ -1,64 +1,341 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="697fb6df-d905-400a-8b28-aa6fb4ab2dd2" name="Changes" comment="">
<list default="true" id="ddb8afd5-d3ba-47b1-b6d0-227403f1abf7" name="Changes" comment="Fixed the contact editor">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/public/index.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/App.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/App.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/App.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="TypeScript File" />
<option value="Vue Single File Component" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectId" id="2HrsD8siGBzwftkpwuTkBesxHO2" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectId" id="2IoEN3itUjwell4xJk5JfAfTqN5" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"last_opened_file_path": "/Users/lionarr/WebstormProjects/agorata/src/assets",
"nodejs_interpreter_path": "node",
"nodejs_package_manager_path": "npm",
"ts.external.directory.path": "/Users/lionarr/WebstormProjects/agorata/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/ennucore/dev/agorata/frontend&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/home/ennucore/dev/agorata/frontend/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/assets" />
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src" />
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/src/assets/headers" />
<recent name="$PROJECT_DIR$/src/assets/images" />
<recent name="$PROJECT_DIR$/src/assets/icons" />
</key>
<key name="es6.move.members.recent.items">
<recent name="$PROJECT_DIR$/src/utils.ts" />
<recent name="$PROJECT_DIR$/src/api.ts" />
</key>
</component>
<component name="RunManager">
<configuration name="Debug Application" type="JavascriptDebugType" uri="http://localhost:3000">
<configuration name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="697fb6df-d905-400a-8b28-aa6fb4ab2dd2" name="Changes" comment="" />
<created>1669054415332</created>
<changelist id="ddb8afd5-d3ba-47b1-b6d0-227403f1abf7" name="Changes" comment="" />
<created>1670839492682</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1669054415332</updated>
<workItem from="1669054416534" duration="4979000" />
<updated>1670839492682</updated>
<workItem from="1670839496719" duration="10862000" />
<workItem from="1670927344373" duration="29000" />
<workItem from="1670927391338" duration="2277000" />
<workItem from="1671024025708" duration="19806000" />
<workItem from="1671204365793" duration="23468000" />
<workItem from="1671490155427" duration="1029000" />
<workItem from="1671554049942" duration="6375000" />
<workItem from="1671797380442" duration="603000" />
<workItem from="1671887059754" duration="6359000" />
<workItem from="1672337938860" duration="148000" />
<workItem from="1672483245701" duration="3321000" />
<workItem from="1672753446569" duration="4197000" />
<workItem from="1672782388693" duration="27397000" />
<workItem from="1673123515646" duration="82000" />
<workItem from="1673195751389" duration="2458000" />
<workItem from="1673683299593" duration="12019000" />
<workItem from="1673906442760" duration="1896000" />
<workItem from="1674161982371" duration="16180000" />
<workItem from="1674453019641" duration="3282000" />
<workItem from="1674576152071" duration="10420000" />
<workItem from="1674812957519" duration="2794000" />
<workItem from="1675542984242" duration="370000" />
<workItem from="1677945869469" duration="608000" />
<workItem from="1678007103478" duration="1012000" />
<workItem from="1678136230438" duration="23821000" />
<workItem from="1678294988188" duration="3137000" />
<workItem from="1678453431365" duration="7802000" />
<workItem from="1678966587213" duration="1715000" />
<workItem from="1679134080413" duration="6305000" />
<workItem from="1679494839005" duration="12422000" />
</task>
<task id="LOCAL-00001" summary="Wrote the landing">
<created>1670844191163</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1670844191164</updated>
</task>
<task id="LOCAL-00002" summary="TON Web page">
<created>1670847897936</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1670847897937</updated>
</task>
<task id="LOCAL-00003" summary="Adaptivity + more assets">
<created>1671026238851</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1671026238851</updated>
</task>
<task id="LOCAL-00004" summary="DarkLayout + Domain Bar">
<created>1671029892636</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1671029892636</updated>
</task>
<task id="LOCAL-00005" summary="Started writing ZoneTable + improvements to the search bar">
<created>1671100962105</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1671100962105</updated>
</task>
<task id="LOCAL-00006" summary="ZoneTable with ZonePricing">
<created>1671116726928</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1671116726928</updated>
</task>
<task id="LOCAL-00007" summary="/get/:zone/:domain">
<created>1671214435706</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1671214435706</updated>
</task>
<task id="LOCAL-00008" summary="Changed zone repr + results, api, and Find">
<created>1671225120000</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1671225120000</updated>
</task>
<task id="LOCAL-00009" summary="Displaying results">
<created>1671225896559</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1671225896559</updated>
</task>
<task id="LOCAL-00010" summary="Checkout, Explore">
<created>1671227655693</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1671227655693</updated>
</task>
<task id="LOCAL-00011" summary="&quot;All zones&quot; button + some other stuff">
<created>1671240411281</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1671240411281</updated>
</task>
<task id="LOCAL-00012" summary="Awesome QR at checkout">
<created>1671302241009</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1671302241009</updated>
</task>
<task id="LOCAL-00013" summary="Add (almost) monochrome logo + loading QR on checkout">
<created>1671302856594</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1671302856594</updated>
</task>
<task id="LOCAL-00014" summary="Back button + stuff for deploy">
<created>1671398124125</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1671398124125</updated>
</task>
<task id="LOCAL-00015" summary="The screens for domain owners">
<created>1671892446204</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1671892446204</updated>
</task>
<task id="LOCAL-00016" summary="Adaptivity">
<created>1671893611233</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1671893611233</updated>
</task>
<task id="LOCAL-00017" summary="Required collections + started implementing login">
<created>1672843430677</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1672843430677</updated>
</task>
<task id="LOCAL-00018" summary="TON Connect is working">
<created>1672850074714</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1672850074714</updated>
</task>
<task id="LOCAL-00019" summary="Working on the buying process">
<created>1672868732315</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1672868732315</updated>
</task>
<task id="LOCAL-00020" summary="Checkout">
<created>1672913681110</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1672913681110</updated>
</task>
<task id="LOCAL-00021" summary="Managing records">
<created>1674596285797</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1674596285797</updated>
</task>
<task id="LOCAL-00022" summary="SiteSettings">
<created>1678286612958</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1678286612958</updated>
</task>
<task id="LOCAL-00023" summary="Site constructor">
<created>1678291060051</created>
<option name="number" value="00023" />
<option name="presentableId" value="LOCAL-00023" />
<option name="project" value="LOCAL" />
<updated>1678291060051</updated>
</task>
<task id="LOCAL-00024" summary="Adding links (contacts)">
<created>1678614272869</created>
<option name="number" value="00024" />
<option name="presentableId" value="LOCAL-00024" />
<option name="project" value="LOCAL" />
<updated>1678614272869</updated>
</task>
<task id="LOCAL-00025" summary="Fix domain result when it's not available + remove failed icon">
<created>1679216484001</created>
<option name="number" value="00025" />
<option name="presentableId" value="LOCAL-00025" />
<option name="project" value="LOCAL" />
<updated>1679216484001</updated>
</task>
<task id="LOCAL-00026" summary="Updated the address">
<created>1679234696595</created>
<option name="number" value="00026" />
<option name="presentableId" value="LOCAL-00026" />
<option name="project" value="LOCAL" />
<updated>1679234696595</updated>
</task>
<task id="LOCAL-00027" summary="Fixed the contact editor">
<created>1679584324701</created>
<option name="number" value="00027" />
<option name="presentableId" value="LOCAL-00027" />
<option name="project" value="LOCAL" />
<updated>1679584324701</updated>
</task>
<option name="localTasksCounter" value="28" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Adaptivity + more assets" />
<MESSAGE value="DarkLayout + Domain Bar" />
<MESSAGE value="Started writing ZoneTable + improvements to the search bar" />
<MESSAGE value="ZoneTable with ZonePricing" />
<MESSAGE value="/get/:zone/:domain" />
<MESSAGE value="Changed zone repr + results, api, and Find" />
<MESSAGE value="Displaying results" />
<MESSAGE value="Checkout, Explore" />
<MESSAGE value="&quot;All zones&quot; button + some other stuff" />
<MESSAGE value="Awesome QR at checkout" />
<MESSAGE value="Add (almost) monochrome logo + loading QR on checkout" />
<MESSAGE value="Back button + stuff for deploy" />
<MESSAGE value="The screens for domain owners" />
<MESSAGE value="Adaptivity" />
<MESSAGE value="Required collections + started implementing login" />
<MESSAGE value="TON Connect is working" />
<MESSAGE value="Working on the buying process" />
<MESSAGE value="Checkout" />
<MESSAGE value="Managing records" />
<MESSAGE value="SiteSettings" />
<MESSAGE value="Site constructor" />
<MESSAGE value="Adding links (contacts)" />
<MESSAGE value="Fix domain result when it's not available + remove failed icon" />
<MESSAGE value="Updated the address" />
<MESSAGE value="Fixed the contact editor" />
<option name="LAST_COMMIT_MESSAGE" value="Fixed the contact editor" />
</component>
</project>

56
README.md

@ -1,46 +1,40 @@
# Getting Started with Create React App
# agorata
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
This template should help get you started developing with Vue 3 in Vite.
## Available Scripts
## Recommended IDE Setup
In the project directory, you can run:
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
### `npm start`
## Type Support for `.vue` Imports in TS
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
The page will reload if you make edits.\
You will also see any lint errors in the console.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
### `npm test`
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
## Customize configuration
### `npm run build`
See [Vite Configuration Reference](https://vitejs.dev/config/).
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
## Project Setup
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
```sh
npm install
```
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### Compile and Hot-Reload for Development
### `npm run eject`
```sh
npm run dev
```
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
### Type-Check, Compile and Minify for Production
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
```sh
npm run build
```

1
env.d.ts vendored

@ -0,0 +1 @@
/// <reference types="vite/client" />

30
index.html

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<title>Agorata</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Raleway', serif;
font-size: 1.5rem;
background: white;
}
html {
padding: 0;
}
</style>
</html>

36096
package-lock.json generated

File diff suppressed because it is too large Load Diff

68
package.json

@ -1,43 +1,41 @@
{
"name": "agorata",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.3",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.3",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"dependencies": {
"@popperjs/core": "^2.11.6",
"@tonconnect/sdk": "^2.0.4",
"axios": "^1.2.1",
"bulma": "^0.9.4",
"qr-code-styling": "^1.6.0-rc.1",
"sass": "^1.56.2",
"semver": "^7.3.8",
"tonweb": "^0.0.59",
"vue": "^3.2.45",
"vue-contenteditable": "^4.1.0",
"vue-router": "^4.1.6",
"vue3-popper": "^1.5.0",
"vuex": "^4.0.2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@babel/cli": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@types/node": "^18.11.16",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/tsconfig": "^0.1.3",
"buffer": "^6.0.3",
"npm-run-all": "^4.1.5",
"tailwindcss": "^3.2.4",
"typescript": "~4.7.4",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
}
}

BIN
public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

33
public/index.html

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

BIN
public/logo192.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/logo_mono.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

25
public/manifest.json

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

5
public/tonconnect-manifest.json

@ -0,0 +1,5 @@
{
"url": "https://tonski-an7.pages.dev",
"name": "Agorata",
"iconUrl": "https://front.agorata.io/favicon.png"
}

44
src/App.css

@ -1,44 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
p {
font-family: "Inter", sans-serif;
padding: 0;
margin: 0;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

9
src/App.test.tsx

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

15
src/App.tsx

@ -1,15 +0,0 @@
import React from 'react';
import './App.css';
import Preview from "./components/Preview";
import Header from "./components/Header";
function App() {
return (
<div className="App">
<Header></Header>
<Preview></Preview>
</div>
);
}
export default App;

16
src/App.vue

@ -0,0 +1,16 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Raleway',serif;
font-size: 1.5rem;
}
</style>

71
src/api.ts

@ -0,0 +1,71 @@
import axios from "axios";
declare var process: {
env: {
NODE_ENV: string;
};
};
export class Api {
public readonly api_url: string;
public readonly ton_api_url: string;
public readonly tonscan_url: string;
public agorata_adnl: string =
"ed4f2afebb5e49dda9684a474c5771141be1f7d85a2fa39f1823844dd476c52d";
public readonly tonviewer_url: string;
constructor() {
if (process.env.NODE_ENV === "development") {
this.api_url = "http://localhost:5170/";
this.ton_api_url = "https://testnet.tonapi.io/v2/";
this.tonscan_url = "https://testnet.tonscan.org/";
this.tonviewer_url = "https://testnet.tonviewer.com/";
} else {
this.api_url = "https://agorata.io/api/";
this.ton_api_url = "https://tonapi.io/v2/";
this.tonscan_url = "https://tonscan.org/";
this.tonviewer_url = "https://testnet.tonviewer.com/";
}
}
}
export const config = new Api();
export async function call_api(url: string) {
return (await axios.get(config.api_url + url)).data;
}
export async function call_api_post(url: string, data: any) {
return (await axios.post(config.api_url + url, data)).data;
}
export async function get_templates(url: string) {
return await new Promise((resolve) =>
resolve([
{
title: "",
description: "",
links: [{ telegram: "", mail: "", site: "" }],
preview: "https://api.agorata.io/static/mountain.jpg",
},
{
title: "",
description: "",
links: [{ telegram: "", mail: "", site: "" }],
preview: "https://api.agorata.io/static/mountain.jpg",
},
{
title: "",
description: "",
links: [{ telegram: "", mail: "", site: "" }],
preview: "https://api.agorata.io/static/mountain.jpg",
},
// {
// title: "",
// description: "",
// links: [{ telegram: "", mail: "", site: "" }],
// preview: "https://api.agorata.io/static/mountain.jpg",
// },
])
);
}

74
src/assets/base.css

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

81
src/assets/headers/getting_ton_domain.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 109 KiB

81
src/assets/headers/i-know.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 110 KiB

55
src/assets/icons/buy.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

3
src/assets/icons/globe.svg

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12C21 16.9706 16.9706 21 12 21M21 12C21 7.02944 16.9706 3 12 3M21 12H3M12 21C7.02944 21 3 16.9706 3 12M12 21C13.6569 21 15 16.9706 15 12C15 7.02944 13.6569 3 12 3M12 21C10.3431 21 9 16.9706 9 12C9 7.02944 10.3431 3 12 3M3 12C3 7.02944 7.02944 3 12 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

58
src/assets/icons/instant_buy.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

3
src/assets/icons/link.svg

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 2C12.4477 2 12 1.55228 12 1C12 0.447715 12.4477 0 13 0H19C19.5523 0 20 0.447715 20 1V7C20 7.55228 19.5523 8 19 8C18.4477 8 18 7.55228 18 7V3.41421L8.70711 12.7071C8.31658 13.0976 7.68342 13.0976 7.29289 12.7071C6.90237 12.3166 6.90237 11.6834 7.29289 11.2929L16.5858 2H13ZM3 5C2.73478 5 2.48043 5.10536 2.29289 5.29289C2.10536 5.48043 2 5.73478 2 6V17C2 17.2652 2.10536 17.5196 2.29289 17.7071C2.48043 17.8946 2.73478 18 3 18H14C14.2652 18 14.5196 17.8946 14.7071 17.7071C14.8946 17.5196 15 17.2652 15 17V11C15 10.4477 15.4477 10 16 10C16.5523 10 17 10.4477 17 11V17C17 17.7957 16.6839 18.5587 16.1213 19.1213C15.5587 19.6839 14.7957 20 14 20H3C2.20435 20 1.44129 19.6839 0.87868 19.1213C0.31607 18.5587 0 17.7957 0 17V6C0 5.20435 0.31607 4.44129 0.87868 3.87868C1.44129 3.31607 2.20435 3 3 3H9C9.55229 3 10 3.44772 10 4C10 4.55228 9.55229 5 9 5H3Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

24
src/assets/icons/logout.svg

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg10005"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs10009" />
<path
d="M7.87828 12.07C7.87828 11.66 8.21828 11.32 8.62828 11.32H14.1083V2.86C14.0983 2.38 13.7183 2 13.2383 2C7.34828 2 3.23828 6.11 3.23828 12C3.23828 17.89 7.34828 22 13.2383 22C13.7083 22 14.0983 21.62 14.0983 21.14V12.81H8.62828C8.20828 12.82 7.87828 12.48 7.87828 12.07Z"
fill="#292D32"
id="path10001"
style="fill:#ffffff" />
<path
d="M20.5416 11.5402L17.7016 8.69016C17.4116 8.40016 16.9316 8.40016 16.6416 8.69016C16.3516 8.98016 16.3516 9.46016 16.6416 9.75016L18.2016 11.3102H14.1016V12.8102H18.1916L16.6316 14.3702C16.3416 14.6602 16.3416 15.1402 16.6316 15.4302C16.7816 15.5802 16.9716 15.6502 17.1616 15.6502C17.3516 15.6502 17.5416 15.5802 17.6916 15.4302L20.5316 12.5802C20.8316 12.3002 20.8316 11.8302 20.5416 11.5402Z"
fill="#292D32"
id="path10003"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

45
src/assets/icons/ton_bottom.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

49
src/assets/icons/ton_left.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 123 KiB

45
src/assets/icons/ton_right.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

45
src/assets/icons/ton_top.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 106 KiB

BIN
src/assets/images/anton_ton.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
src/assets/images/my_name_is.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

BIN
src/assets/images/personal_page.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

BIN
src/assets/images/send_by_name.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
src/assets/images/tg_nft.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
src/assets/images/tondns_nft.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
src/assets/images/tonweb_duck.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/images/usage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
src/assets/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 119 KiB

1
src/assets/logo.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

BIN
src/assets/logo_landing.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
src/assets/logo_single.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

196
src/assets/main.css

@ -0,0 +1,196 @@
@import "./base.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
#app {
margin: 0 auto;
padding: 0;
min-width: 100vw;
min-height: 100vh;
}
a,
.green {
text-decoration: none;
color: hsl(201, 100%, 37%);
transition: 0.4s;
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0;
}
}
.b {
border: 2px solid transparent;
border-radius: 5px;
padding: 16px 26px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1.55rem;
margin: 4px 2px;
cursor: pointer;
font-weight: bold;
}
@media (max-width: 800px) {
.b {
font-size: 1.4rem;
}
}
.b.white {
background-color: white;
color: #282e46;
}
.b.white:hover {
background-color: #282e46;
color: white;
border: 2px solid white;
}
.b.blue {
background-color: #0088cc;
color: white;
border-radius: 1rem;
}
.b > img:first-child {
max-height: 1.4rem;
margin-right: 0.7rem;
}
.b > img:not(:first-child) {
max-height: 1.4rem;
margin-left: 2rem;
}
.wide.b {
min-width: 16rem;
margin: 0.5rem;
}
/* A #edeef1 box with rounded corners with black content color and content centered vertically and horizontally */
.rbox {
background-color: #edeef1;
border-radius: 2rem;
padding: 2.5rem 32px;
text-align: center;
display: flex;
align-items: center;
place-content: center;
justify-content: center;
font-size: 1.6rem;
font-weight: bold;
color: #282e46;
min-width: 16rem;
margin: 0.5rem 3rem;
}
/* small margin on mobile */
@media (max-width: 800px) {
.rbox {
margin: 0.5rem 0.5rem;
}
}
.rbox > p:not(:first-child) {
margin-top: 1rem;
}
.center {
display: flex;
justify-content: center;
align-items: center;
place-items: center;
flex-direction: column;
}
.b.darkish {
background-color: #4e5a88;
color: white;
border-radius: 0.5rem;
font-size: 1rem;
}
.mono {
font-family: "Inconsolata", monospace;
}
:root {
--popper-theme-background-color: #fff;
--popper-theme-background-color-hover: #fff;
--popper-theme-text-color: black;
--popper-theme-border-width: 0px;
--popper-theme-border-style: solid;
--popper-theme-border-radius: 6px;
--popper-theme-padding: 4px;
--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.25);
}
.popper {
font-size: 0.8rem;
font-family: "Inconsolata", monospace;
}
.mobile-scale {
scale: 100%;
}
@media (max-width: 800px) {
.mobile-scale {
scale: 80%;
}
}
.get_b {
background-color: #e36464;
color: #363e5e;
border-radius: 0.5rem;
padding: 0.2rem 0.8rem;
cursor: pointer;
}
@media only screen and (max-width: 800px) {
.get_b {
margin-left: 0.1rem;
padding-left: 0.4rem;
padding-right: 0.4rem;
}
}
.flex {
display: flex;
}
.b.back {
font-family: "Inconsolata", monospace;
font-size: 1.5rem;
padding: 0.8rem;
}
.b.back > img {
max-height: 1rem;
margin-right: 0.4rem;
}
.material-icons.language {
position: relative;
display: inline-block;
}
.material-icons.language:after {
content: "language";
}

84
src/assets/tondns.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

115
src/assets/tonweb.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 93 KiB

11
src/collection.ts

@ -0,0 +1,11 @@
export class Collection {
address: string;
name: string;
logo_url?: string;
constructor(address: string, name: string, logo_url?: string) {
this.address = address;
this.name = name;
this.logo_url = logo_url;
}
}

9
src/components/Button/button.module.css

@ -1,9 +0,0 @@
.container {
padding: 15px 30px;
background: white;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.2));
}
.container p {
font-size: 32px;
}

18
src/components/Button/index.tsx

@ -1,18 +0,0 @@
import React from 'react';
import style from './button.module.css';
type TButton = {
onClick: () => void;
text: string;
};
function Button({text, onClick}: TButton) {
return (
<div className={style.container} onClick={onClick}>
<p>{text}</p>
</div>
)
}
export default Button;

48
src/components/DarkLayout.vue

@ -0,0 +1,48 @@
<template>
<main style="width: 100vw; min-height: 100vh; height: 100%;" id="dark_body">
<Header @login-modal="login()" @logout="logout()">
<slot name="header"></slot>
</Header>
<div class="center">
<div class="content center">
<slot></slot>
</div>
</div>
<LoginModal ref="loginModal"/>
</main>
</template>
<script>
import Header from "../components/Header.vue";
import LoginModal from "./LoginModal.vue";
export default {
name: "DarkLayout",
components: {LoginModal, Header},
methods: {
login() {
this.$refs.loginModal.openModal();
},
logout() {
this.$store.dispatch('disconnect')
}
}
}
</script>
<style scoped>
#dark_body {
background-color: #282e46;
}
.content {
padding: 2.5rem 32px;
text-align: center;
display: inline-block;
font-size: 1.6rem;
color: white;
min-height: 50vh;
min-width: 16rem;
margin: 0.5rem 3rem;
}
</style>

98
src/components/DnD.vue

@ -0,0 +1,98 @@
<template>
<div class="drag-drop-uploader">
<div class="drag-drop-area" @dragover="handleDragOver" @drop="handleDrop">
<template v-if="uploadedFiles.length === 0">
<button class="button" type="button" @click="openFileInput">
Add File
</button>
<p>...or drag and drop a file.</p>
</template>
<template v-else>
<button
class="button"
type="button"
v-if="uploadedFiles.length > 0"
@click="openFileInput"
>
Change
</button>
<p>{{ uploadedFiles.at(-1).name }}</p>
</template>
</div>
<input
type="file"
ref="fileInput"
style="display: none"
@change="handleFileInput"
multiple
/>
</div>
</template>
<script>
export default {
data() {
return {
uploadedFiles: [],
};
},
methods: {
handleDragOver(event) {
event.preventDefault();
},
handleDrop(event) {
event.preventDefault();
const files = event.dataTransfer.files;
this.uploadFiles(files);
},
openFileInput() {
this.$refs.fileInput.click();
},
handleFileInput(event) {
const files = event.target.files;
this.uploadFiles(files);
},
uploadFiles(files) {
console.log(files);
for (let i = 0; i < files.length; i++) {
this.uploadedFiles.push(files[i]);
}
},
change() {
this.openFileInput();
},
},
};
</script>
<style scoped>
.drag-drop-uploader {
margin: 0 auto;
}
.drag-drop-area {
display: grid;
align-content: center;
border: 2px dashed #ccc;
border-radius: 10px;
padding: 20px;
text-align: center;
}
ul {
margin-top: 20px;
}
.button {
justify-self: center;
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
}
</style>

148
src/components/DomainBar.vue

@ -0,0 +1,148 @@
<template>
<div class="title">Find the domain of interest in the zone:</div>
<div :class="{ 'domain-bar': true, center: !has_button }">
<div style="display: flex" class="prompt-cont">
<contenteditable
:contenteditable="editable"
tag="div"
class="prompt"
:no-nl="true"
v-model="val"
:no-html="true"
ref="domain_field"
@returned="search()"
spellcheck="false"
></contenteditable>
<div class="post-prompt">{{ zone }}</div>
</div>
<div class="search" v-if="has_button" @click="search()">Search</div>
</div>
</template>
<script>
import contenteditable from "vue-contenteditable";
export default {
name: "DomainBar",
components: { contenteditable },
props: {
zone: {
type: String,
default: ".*",
},
has_button: {
type: Boolean,
default: true,
},
value: {
type: String,
default: "example",
},
editable: {
type: Boolean,
default: true,
},
},
data() {
return {
val: this.value,
};
},
mounted() {
this.$refs["domain_field"].$el.focus();
},
methods: {
search() {
this.$emit("search", this.val);
},
},
watch: {
value: function (val) {
val = val.replace("\n", "");
this.val = val;
},
val: function (val) {
this.$emit("input_d", val);
},
},
};
</script>
<style scoped>
.domain-bar {
border-radius: 1rem;
min-width: 30rem;
min-height: 5rem;
padding: 0 1rem;
background-color: #4e5a88;
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
/* flex container with vertical centering */
.prompt-cont {
display: flex;
justify-content: center;
align-items: center;
place-items: center;
margin-top: 1rem;
margin-bottom: 1rem;
}
/* the bar is narrower on mobile (90% of the screen) */
@media only screen and (max-width: 800px) {
.domain-bar {
min-width: revert;
width: 90vw;
/* allow multiple rows with 0.5 spacing */
flex-wrap: wrap;
}
}
.prompt {
border-radius: 0.5rem;
min-width: 8rem;
height: 3rem;
background-color: #363e5e;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
color: white;
font-size: 1.5rem;
font-weight: 500;
padding: 0 1rem;
outline: none;
user-select: none;
cursor: text;
margin-right: 0.4rem;
}
/* #e36464 search button with #363e5e text */
.search {
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.post-prompt {
font-family: "Inconsolata", monospace;
}
.title {
margin-bottom: 6px;
}
</style>

116
src/components/DomainResult.vue

@ -0,0 +1,116 @@
<template>
<div class="center" v-if="result === null">
<RotateSquare2 style="width: 5rem; height: 5rem; margin-top: 3rem;"/>
</div>
<GetDomainBtn v-else-if="result.canBuy() && result.condition_fullfilled !== false && result.buy_price > 0"
:domain="domain" :price="result.buy_price"
:collection_required="result.collection_required"
@click="buy()"/>
<!-- todo: differentiate between auction and buy -->
<GetDomainBtn v-else-if="result.canAuction() && result.condition_fullfilled !== false"
:domain="domain" :price="result.auction_price"
@click="buy()"/>
<div class="owned" v-else-if="result.canBuy() && result.condition_fullfilled === false" style="cursor: initial;">
<p>To buy</p>
<p><span class="domain">*.{{ zone }}</span></p>
<p>you need to be a holder of the collection</p>
<div class="collection">{{ result.collection_required.name }}</div>
</div>
<div class="owned" v-else-if="result.canBuy() && result.buy_price === 0" style="cursor: initial;">
<p>The domain</p>
<p class="domain">{{ domain }}</p>
<p>is unavailable.</p>
<p>Check the domain length or forbidden characters.</p>
</div>
<div v-else class="owned" @click="$router.push({name: 'Explore', params: {domain: domain}})">
<p class="domain">{{ domain }}</p>
<p>is owned.</p>
<p>Click here to explore or manage</p>
</div>
</template>
<script>
import RotateSquare2 from "./RotateSquare2.vue";
import {get_domain_result} from "../result";
import GetDomainBtn from "./GetDomainBtn.vue";
import {parse_zone} from "../utils";
export default {
name: "DomainResult",
components: {GetDomainBtn, RotateSquare2},
props: {
domain: {
type: String,
}
},
data() {
return {result: null}
},
mounted() {
this.get_result();
},
methods: {
async get_result() {
this.result = await get_domain_result(this.domain, this.$store.getters.address);
},
buy() {
if (this.$store.getters.is_connected) {
this.$router.push({name: 'Checkout', params: this.result.getRouteParams()})
// todo
} else {
this.$emit('login');
}
}
},
watch: {
domain: function () {
this.result = null;
this.get_result();
},
is_connected: function () {
this.result = null;
this.get_result();
}
},
computed: {
zone() {
return parse_zone(this.domain);
},
is_connected() {
return this.$store.getters.is_connected;
}
}
}
</script>
<style scoped>
.domain {
font-family: 'Inconsolata', monospace;
font-weight: bold;
font-size: 2.5rem;
}
@media only screen and (max-width: 800px) {
.domain {
font-size: 1.7rem;
}
}
.owned {
border-radius: 1rem;
background-color: #363e5e;
color: white;
font-size: 1.5rem;
text-align: center;
padding: 2rem;
cursor: pointer;
}
.collection {
border-radius: 10px;
background-color: white;
color: #0088cc;
}
</style>

198
src/components/DomainTable.vue

@ -0,0 +1,198 @@
<template>
<table class="table_outer">
<tbody class="table_content">
<tr v-for="item in items">
<td>
<div style="display: flex; align-items: center; gap: 12px">
<img
style="border-radius: 10px"
width="50"
height="50"
:src="item.previews[1].url"
:alt="item?.metadata?.name"
/>
<Tooltip text="View in explorer">
<a
target="_blank"
:href="`${config.tonscan_url}address/${item?.address}`"
>
<img src="@/assets/icons/link.svg" />
</a>
</Tooltip>
<span
style="
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{ item?.metadata?.name }}
</span>
</div>
</td>
<td>
<Tooltip text="Go to site">
<a target="_blank" :href="item?.dns">
<img
src="@/assets/icons/globe.svg"
style="display: flex"
cursor="pointer"
/>
</a>
</Tooltip>
</td>
<td>
<button @click="onManageClick(item)" class="button">Manage</button>
</td>
</tr>
</tbody>
</table>
<div class="center">
<RotateSquare2
v-if="isLoading"
style="width: 5rem; height: 5rem; margin-top: 3rem"
/>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup lang="ts">
import type { CollectionItem } from "@/types";
import axios, { AxiosError } from "axios";
import { computed, onMounted, ref, watch } from "vue";
import Tooltip from "./Tooltip.vue";
import { useRouter } from "vue-router";
import { get_zones } from "@/result";
import type { Zone } from "@/zone";
import { convertAddress } from "@/utils";
import { useStore } from "vuex";
import { config } from "@/api";
import RotateSquare2 from "../components/RotateSquare2.vue";
const store = useStore();
const { push } = useRouter();
const onManageClick = (item: CollectionItem) => {
push({ name: "Explore", params: { domain: item.metadata.name } });
};
const zonesAddresses = ref<(string | undefined)[]>([]);
const items = ref<CollectionItem[]>([]);
const isLoading = ref(false);
const error = ref("");
const address = computed(() =>
store.getters.address ? convertAddress(store.getters.address) : ""
);
const fetchDomains = async () => {
if (address.value) {
isLoading.value = true;
const zones = (await get_zones()) as Zone[];
zonesAddresses.value = zones.map(({ address }) => address?.toLowerCase());
try {
const [
resultNfts = { data: { nft_items: [] } },
resultTonNfts = { data: { nft_items: [] } },
] = await Promise.all([
axios.get<{ nft_items: CollectionItem[] }>(
`${config.ton_api_url}accounts/${address.value}/nfts`
),
axios.get<{ nft_items: CollectionItem[] }>(
`${config.ton_api_url}accounts/${address.value}/nfts`,
{
params: {
collection: "EQC3dNlesgVD8YbAazcauIrXBPfiVhMMr5YYk2in0Mtsz0Bz",
},
}
),
]);
items.value = [
...resultTonNfts.data.nft_items,
...resultNfts.data.nft_items
.map((item) => ({
...item,
formattedCollectionAddress: item.collection?.address
? convertAddress(item.collection?.address).toLowerCase()
: "",
}))
.filter(({ formattedCollectionAddress }) =>
zonesAddresses.value.includes(formattedCollectionAddress)
),
];
} catch (e) {
if ((e as AxiosError).response?.status) {
error.value = "You have made too many requests, please try again later";
console.log("too many requests");
}
} finally {
isLoading.value = false;
}
}
};
onMounted(fetchDomains);
watch(address, fetchDomains);
</script>
<style scoped>
.table_outer {
background-color: #4e5a88;
border-radius: 0.5rem;
}
td {
padding: 8px 16px;
}
td:first-child {
max-width: 400px;
}
a {
color: white;
}
@media only screen and (max-width: 800px) {
td.zone {
font-size: 1.1rem;
}
}
th {
padding: 4px 8px;
}
.table_content {
padding: 1rem;
min-height: 10rem;
}
.button {
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
</style>

91
src/components/GetDomainBtn.vue

@ -0,0 +1,91 @@
<template>
<div class="get-domain">
Get<br />
<p class="domain">{{ domain }}</p>
for<br />
<span class="price">
{{ price }}
<img src="@/assets/icons/ton_bottom.svg" class="ton_img" alt="TON" />
</span>
<template
v-if="collection_required !== undefined && collection_required !== null"
>
<br />
<div class="collection_required">
for owners of
<div class="collection">{{ collection_required.name }}</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: "GetDomainBtn",
props: {
domain: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
collection_required: {
// type: Collection | null,
default: null,
},
},
};
</script>
<style scoped>
/* #0088cc rounded box with cursor: pointer */
.get-domain {
border-radius: 1rem;
background-color: #0088cc;
color: white;
font-size: 1.5rem;
text-align: center;
padding: 2rem;
cursor: pointer;
}
@media only screen and (max-width: 800px) {
.get-domain {
width: 90vw;
/* allow multiple rows with 0.5 spacing */
flex-wrap: wrap;
}
}
.domain {
font-family: "Inconsolata", monospace;
font-weight: bold;
font-size: 2.5rem;
}
@media only screen and (max-width: 800px) {
.domain {
font-size: 1.7rem;
}
}
.price {
font-size: 3rem;
font-weight: bold;
font-family: "Inconsolata", monospace;
}
.price > img {
width: 2rem;
height: 2rem;
margin-left: -0.3rem;
}
.collection {
border-radius: 10px;
background-color: white;
color: #0088cc;
}
</style>

177
src/components/Header.vue

@ -0,0 +1,177 @@
<template>
<header>
<div class="wrapper flex" style="width: 100%">
<router-link to="/">
<img class="logo" src="@/assets/logo.png" alt="Agorata" />
</router-link>
<nav style="display: flex">
<!-- <router-link to="/">Home</router-link>-->
<!-- <router-link to="/about">About</router-link>-->
<!-- <router-link to="/contact">Contact</router-link>-->
<div>
<slot></slot>
</div>
<!-- right-aligned login button -->
<div class="login-b" v-if="showLogin">
<button
class="b blue wide"
@click="$emit('login-modal')"
v-if="!$store.getters.is_connected"
>
<span>Login</span>
</button>
<div v-else>
<div class="address">
<span>{{ address }}</span>
<img
src="@/assets/icons/logout.svg"
alt="Logout"
@click="$emit('logout')"
class="logout-icon"
/>
<div class="menu">
<router-link to="/my-domains">
<span class="menu-item">My domains</span>
</router-link>
</div>
</div>
</div>
</div>
</nav>
</div>
</header>
</template>
<script>
import { convertAddress } from "../utils.ts";
export default {
name: "Header",
props: {
showLogin: {
type: Boolean,
default: true,
},
},
computed: {
address() {
if (!this.$store.getters.is_connected) {
return "";
}
let address_raw = this.$store.getters.address;
if (address_raw === undefined) {
return "";
}
let address = convertAddress(address_raw);
return address.slice(0, 5) + "..." + address.slice(-4);
},
},
async mounted() {
this.$store.getters.connector.restoreConnection();
},
};
</script>
<style lang="scss" scoped>
header {
line-height: 1.5;
max-height: 10rem;
padding-left: 1rem;
padding-top: 1rem;
margin-bottom: 2rem;
}
div.wrapper {
flex-wrap: wrap;
}
.logo {
display: block;
margin: 0 auto 2rem;
width: 17rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
}
.menu {
background-color: #4e5a88;
position: absolute;
bottom: -30px;
right: 0;
text-align: center;
left: 0;
display: none;
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
border-top: 1px solid #363e5e;
}
.menu-item {
display: block;
padding: 4px 16px;
color: white;
}
.address {
color: white;
background-color: #4e5a88;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-family: "Inconsolata", monospace;
position: relative;
&:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
& .menu {
display: block;
}
}
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: initial;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
}
}
/* right-aligned login button */
.login-b {
margin-left: auto;
}
.logout-icon {
width: 1rem;
margin-left: 0.5rem;
cursor: pointer;
}
.address {
display: flex;
align-items: center;
}
</style>

28
src/components/Header/header.module.css

@ -1,28 +0,0 @@
.container {
width: 100%;
height: 100px;
display: flex;
align-items: center;
}
.logo {
flex-grow: 1;
display: flex;
align-items: center;
}
.logo p {
font-size: 32px;
font-weight: 600;
}
.menu {
flex-grow: 2;
display: flex;
list-style: none;
}
.menu p {
font-size: 24px;
font-width: 400;
}

22
src/components/Header/index.tsx

@ -1,22 +0,0 @@
import React from 'react';
import style from './header.module.css';
import logo from '../../assets/logo.png';
function Header() {
return (
<div className={style.container}>
<div className={style.logo}>
<img src={logo} alt="logotype"/>
<p>Agorata</p>
</div>
<ul className={style.menu}>
<li><p>how it work?</p></li>
<li><p>Community</p></li>
<li><p>Contacts</p></li>
</ul>
</div>
)
}
export default Header;

179
src/components/LoginModal.vue

@ -0,0 +1,179 @@
<template>
<transition name="fade">
<div class="modal" :class="{'hide': !show}">
<div class="modal__backdrop" @click="closeModal()"/>
<div class="modal__dialog">
<div class="modal__header">
<slot name="header"/>
<div type="button" class="modal__close" @click="closeModal()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512">
<path
fill="#fff"
d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
></path>
</svg>
</div>
</div>
<div class="modal__body content center">
<div class="text">To log in, scan this:</div>
<div class="qr center">
<div ref="loginqr" class="center qr-in"></div>
</div>
<a :href="connection_url" class="b blue wide">Or click here</a>
</div>
</div>
</div>
</transition>
</template>
<script>
import {qr_options} from "../utils";
import QRCodeStyling from 'qr-code-styling';
export default {
name: "LoginModal",
data() {
return {
show: false,
qr_code: null,
connection_url: null
};
},
methods: {
closeModal() {
this.show = false;
document.querySelector("body").classList.remove("overflow-hidden");
},
async openModal() {
this.connection_url = await this.$store.dispatch("get_connection_url");
console.log(this.connection_url, this.$store.getters.is_connected);
if (this.$store.getters.is_connected) {
return;
}
this.show = true;
document.querySelector("body").classList.add("overflow-hidden");
this.$refs["loginqr"].innerHTML = "";
this.setQR();
this.$store.state.connector.onStatusChange(
wallet => {
console.log(wallet);
this.closeModal()
}
)
},
setQR() {
this.qr_code = new QRCodeStyling(this.qr)
console.log(this.$refs["loginqr"], this.$refs)
this.qr_code.append(this.$refs["loginqr"])
}
},
computed: {
qr() {
return qr_options(this.connection_url, 0.2, 3, 0.2);
}
}
};
</script>
<style lang="scss" scoped>
.modal {
overflow-x: hidden;
overflow-y: auto;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9;
&__backdrop {
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
}
&__dialog {
background-color: #4e5a88;
position: relative;
width: 600px;
margin: 50px auto;
display: flex;
padding-bottom: 2rem;
flex-direction: column;
border-radius: 1rem;
z-index: 2;
@media screen and (max-width: 992px) {
width: 90%;
}
}
&__close {
width: 30px;
height: 30px;
cursor: pointer;
//background-color: white;
padding: 7px;
border-radius: 4px;
}
&__header {
padding: 20px 20px 10px;
display: flex;
align-items: flex-start;
justify-content: space-between;
}
&__body {
padding: 10px 20px 10px;
overflow: auto;
display: flex;
flex-direction: column;
align-items: stretch;
}
&__footer {
padding: 10px 20px 20px;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.hide {
display: none;
}
.qr > div > svg {
border-radius: 2.5rem;
padding: 1rem;
}
.qr-in {
padding: 1rem;
background-color: white;
border-radius: 1.5rem;
}
.content {
padding: 2.5rem 32px;
text-align: center;
display: inline-block;
font-size: 1.6rem;
color: white;
}
</style>

28
src/components/Preview/index.tsx

@ -1,28 +0,0 @@
import React from 'react';
import style from './preview.module.css';
import Button from "../Button";
function Preview() {
return (
<div className={style.container}>
<h1>The World Of <br/> Web3 Domains</h1>
<div className={style.buttons}>
<Button
text='I have a domain'
onClick={() => ''}
></Button>
<Button
text='I want a domain'
onClick={() => ''}
></Button>
</div>
<div className={style.domainButton}>
<div className={style.question}><p>i</p></div>
<p>What domain?</p>
</div>
</div>
)
}
export default Preview;

48
src/components/Preview/preview.module.css

@ -1,48 +0,0 @@
.container {
width: 100vw;
height: calc(100vh - 100px);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.container h1 {
padding: 0;
margin: 0;
font-size: 100px;
font-family: "Inter", sans-serif;
text-align: center;
font-weight: 500;
}
.buttons {
display: flex;
width: 620px;
justify-content: space-between;
margin: 50px 0;
}
.domainButton {
display: flex;
align-items: center;
}
.domainButton p {
font-size: 24px;
}
.question {
margin-right: 10px;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.question p {
font-size: 24px;
}

88
src/components/RotateSquare2.vue

@ -0,0 +1,88 @@
<template>
<div v-bind:style="styles" class="spinner spinner--rotate-square-2"></div>
</template>
<script>
export default {
props: {
size: {
default: '40px'
},
background: {
default: '#0000'
}
},
computed: {
styles () {
return {
width: this.size,
height: this.size,
backgroundColor: this.background
}
}
}
}
</script>
<style lang="scss" scoped>
.spinner {
position: relative;
* {
line-height: 0;
box-sizing: border-box;
}
&:before {
content: '';
width: 100%;
height: 20%;
min-width: 5px;
background: #000;
opacity: 0.1;
position: absolute;
bottom: 0%;
left: 0;
border-radius: 50%;
animation: rotate-square-2-shadow .5s linear infinite;
}
&:after {
content: '';
width: 100%;
height: 100%;
background: #834c6e;
animation: rotate-square-2-animate .5s linear infinite;
position: absolute;
bottom:40%;
left: 0;
border-radius: 3px;
}
}
@keyframes rotate-square-2-animate {
17% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(20%) rotate(22.5deg);
}
50% {
transform: translateY(40%) scale(1, .9) rotate(45deg);
border-bottom-right-radius: 50%;
}
75% {
transform: translateY(20%) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes rotate-square-2-shadow {
0%, 100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1);
}
}
</style>

400
src/components/SiteSettings.vue

@ -0,0 +1,400 @@
<template>
<div class="center">
<div>Host domain by:</div>
<div class="constr-switcher">
<div
@click="constructor_site = true"
:class="{ inactive: !constructor_site, 'constr-switch': true }"
>
Selected template
</div>
<div
@click="constructor_site = false"
:class="{ inactive: constructor_site, 'constr-switch': true }"
>
Custom server
</div>
</div>
<div class="adnl-label" v-if="!constructor_site">
You can use a custom server with an address in ADNL, TON's networking
layer
<Tooltip position="top" :width="300" class="adnl-tooltip">
<template v-slot:content>
The easiest way to set up an ADNL server is to use
<a href="https://github.com/tonwhales/ton-proxy" target="_blank"
>TON Proxy</a
>
</template>
<svg
width="24px"
height="24px"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 how-to-get"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
</Tooltip>
</div>
<div id="site_input" class="rec-field" v-if="!constructor_site">
<div class="adnl-panel">
<p class="label">Site:</p>
<contenteditable
class="record-inp site-record-field adnl-input"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="site_rec"
></contenteditable>
<div
:class="{
get_b: true,
inactive: inactiveSave,
signing: signingSite,
}"
@click="$emit('save')"
>
<template v-if="!signingSite">Save and host</template>
<template v-else>
<Socket
secondary-color="#a7aab3"
color="#282e46"
size="50px"
style="min-width: 3rem"
/>
Confirm in the wallet...
</template>
</div>
</div>
</div>
<div v-else class="constructor-form">
<Switcher
:items="templates"
@change="
(item) => {
activeTemplateName = item.name;
selectTemplate(item);
}
"
:active-name="activeTemplateName"
>
<template v-slot:suffix>
<router-link
:to="`/add-template/${$route?.params?.domain}`"
class="button"
>
Add
</router-link>
</template>
</Switcher>
<TemplatesList
:site-changed="siteChanged"
:templates="templates"
:active-template-name="activeTemplateName"
:constructor-changed="constructorChanged"
:site_rec_init="site_rec_init"
:signing-site="signingSite"
@change="
site_rec = $event;
$emit('change', site_rec_patched);
"
@change-constructor="
constructor_params = $event;
$emit('change-constructor', constructor_params);
"
@save="$emit('save', $event)"
@save-constructor="$emit('save-constructor', $event)"
/>
</div>
</div>
</template>
<script>
import Socket from "./Socket.vue";
import contenteditable from "vue-contenteditable";
import { config, get_templates } from "../api";
import { default_links, SiteConstructorParams } from "../result";
import { link_types, link_icons } from "../result";
import TemplatesList from "./TemplatesList.vue";
import Switcher from "./Switcher.vue";
import Tooltip from "./Tooltip.vue";
export default {
name: "SiteSettings",
components: { Socket, contenteditable, TemplatesList, Switcher, Tooltip },
props: {
site_rec_init: {
default: null,
},
siteChanged: {
type: Boolean,
default: false,
},
constructorChanged: {
type: Boolean,
default: false,
},
signingSite: {
type: Boolean,
default: false,
},
templateId: {
type: String,
},
},
data() {
let site_rec = this.site_rec_init;
if (!site_rec) {
site_rec = config.agorata_adnl;
this.$emit("change", this.site_rec);
}
let constructor_site =
site_rec.toLowerCase() === config.agorata_adnl.toLowerCase();
return {
site_rec,
constructor_site,
constructor_params: new SiteConstructorParams(""),
saved_constructor_params: new SiteConstructorParams(""),
contacts: [],
templates: [],
activeTemplateName: "Template #1",
};
},
watch: {
site_rec_patched() {
this.$emit("change", this.site_rec_patched);
},
constructor_params: {
handler: function (newVal) {
this.$emit("change-constructor", newVal);
this.constructor_params.template_id = this.activeTemplateName;
},
deep: true,
},
saved_constructor_params: {
handler: function (newVal, oldVal) {
if (newVal === oldVal) return;
// this.constructor_params.template_id = this.activeTemplateName;
// this.constructor_params = newVal.copy();
this.contacts = newVal.contacts ? Object.entries(newVal.contacts) : [];
},
deep: true,
},
contacts: {
handler: function (newVal) {
this.constructor_params.contacts = new Map();
for (let contact of newVal) {
this.constructor_params.contacts[contact[0]] = contact[1];
}
},
deep: true,
},
templateId() {
if (this.templateId) this.activeTemplateName = this.templateId;
},
},
computed: {
site_rec_patched() {
if (this.constructor_site) {
return config.agorata_adnl;
} else {
return this.site_rec;
}
},
templateId() {
return this.templateId;
},
inactiveSave() {
return !this.siteChanged && this.site_rec !== null;
},
link_types() {
// return the types from link_types that are not in the constructor_params.contacts
return link_types.filter(
(link_type) => !(link_type in this.constructor_params.contacts)
);
},
link_icons() {
return link_icons;
},
// used_link_types() {
// return Object.keys(this.constructor_params.contacts);
// },
},
methods: {
set_site_rec(site_rec) {
this.site_rec = site_rec;
},
addLink(link_type) {
console.log("adding link", link_type);
// If there's already a link of this type, don't add it
if (link_type in this.constructor_params.contacts) return;
this.contacts.push([link_type, default_links[link_type]]);
this.constructor_params.contacts[link_type] = default_links[link_type];
},
async setTemplates() {
this.templates = await get_templates();
this.templates = this.templates.map((template, i) => ({
...template,
name: `Template #${i + 1}`,
}));
},
selectTemplate(template) {
this.$emit("select-template", template);
},
},
mounted() {
this.setTemplates();
if (this.site_rec === null) {
this.site_rec = config.agorata_adnl;
this.$emit("change", this.site_rec);
}
},
};
</script>
<style scoped>
.record-inp {
padding: 0.3rem;
border-radius: 0.3rem;
background-color: #4e5a88;
color: white;
}
.rec-field:not(:last-child) {
margin-bottom: 1rem;
}
.get_b.inactive {
opacity: 0.5;
cursor: default;
}
.get_b.signing {
/* Center elements inside horizontally, allow rows */
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: white;
font-size: 1.4rem;
}
.constructor-form > div > div:not(:last-child) {
margin-bottom: 1rem;
}
/* The switcher with two buttons - create from template or use custom */
.constr-switcher {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-radius: 1.3rem;
padding: 1rem;
background-color: #4e5a88;
}
/* The button inside the switcher */
.constr-switch {
padding: 0.5rem 1rem;
border-radius: 0.7rem;
background-color: #cdcee8;
color: #282e46;
cursor: default;
width: 250px;
}
.constr-switch:not(:last-child) {
margin-right: 1rem;
}
.constr-switch.inactive {
/*background-color: #5d5f79;*/
background-color: #6a6e95;
color: #cecddb;
cursor: pointer;
}
.site-record-field {
font-family: "Inconsolata", monospace;
}
.link-type:not(:last-child) {
margin-right: 1rem;
}
.link-type-icon.add {
font-size: 2rem;
cursor: pointer;
}
.link-type-icon.add:hover {
color: #e88484;
}
.link-type-icon {
font-size: 2.5rem;
}
.button {
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
justify-self: end;
min-width: 150px;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.how-to-get {
cursor: pointer;
}
.adnl-panel {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
& .label {
display: grid;
justify-content: end;
}
}
.adnl-label {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.adnl-tooltip {
display: grid;
align-content: center;
}
.adnl-input {
width: 826px;
}
</style>

539
src/components/Socket.vue

@ -0,0 +1,539 @@
<template>
<div v-bind:style="styles" class="spinner spinner--socker">
<div v-bind:style="innerStyles" class="spinner-inner">
<div class="gel center-gel">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c1 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c2 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c3 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c4 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c5 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c6 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c7 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c8 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c9 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c10 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c11 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c12 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c13 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c14 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c15 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c16 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c17 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c18 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c19 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c20 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c21 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c22 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c23 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c24 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c25 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c26 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c28 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c29 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c30 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c31 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c32 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c33 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c34 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c35 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c36 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c37 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
size: {
default: '100px'
},
color: {
default: '#ec6068' // '#834c6e'
},
secondaryColor: {
default: '#7074f4' // '#286b4b'
}
},
computed: {
innerStyles() {
let size = parseInt(this.size)
return {
transform: 'scale(' + (size / 220) + ')',
'--bg-color': this.color,
'--secondary-color': this.secondaryColor
}
},
styles() {
return {
width: this.size,
height: this.size
}
}
}
}
</script>
<style lang="scss" scoped>
.spinner {
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
* {
line-height: 0;
box-sizing: border-box;
}
}
.spinner-inner {
transform-origin: center center;
width: 200px;
height: 200px;
position: relative;
}
.hex-brick {
background: var(--bg-color);
width: 30px;
height: 17px;
position: absolute;
top: 5px;
animation-name: socket-fade;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.h2 {
transform: rotate(60deg);
}
.h3 {
transform: rotate(-60deg);
}
.gel {
height: 30px;
width: 30px;
transition: all .3s;
position: absolute;
top: 50%;
left: 50%;
}
.center-gel {
margin-left: -15px;
margin-top: -15px;
animation-name: socket-pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.c1 {
margin-left: -47px;
margin-top: -15px;
}
.c2 {
margin-left: -31px;
margin-top: -43px;
}
.c3 {
margin-left: 1px;
margin-top: -43px;
}
.c4 {
margin-left: 17px;
margin-top: -15px;
}
.c5 {
margin-left: -31px;
margin-top: 13px;
}
.c6 {
margin-left: 1px;
margin-top: 13px;
}
.c7 {
margin-left: -63px;
margin-top: -43px;
}
.c8 {
margin-left: 33px;
margin-top: -43px;
}
.c9 {
margin-left: -15px;
margin-top: 41px;
}
.c10 {
margin-left: -63px;
margin-top: 13px;
}
.c11 {
margin-left: 33px;
margin-top: 13px;
}
.c12 {
margin-left: -15px;
margin-top: -71px;
}
.c13 {
margin-left: -47px;
margin-top: -71px;
}
.c14 {
margin-left: 17px;
margin-top: -71px;
}
.c15 {
margin-left: -47px;
margin-top: 41px;
}
.c16 {
margin-left: 17px;
margin-top: 41px;
}
.c17 {
margin-left: -79px;
margin-top: -15px;
}
.c18 {
margin-left: 49px;
margin-top: -15px;
}
.c19 {
margin-left: -63px;
margin-top: -99px;
}
.c20 {
margin-left: 33px;
margin-top: -99px;
}
.c21 {
margin-left: 1px;
margin-top: -99px;
}
.c22 {
margin-left: -31px;
margin-top: -99px;
}
.c23 {
margin-left: -63px;
margin-top: 69px;
}
.c24 {
margin-left: 33px;
margin-top: 69px;
}
.c25 {
margin-left: 1px;
margin-top: 69px;
}
.c26 {
margin-left: -31px;
margin-top: 69px;
}
.c27 {
margin-left: -79px;
margin-top: -15px;
}
.c28 {
margin-left: -95px;
margin-top: -43px;
}
.c29 {
margin-left: -95px;
margin-top: 13px;
}
.c30 {
margin-left: 49px;
margin-top: 41px;
}
.c31 {
margin-left: -79px;
margin-top: -71px;
}
.c32 {
margin-left: -111px;
margin-top: -15px;
}
.c33 {
margin-left: 65px;
margin-top: -43px;
}
.c34 {
margin-left: 65px;
margin-top: 13px;
}
.c35 {
margin-left: -79px;
margin-top: 41px;
}
.c36 {
margin-left: 49px;
margin-top: -71px;
}
.c37 {
margin-left: 81px;
margin-top: -15px;
}
.r1 {
animation-name: socket-pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .2s;
}
.r2 {
animation-name: socket-pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .4s;
}
.r3 {
animation-name: socket-pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .6s;
}
.r1 > .hex-brick {
animation-name: socket-fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .2s;
}
.r2 > .hex-brick {
animation-name: socket-fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .4s;
}
.r3 > .hex-brick {
animation-name: socket-fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: .6s;
}
@keyframes socket-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(0.01);
}
100% {
transform: scale(1);
}
}
@keyframes socket-fade {
0% {
background: var(--bg-color);
}
50% {
background: var(--secondary-color);
}
100% {
background: var(--bg-color);
}
}
</style>

56
src/components/Switcher.vue

@ -0,0 +1,56 @@
<template>
<div class="constr-switcher">
<div
v-for="item in items"
@click="$emit('change', item)"
:class="['constr-switch', { inactive: activeName !== item.name }]"
>
{{ item.name }}
</div>
<slot name="suffix" />
</div>
</template>
<script setup lang="ts">
type Item = { name: string };
defineProps<{ items: Item[]; activeName: string }>();
defineEmits<{
(e: "change", item: Item): void;
}>();
</script>
<style scoped>
/* The switcher with two buttons - create from template or use custom */
.constr-switcher {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-radius: 1.3rem;
padding: 1rem;
background-color: #4e5a88;
}
/* The button inside the switcher */
.constr-switch {
padding: 0.5rem 1rem;
border-radius: 0.7rem;
background-color: #cdcee8;
color: #282e46;
width: 13rem;
cursor: default;
}
.constr-switch:not(:last-child) {
margin-right: 1rem;
}
.constr-switch.inactive {
/*background-color: #5d5f79;*/
background-color: #6a6e95;
color: #cecddb;
cursor: pointer;
}
</style>

294
src/components/TemplatesList.vue

@ -0,0 +1,294 @@
<template>
<div v-for="template in templates">
<div v-if="template.name === activeTemplateName">
<img class="preview" width="280" height="280" :src="template.preview" />
<div style="display: flex; width: 100%">
<p style="width: 9rem">Title:</p>
<contenteditable
class="record-inp"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="constructor_params.title"
></contenteditable>
</div>
<div style="display: flex; width: 100%">
<p style="width: 9rem">Description:</p>
<contenteditable
class="record-inp"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="constructor_params.description"
></contenteditable>
</div>
<div style="display: flex; width: 100%">
<p style="width: 9rem">Picture:</p>
<contenteditable
class="record-inp"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="constructor_params.picture"
></contenteditable>
</div>
<div style="display: flex; width: 100%">
<p style="width: 9rem">Add link:</p>
<div
v-for="link_type in link_types"
:key="link_type"
class="link-type"
@click="addLink(link_type)"
>
<i class="link-type-icon add" :class="link_icons[link_type]"></i>
</div>
</div>
<!-- The links themselves (editing constructor_params.contacts[link_type] for each key) -->
<div v-for="contact in contacts" :key="contact[0]" class="link-type">
<div style="display: flex; width: 100%">
<!-- the icon as a label -->
<i
style="width: 9rem"
class="link-type-icon"
:class="link_icons[contact[0]]"
></i>
<!-- editing the link - constructor_params.contacts[link_type] -->
<contenteditable
class="record-inp"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="constructor_params.contacts[contact[0]]"
></contenteditable>
</div>
</div>
<div class="save_container center">
<div
:class="{
'record-submit': true,
get_b: true,
inactive: !siteChanged,
signing: signingSite,
}"
@click="$emit('save-constructor')"
>
<template v-if="!signingSite">Save and host</template>
<template v-else>
<Socket
secondary-color="#a7aab3"
color="#282e46"
size="50px"
style="min-width: 3rem"
/>
Confirm in the wallet...
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import Socket from "./Socket.vue";
import contenteditable from "vue-contenteditable";
import { config } from "../api";
import { default_links, SiteConstructorParams } from "../result";
import { link_types, link_icons } from "../result";
export default {
name: "TemplatesList",
components: { Socket, contenteditable },
props: {
site_rec_init: {
default: null,
},
siteChanged: {
type: Boolean,
default: false,
},
constructorChanged: {
type: Boolean,
default: false,
},
signingSite: {
type: Boolean,
default: false,
},
templates: {
default: [],
},
activeTemplateName: "",
},
data() {
let site_rec = this.site_rec_init;
if (!site_rec) site_rec = config.agorata_adnl;
let constructor_site =
site_rec.toLowerCase() === config.agorata_adnl.toLowerCase();
return {
site_rec,
constructor_site,
constructor_params: new SiteConstructorParams(""),
saved_constructor_params: new SiteConstructorParams(""),
contacts: [],
};
},
watch: {
site_rec_patched() {
this.$emit("change", this.site_rec_patched);
},
constructor_params: {
handler: function (newVal) {
this.$emit("change-constructor", newVal);
},
deep: true,
},
saved_constructor_params: {
handler: function (newVal, oldVal) {
if (newVal === oldVal) return;
this.constructor_params = newVal.copy();
this.contacts = Object.entries(newVal.contacts);
},
deep: true,
},
contacts: {
handler: function (newVal) {
this.constructor_params.contacts = new Map();
for (let contact of newVal) {
this.constructor_params.contacts[contact[0]] = contact[1];
}
},
deep: true,
},
},
computed: {
site_rec_patched() {
if (this.constructor_site) {
return config.agorata_adnl;
} else {
return this.site_rec;
}
},
link_types() {
// return the types from link_types that are not in the constructor_params.contacts
return link_types.filter(
(link_type) => !(link_type in this.constructor_params.contacts)
);
},
link_icons() {
return link_icons;
},
// used_link_types() {
// return Object.keys(this.constructor_params.contacts);
// },
},
methods: {
set_site_rec(site_rec) {
this.site_rec = site_rec;
},
addLink(link_type) {
console.log("adding link", link_type);
// If there's already a link of this type, don't add it
if (link_type in this.constructor_params.contacts) return;
this.contacts.push([link_type, default_links[link_type]]);
this.constructor_params.contacts[link_type] = default_links[link_type];
},
},
};
</script>
<style scoped>
.record-inp {
padding: 0.3rem;
margin-left: 0.5rem;
border-radius: 0.3rem;
background-color: #4e5a88;
color: white;
min-width: 50vw;
width: 100%;
}
.rec-field:not(:last-child) {
margin-bottom: 1rem;
}
.get_b.inactive {
opacity: 0.5;
cursor: default;
}
.get_b.signing {
/* Center elements inside horizontally, allow rows */
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: white;
font-size: 1.4rem;
}
.constructor-form > div > div > div:not(:last-child) {
margin-bottom: 1rem;
}
/* The switcher with two buttons - create from template or use custom */
.constr-switcher {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-radius: 1.3rem;
padding: 1rem;
max-width: 35rem;
background-color: #4e5a88;
}
/* The button inside the switcher */
.constr-switch {
padding: 0.5rem 1rem;
border-radius: 0.7rem;
background-color: #cdcee8;
color: #282e46;
width: 13rem;
cursor: default;
}
.constr-switch:not(:last-child) {
margin-right: 1rem;
}
.constr-switch.inactive {
/*background-color: #5d5f79;*/
background-color: #6a6e95;
color: #cecddb;
cursor: pointer;
}
.site-record-field {
font-family: "Inconsolata", monospace;
}
.link-type:not(:last-child) {
margin-right: 1rem;
}
.link-type-icon.add {
font-size: 2rem;
cursor: pointer;
}
.link-type-icon.add:hover {
color: #e88484;
}
.link-type-icon {
font-size: 2.5rem;
}
.preview {
border-radius: 1.5rem;
}
</style>

37
src/components/TonButton.vue

@ -0,0 +1,37 @@
<template>
<button class="b darkish mobile-scale">
<span><slot></slot></span>
<img src="@/assets/icons/ton_bottom.svg" alt="TON" />
</button>
</template>
<script>
export default {
name: "TonButton",
};
</script>
<style scoped>
button.b.darkish {
width: 4rem;
height: 2rem;
padding: 0.3rem;
font-family: "Inconsolata", monospace;
font-weight: bold;
font-size: 1.15rem;
cursor: default;
}
button > img:not(:first-child) {
width: 0.8rem;
height: 0.8rem;
margin-left: 0.4rem;
}
@media only screen and (max-width: 800px) {
button.b.darkish {
margin: 0.15rem;
padding: 0.15rem;
}
}
</style>

78
src/components/Tooltip.vue

@ -0,0 +1,78 @@
<template>
<div
@mouseleave="onMouseLeave"
@mouseenter="onMouseEnter"
class="tooltip-container"
>
<slot />
<div
v-if="showTooltip"
:class="{ tooltip: true, top: position === 'top' }"
:style="width ? `width: ${width}px` : ''"
>
<slot name="content" />
{{ text }}
</div>
</div>
</template>
<script>
import { ref } from "vue";
export default {
name: "Tooltip",
props: {
text: {
type: String,
required: true,
},
position: {
type: String,
},
width: {
type: Number,
},
},
setup(props) {
const showTooltip = ref(false);
function onMouseEnter() {
showTooltip.value = true;
}
function onMouseLeave() {
showTooltip.value = false;
}
return {
showTooltip,
onMouseEnter,
onMouseLeave,
};
},
};
</script>
<style>
.tooltip-container {
position: relative;
}
.tooltip {
min-width: 100px;
font-size: large;
z-index: 1;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 0.5rem;
border-radius: 0.25rem;
&.top {
transform: translate(-50%, calc(-100% - 24px));
}
}
</style>

68
src/components/WhiteLayout.vue

@ -0,0 +1,68 @@
<template>
<div style="width: 100vw">
<Header :show-login="false" />
<div class="header-logo">
<slot name="header"></slot>
</div>
<div>
<div class="rbox" style="min-height: 50vh">
<slot name="content"></slot>
</div>
</div>
<div class="buttons columns is-center is-centered">
<slot name="buttons">
<router-link :to="next">
<!-- "Next" button of class "bblue wide" with @/assets/icons/ton_right.svg icon on the right -->
<button class="b blue wide">
<span>{{ nexttext }}</span>
<img src="@/assets/icons/ton_right.svg" alt="Next" />
</button>
</router-link>
</slot>
</div>
</div>
</template>
<script>
import Header from "../components/Header.vue";
export default {
name: "WhiteLayout",
components: { Header },
props: {
next: {
type: String,
},
nexttext: {
type: String,
default: "Next",
},
},
};
</script>
<style scoped>
.header-logo {
display: block;
text-align: center;
justify-content: center;
height: 4rem;
margin: 0 auto 1.5rem;
}
.buttons {
justify-content: center;
margin: 1rem 0;
width: 100%;
display: flex;
/* Center the buttons */
align-items: center;
place-items: center;
/* Center the buttons */
}
.rbox {
min-width: 90%;
margin: auto 3%;
}
</style>

128
src/components/ZonePricing.vue

@ -0,0 +1,128 @@
<template>
<!-- div with vertical centering flex -->
<div style="display: flex; align-items: center; place-items: center">
<!--
It's a table:
| | {icon @/assets/icons/buy.svg} | {icon @/assets/icons/instant_buy.svg} |
| ---------- | ---------------------------- | ------------------------------------- |
| {{'*' * zone.length_1 + zone.zone}} | <TonButton>{{zone.price_auction_1}}</TonButton> | <TonButton>{{zone.price_buy_1}}</TonButton> |
| {{'*' * zone.length_2 + zone.zone}} | <TonButton>{{zone.price_auction_2}}</TonButton> | <TonButton>{{zone.price_buy_2}}</TonButton> |
-->
<table>
<thead>
<tr>
<th></th>
<th v-if="zone.canAuction()">
<Popper
content="Buy on auction"
placement="left"
:hover="true"
:arrow="true"
class="mobile-scale"
>
<img
src="@/assets/icons/buy.svg"
class="buy_img"
alt="Buy on auction"
/>
</Popper>
</th>
<th v-if="zone.canBuy()">
<Popper
content="Buy instantly"
placement="right"
:hover="true"
:arrow="true"
class="mobile-scale"
>
<img
src="@/assets/icons/instant_buy.svg"
class="buy_img"
alt="Instant Buy"
/>
</Popper>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ "*".repeat(zone.length_2) + "." + zone.zone }}</td>
<td v-if="zone.canAuction()">
<TonButton>{{ zone.price_auction_2 }}</TonButton>
</td>
<td v-if="zone.canBuy()">
<TonButton>{{ zone.price_buy_2 }}</TonButton>
<small style="display: block">to</small>
</td>
</tr>
<tr>
<td>{{ "*".repeat(zone.length_1) + "." + zone.zone }}</td>
<td v-if="zone.canAuction()">
<TonButton>{{ zone.price_auction_1 }}</TonButton>
</td>
<td v-if="zone.canBuy()">
<TonButton>{{ zone.price_buy_1 }}</TonButton>
</td>
</tr>
</tbody>
</table>
<router-link
:to="{ name: 'Get', params: { domain_init: 'example', zone: zone.zone } }"
>
<div v-if="has_get" class="get_b mobile-scale">Get</div>
</router-link>
</div>
</template>
<script>
import { Zone } from "../zone";
import Popper from "vue3-popper";
import TonButton from "./TonButton.vue";
export default {
name: "ZonePricing",
components: { TonButton, Popper },
props: {
zone: {
type: Zone,
default: new Zone("example.ton", 3, 5),
},
// Whether to show the "Get" button
has_get: {
type: Boolean,
default: true,
},
},
};
</script>
<style scoped>
tr > td:first-child {
font-family: "Inconsolata", monospace;
padding-right: 1rem;
padding-left: 3rem;
}
@media only screen and (max-width: 800px) {
tr > td:first-child {
font-size: 0.9rem;
padding: 0;
}
tr > td {
padding: 0;
}
}
tr > th {
padding-bottom: -10px;
}
.buy_img {
margin-bottom: -15px;
}
.get_b {
margin-top: 3.1rem;
}
</style>

143
src/components/ZoneTable.vue

@ -0,0 +1,143 @@
<template>
<div class="center" v-if="!zones.length">
<RotateSquare2 style="width: 5rem; height: 5rem; margin-top: 3rem" />
</div>
<div style="overflow-x: auto; max-width: 98vw" v-else>
<table class="table_outer">
<thead class="table_header">
<tr>
<th>Zone</th>
<th>
Prices
<Tooltip style="display: inline" text="Depends on length">
<svg
style="padding-top: 4px"
width="24px"
height="24px"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 how-to-get"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
</Tooltip>
</th>
</tr>
</thead>
<tbody class="table_content">
<tr v-for="zone in zones" :key="zone.zone">
<td v-if="zone.zone !== 'ton'" class="zone">
<router-link
:to="{
name: 'GetZ',
params: { zone: zone.zone, domain_init: domain },
}"
style="color: white"
>
{{ zone.zone }}
</router-link>
</td>
<td
v-if="zone.zone !== 'ton'"
style="display: flex; justify-content: flex-end"
>
<ZonePricing :zone="zone" />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { Zone } from "../zone";
import ZonePricing from "./ZonePricing.vue";
import RotateSquare2 from "./RotateSquare2.vue";
import Tooltip from "./Tooltip.vue";
export default {
name: "ZoneTable",
components: { ZonePricing, RotateSquare2, Tooltip },
props: {
zones: {
type: Array[Zone],
default: [],
},
domain: {
type: String,
},
},
};
</script>
<style scoped>
/*
We want a #4e5a88 rounded box with a #363e5e box inside - so that only the header is #363e5e
The outer rounded box is table_outer
The inner box is table_content, it takes all of the space except for the header
*/
.table_outer {
border-radius: 1rem;
min-height: 5rem;
background-color: #4e5a88;
display: flex;
justify-content: space-between;
flex-direction: column;
min-width: max(30rem, 50vw);
}
@media only screen and (max-width: 800px) {
td.zone {
font-size: 1.1rem;
}
}
.table_header {
padding: 0.5rem 1.3rem;
}
thead > tr {
display: flex;
}
tr > :last-child {
/* place it on the right */
justify-content: flex-end;
text-align: center;
width: 100%;
}
tr > th:first-child {
text-align: center;
}
tr > :first-child {
/* place it on the left */
text-align: left;
}
tr:not(:first-child) > td {
border-top: 2px solid white;
}
.table_content {
background-color: #363e5e;
border-radius: 1rem;
padding: 1rem;
min-height: 10rem;
}
.ton-coin-icon {
margin-left: 4px;
width: 15px;
height: 15px;
}
</style>

13
src/index.css

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

19
src/index.tsx

@ -1,19 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

112
src/main.ts

@ -0,0 +1,112 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createStore } from "vuex";
import TonConnect from "@tonconnect/sdk";
import { isWalletInfoInjected } from "@tonconnect/sdk";
import type {
WalletInfo,
WalletInfoInjected,
WalletInfoRemote,
} from "@tonconnect/sdk";
import "./assets/main.css";
const app = createApp(App);
class State {
connector: TonConnect;
walletList: WalletInfo[] = [];
initialization: Promise<void>;
constructor() {
this.connector = new TonConnect({
manifestUrl: "https://agorata.io/tonconnect-manifest.json",
});
this.initialization = this.initialize();
}
async initialize() {
await this.connector.restoreConnection();
this.walletList = await this.connector.getWallets();
}
}
const store = createStore({
state() {
return new State();
},
mutations: {
set_connector(state, connector) {
state.connector = connector;
},
},
getters: {
is_connected(state) {
return state.connector.connected;
},
address(state) {
if (
state.connector.account !== null &&
state.connector.account.address !== undefined
) {
return state.connector.account.address;
} else {
return "";
}
},
connector(state) {
return state.connector;
},
},
actions: {
async connect_embedded({ state }) {
const walletsList = await TonConnect.getWallets();
let connector = state.connector;
const embeddedWallet = walletsList.find(
(wallet) => isWalletInfoInjected(wallet) && wallet.embedded
) as WalletInfoInjected;
console.log(walletsList, embeddedWallet);
if (embeddedWallet) {
connector.connect({ jsBridgeKey: embeddedWallet.jsBridgeKey });
}
},
/* Connect embedded wallet if it exists, otherwise get connection url for a QR code unless we're already connected */
async get_connection_url({ state }): Promise<string | null> {
await state.initialization;
await this.dispatch("connect_embedded");
if (state.connector.connected) {
return null;
}
let walletList = this.state.walletList as WalletInfo[];
if (walletList.length === 0) {
return null;
}
/* iterate through wallets and do try-catch */
for (let wallet of walletList) {
try {
let wallet_r = wallet as WalletInfoRemote;
let wallet_desc = {
bridgeUrl: wallet_r.bridgeUrl,
universalLink: wallet_r.universalLink,
};
let res = await state.connector.connect(wallet_desc);
if (typeof res === "string") {
return res;
}
} catch (e) {
console.log(wallet, e);
}
}
return null;
},
async disconnect({ state }) {
await state.connector.disconnect();
},
},
});
app.use(router);
app.use(store);
app.mount("#app");

18
src/pages/main.tsx

@ -1,18 +0,0 @@
import React from 'react';
import style from './button.module.css';
type TButton = {
onClick: () => void;
text: string;
};
function Button({text, onClick}: TButton) {
return (
<div className={style.container} onClick={onClick}>
<p>{text}</p>
</div>
)
}
export default Button;

1
src/react-app-env.d.ts vendored

@ -1 +0,0 @@
/// <reference types="react-scripts" />

15
src/reportWebVitals.ts

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

269
src/result.ts

@ -0,0 +1,269 @@
// import {call_api} from "@/api";
import { Zone } from "@/zone";
import type { Message } from "@/utils";
import type { Collection } from "@/collection";
import { call_api } from "@/api";
// let ex_collection = () => new Collection("example.ton", "Example collection");
export class Result {
domain: string;
buy_price?: number;
auction_price?: number;
owner?: string;
collection_required: Collection | null;
condition_fulfilled: boolean | null = null;
buy_msg: Message | null = null;
content_msg: Message | null = null;
nft_info?: any;
constructor(
domain: string,
buy_price?: number,
auction_price?: number,
owner?: string,
collection_required: Collection | null = null,
condition_fulfilled: boolean | null = null,
buy_msg: Message | null = null,
content_msg: Message | null = null,
nft_info?: any
) {
this.domain = domain;
this.buy_price = buy_price;
this.auction_price = auction_price;
this.owner = owner;
this.collection_required = collection_required;
this.condition_fulfilled = condition_fulfilled;
this.buy_msg = buy_msg;
this.content_msg = content_msg;
this.nft_info = nft_info;
}
static fromBackend(data: any): Result {
let domain = data.domain;
let buy_price = data.buy_price;
let auction_price = data.auction_price;
let owner = data.owner;
let collection_required = data.collection_required;
let condition_fulfilled = data.condition_fulfilled;
let buy_msg = data.buy_msg;
let content_msg = data.content_msg;
let nft_info = data.nft_info;
return new Result(
domain,
buy_price,
auction_price,
owner,
collection_required,
condition_fulfilled,
buy_msg,
content_msg,
nft_info
);
}
getRouteParams(): any {
return {
domain_init: /* domain up to . */ this.domain.split(".")[0],
domain: /* domain up to . */ this.domain.split(".")[0],
zone: /* domain after . */ this.domain.split(".").slice(1).join("."),
};
}
canAuction(): boolean {
return (
this.auction_price !== undefined &&
this.auction_price !== null &&
this.owner === undefined
);
}
canBuy(): boolean {
return (
this.buy_price !== undefined &&
this.buy_price !== null &&
this.owner === undefined
);
}
zone(): string {
return this.domain.split(".").slice(1).join(".");
}
}
// const sleep = (milliseconds: number) => {
// return new Promise(resolve => setTimeout(resolve, milliseconds))
// }
export async function get_search_results(query: string) {
const res = await call_api("find/" + query);
return res.map((item: any) => ({
...item,
getRouteParams: () => ({
domain_init: /* domain up to . */ item.domain.split(".")[0],
domain: /* domain up to . */ item.domain.split(".")[0],
zone: /* domain after . */ item.domain.split(".").slice(1).join("."),
}),
canAuction: () =>
item.auction_price !== undefined &&
item.auction_price !== null &&
item.owner === undefined,
canBuy: () =>
item.buy_price !== undefined &&
item.buy_price !== null &&
item.owner === undefined,
zone: () => item.domain.split(".").slice(1).join("."),
}));
// canAuction
// canBuy
// zone
// return {...res, };};
/*await sleep(200);
return [
new Result(query + '.ton', 5, 3),
new Result(query + '.ton', 1),
new Result(query + '.ton', undefined, 2),
new Result(query + '.ton', undefined, undefined, '123')
];*/
}
export async function get_domain_result(domain: string, _address?: string) {
return Result.fromBackend(await call_api("get/" + domain));
/*await sleep(100);
if (domain === 'test.ton') {
return new Result(domain);
}
let exc = ex_collection();
if (domain.startsWith('owned')) {
return new Result(domain, 5, 3, '123');
}
if (parse_zone(domain) === 'example.ton') {
return new Result(domain, 5, 3, undefined, exc);
}
if (parse_zone(domain) === 'testtesttest.ton') {
console.log(address)
if (address !== undefined && address !== '') {
return new Result(domain, 5, 3, undefined, exc, false);
} else {
return new Result(domain, 5, 3, undefined, exc);
}
}
return new Result(domain, 5, 3);*/
}
export async function get_records(address: string) {
return await call_api("records/" + address);
}
export async function get_zones() {
let zones_back = await call_api("zones");
return zones_back.map(
(zone: any) =>
new Zone(
zone.zone,
zone.price_buy_1,
zone.price_buy_2,
zone.collection_required,
zone.price_auction_1,
zone.price_auction_2,
zone.min_length,
zone.length_1,
zone.length_2,
zone.address
)
);
/*await sleep(10);
return [
new Zone("example.ton", 3, 5, ex_collection()),
new Zone("agorata.ton", 3, 5),
new Zone("testtesttest.ton", 1, 1, ex_collection())];*/
}
export class TonLink {
address: string;
sum?: number;
message: string;
constructor(address: string, message: string, sum?: number) {
this.address = address;
this.sum = sum;
this.message = message;
}
getLink(): string {
// todo: use tonapi to run dnsresolve on the address (or move the task to the backend) - domains don't work well in links
let link = `ton://transfer/${this.address}?message=${this.message}`;
if (this.sum !== undefined) {
link += `&amount=${this.sum}`;
}
return link;
}
}
// Get the link for buying a domain
export function get_ton_link(res: Result) {
return new TonLink(res.zone(), "b/" + res.domain, res.buy_price!);
}
export let link_types = ["telegram", /*'website',*/ "getgems", "email"];
export let link_icons = {
telegram: "fab fa-telegram",
website: "material-icons language",
getgems: "fas fa-gem",
email: "fas fa-envelope",
};
export let default_links = {
telegram: "https://t.me/",
website: "https://",
getgems: "https://getgems.org/",
email: "example@example.org",
};
export class SiteConstructorParams {
domain: string;
title: string;
description: string;
contacts: Map<string, string> = new Map<string, string>();
template_id: string;
picture: string;
blog: string[];
constructor(
domain: string,
title: string = "",
description: string = "",
contacts: Map<string, string> = new Map<string, string>(),
template_id: string = "",
picture: string = "",
blog = []
) {
this.domain = domain;
this.title = title;
this.description = description;
this.contacts = contacts;
this.template_id = template_id;
this.picture = picture;
this.blog = blog;
}
copy(): SiteConstructorParams {
return new SiteConstructorParams(this.domain, this.title);
}
}
export async function get_constr_params(domain: string) {
let res = await call_api("get-site-data/" + domain);
return new SiteConstructorParams(
res.domain,
res.title,
res.description,
res.contacts,
res.template_id,
res.picture,
res.blog
);
}

96
src/router/index.ts

@ -0,0 +1,96 @@
import { createRouter, createWebHistory } from "vue-router";
import Landing from "../views/Landing.vue";
import { addTemplate, mintCollection, myDomains } from "./routes";
/*
/find - bar + table
/find/query - search results
/get/zone - get a domain in a zone (should be able to go to /find)
/get/zone/domain - get a specific domain
*/
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: Landing,
},
{
path: "/tonweb",
name: "tonweb",
component: () => import("../views/TonWeb.vue"),
},
{
path: "/i-know",
name: "IKnow",
component: () => import("../views/IKnow.vue"),
},
{
path: "/i-have",
name: "IHave",
component: () => import("../views/IHave.vue"),
},
{
path: "/tondns",
name: "tondns",
component: () => import("../views/TonDns.vue"),
},
{
path: "/get/:zone/:domain_init",
name: "GetZ",
component: () => import("../views/Get.vue"),
props: true,
},
{
path: "/get/:zone/:domain_init",
name: "Get",
component: () => import("../views/Get.vue"),
props: true,
},
{
path: "/find",
name: "Find",
component: () => import("../views/Find.vue"),
},
{
path: "/checkout/:zone/:domain",
name: "Checkout",
component: () => import("../views/Checkout.vue"),
props: true,
},
{
path: "/find/:query",
name: "FindQ",
component: () => import("../views/FindQ.vue"),
props: true,
},
{
path: "/explore/:domain",
name: "Explore",
component: () => import("../views/Explore.vue"),
props: true,
},
{
path: "/my-domains",
name: myDomains,
component: () => import("../views/MyDomains.vue"),
props: true,
},
{
path: "/mint-collection/:domain",
name: mintCollection,
component: () => import("../views/MintCollection.vue"),
props: true,
},
{
path: "/add-template/:domain",
name: addTemplate,
component: () => import("../views/AddTemplate.vue"),
props: true,
},
],
});
export default router;

3
src/router/routes.ts

@ -0,0 +1,3 @@
export const myDomains = "my-domains";
export const mintCollection = "mint-collection";
export const addTemplate = "add-template";

5
src/setupTests.ts

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

24
src/types.ts

@ -0,0 +1,24 @@
export type Collection = {
address: string;
metadata: {
description: string;
image: string;
name: string;
};
next_item_index: number;
raw_collection_content: string;
};
export type CollectionItem = {
address: string;
metadata: {
name: string;
description: string;
image: string;
};
collection: {
address: string;
};
dns: string;
previews: { resolution: string; url: string }[];
};

126
src/utils.ts

@ -0,0 +1,126 @@
export class Message {
address: string;
amount: string;
payload: string;
constructor(address: string, amount: string, payload: string) {
this.address = address;
this.amount = amount;
this.payload = payload;
}
}
export function qr_options(url: string, imageSize: number = 0.4, imageMargin: number = 3, margin: number = 10): any {
return {
width: 300,
height: 300,
type: 'svg',
data: url,
image: '/logo_mono.png',
margin: margin,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'Q'
},
imageOptions: {
hideBackgroundDots: true,
imageSize: imageSize,
margin: imageMargin,
crossOrigin: 'anonymous',
},
dotsOptions: {
color: '#282e46',
type: 'rounded'
},
backgroundOptions: {
color: '#ffffff',
},
cornersSquareOptions: {
color: '#282e46',
type: 'extra-rounded',
},
cornersDotOptions: {
color: '#282e46',
type: 'dot',
}
}
}
// export function convert_address(address_raw: string): string {
// const address = new TonWeb.utils.Address(address_raw);
// return address.toString(true, true, true);
// }
export function parse_zone(domain: string) {
// extract the zone from the domain (e.g. example.ton from 123.example.ton)
return domain.split('.').slice(1).join('.');
}
// look up tables
const to_hex_array = [];
// map from string to a number
const to_byte_map: any = {};
for (let ord = 0; ord <= 0xff; ord++) {
let s = ord.toString(16);
if (s.length < 2) {
s = "0" + s;
}
to_hex_array.push(s);
to_byte_map[s] = ord;
}
export function hexToBytes(s: string) {
s = s.toLowerCase();
const length2 = s.length;
if (length2 % 2 !== 0) {
throw "hex string must have length a multiple of 2";
}
const length = length2 / 2;
const result = new Uint8Array(length);
for (let i = 0; i < length; i++) {
const i2 = i * 2;
const b = s.substring(i2, i2 + 2);
if (!to_byte_map.hasOwnProperty(b)) throw new Error('invalid hex character ' + b);
result[i] = to_byte_map[b];
}
return result;
}
function crc16(data: any) {
const poly = 0x1021;
let reg = 0;
const message = new Uint8Array(data.length + 2);
message.set(data);
for (let byte of message) {
let mask = 0x80;
while (mask > 0) {
reg <<= 1;
if (byte & mask) {
reg += 1;
}
mask >>= 1
if (reg > 0xffff) {
reg &= 0xffff;
reg ^= poly;
}
}
}
return new Uint8Array([Math.floor(reg / 256), reg % 256]);
}
export function convertAddress(address: string) {
const addrSrc = hexToBytes(address.split(':')[1]);
const addr = new Int8Array(34);
addr[0] = 0x11; // tag (bounceable)
addr[1] = 0; // workchain
addr.set(addrSrc, 2);
const addressWithChecksum = new Uint8Array(36);
addressWithChecksum.set(addr);
addressWithChecksum.set(crc16(addr), 34);
// @ts-ignore
let addressBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(addressWithChecksum)));
addressBase64 = addressBase64.replace(/\+/g, '-').replace(/\//g, '_');
return addressBase64;
}

134
src/views/AddTemplate.vue

@ -0,0 +1,134 @@
<template>
<DarkLayout>
<form class="form">
<div class="input-wrapper">
<label for="template-name">Template name</label>
<input v-model="name" class="input" id="template-name" type="text" />
</div>
<DragDropUploader class="dnd" />
<div v-for="(_, i) in fields" class="input-wrapper">
<div class="fields-wrapper">
<div class="input-wrapper">
<label :for="`field-${i}`">Field {{ i + 1 }}</label>
<input
placeholder="Name"
v-model="fields[i].name"
class="input"
:id="`field-${i}`"
type="text"
/>
</div>
<div>
<input
placeholder="Value"
v-model="fields[i].value"
class="input"
:id="`field-${i}`"
type="text"
/>
</div>
</div>
</div>
<button
class="button button-plus"
style="justify-self: start"
type="button"
@click="fields.push({ name: '', value: '' })"
>
+
</button>
<button class="button" type="button">Submit</button>
</form>
<template v-slot:header>
<router-link to="/find">
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
Back
</button>
</router-link>
</template>
</DarkLayout>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DarkLayout from "../components/DarkLayout.vue";
import DragDropUploader from "@/components/DnD.vue";
const name = ref("");
const fields = ref([{ name: "", value: "" }]);
</script>
<style>
.input-wrapper {
display: flex;
flex-direction: column;
align-items: start;
}
.input {
display: block;
width: 100%;
min-height: 40px;
border: 0;
border-radius: 10px;
padding: 12px 24px;
background-color: #4e5a88;
color: white;
font-size: 24px;
font-family: "Raleway", serif;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.form {
display: grid;
gap: 24px;
min-width: 600px;
}
.button {
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
justify-self: end;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
}
.button-plus {
background-color: #4e5a88;
color: white;
}
.info {
width: 600px;
font-size: 16px;
text-align: right;
margin-top: 40px;
}
.fields-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: end;
gap: 40px;
}
.dnd {
width: 600px;
}
</style>

112
src/views/Checkout.vue

@ -0,0 +1,112 @@
<template>
<DarkLayout>
<template v-slot:header>
<router-link
:to="{ name: 'Get', params: { domain_init: domain, zone: zone } }"
>
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
Back
</button>
</router-link>
</template>
<!-- todo: handle auction -->
<div class="text">To buy</div>
<DomainBar
:zone="'.' + zone"
:value="domain"
:has_button="false"
:editable="false"
/>
<div class="text">Confirm the transaction in your wallet</div>
<div class="center">
<Socket />
</div>
</DarkLayout>
</template>
<script>
import DarkLayout from "../components/DarkLayout.vue";
import DomainBar from "../components/DomainBar.vue";
import { get_domain_result, get_ton_link } from "../result";
import RotateSquare2 from "../components/RotateSquare2.vue";
import { qr_options } from "../utils";
import Socket from "../components/Socket.vue";
export default {
name: "Checkout",
components: { Socket, RotateSquare2, DomainBar, DarkLayout },
props: {
domain: {
type: String,
},
zone: {
type: String,
},
},
data() {
return {
result: null,
ton_link: null,
qr_code: null,
};
},
methods: {
async get_result() {
this.result = await get_domain_result(this.domain + "." + this.zone);
this.ton_link = await get_ton_link(this.result);
},
async sign_transaction() {
await this.get_result();
let d = new Date();
let validness = parseInt((d.getTime() / 1000).toFixed(0)) + 360000;
const transaction = {
validUntil: validness,
messages: [Object.assign({}, this.result.buy_msg)],
};
console.log("Signing the transaction", transaction);
const result = await this.$store.state.connector.sendTransaction(
transaction
);
console.log(result);
if (result.boc === "") {
this.$router.push({
name: "Get",
params: { domain_init: this.domain, zone: this.zone },
});
}
let res = await get_domain_result(this.domain + "." + this.zone);
while (res.owner !== this.$store.getters.address) {
res = await get_domain_result(this.domain + "." + this.zone);
}
this.$router.push({
name: "Explore",
params: { domain: this.domain + "." + this.zone },
});
},
},
mounted() {
this.sign_transaction();
},
computed: {
loaded() {
return this.result !== null && this.ton_link !== null;
},
options() {
if (!this.loaded) {
return null;
}
return qr_options(this.ton_link.getLink());
},
},
};
</script>
<style>
.qr > div > svg {
border-radius: 2rem;
}
</style>

496
src/views/Explore.vue

@ -0,0 +1,496 @@
<template>
<DarkLayout>
<template v-slot:header>
<router-link
:to="{ name: 'Get', params: { domain_init: core_domain, zone: zone } }"
>
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
Back
</button>
</router-link>
</template>
<div style="min-width: 50%">
<DomainBar
:value="core_domain"
:zone="'.' + zone"
:has_button="false"
:editable="false"
/>
<div v-if="loading" class="center">
<RotateSquare2 style="width: 5rem; height: 5rem; margin-top: 3rem" />
</div>
<div v-else>
<div class="owned" v-if="result.owner && !isMine">
<p class="domain">{{ domain }}</p>
<div>
is owned by
<div class="owner-address">{{ ownerAddr }}</div>
</div>
<a :href="result.nft_info.explorer_link">View nft</a>
</div>
<div v-else-if="isMine" class="owned">
<p class="domain">{{ domain }}</p>
<div>
is owned by
<div class="owner-address">you</div>
</div>
<div class="buttons">
<a :href="result.nft_info.explorer_link">View nft</a>
<router-link class="button" :to="`/mint-collection/${domain}`">
Mint a subdomain collection
</router-link>
</div>
<br />
<div>
You can associate your domain with your wallet<br />to receive
incoming transaction at the domain address
</div>
<div id="wallet_input" class="rec-field">
<div class="wallet-panel">
<p class="label">Wallet:</p>
<contenteditable
class="record-inp wallet-record-field"
tag="div"
:no-hl="true"
:no-html="true"
spellcheck="false"
v-model="wallet_rec"
></contenteditable>
<div
:class="`use-my-wallet ${
wallet_rec === $store.getters.address ? 'disabled' : ''
}`"
:disabled="wallet_rec === $store.getters.address"
@click="useMyWallet"
>
Use my wallet
</div>
<div
:class="{
'record-submit': true,
get_b: true,
inactive: !walletChanged,
signing: signingWallet,
}"
@click="saveWallet()"
>
<template v-if="!signingWallet">Save</template>
<template v-else>
<Socket
secondary-color="#a7aab3"
color="#282e46"
size="50px"
style="min-width: 3rem"
/>
Confirm in the wallet...
</template>
</div>
</div>
</div>
<SiteSettings
ref="site_settings"
@save="saveSite()"
@save-constructor="saveSiteConstr()"
@select-template="selectTemplate"
@change="site_rec = $event"
:template-id="constructor_params.template_id"
:site-changed="siteChanged"
:signing-site="signingSite"
@change-constructor="changeConstructor"
/>
<!-- @change-constructor="changeConstructor" -->
</div>
<div v-else>
This domain is not owned.
<router-link
:to="{
name: 'Get',
params: { domain_init: core_domain, zone: zone },
}"
>
<button class="b darkish back">Get it</button>
</router-link>
</div>
</div>
</div>
</DarkLayout>
</template>
<script>
import DarkLayout from "../components/DarkLayout.vue";
import DomainBar from "../components/DomainBar.vue";
import RotateSquare2 from "../components/RotateSquare2.vue";
import {
get_constr_params,
get_domain_result,
get_records,
SiteConstructorParams,
} from "../result";
import { convertAddress } from "../utils";
import contenteditable from "vue-contenteditable";
import { call_api, call_api_post, config } from "../api";
import Socket from "../components/Socket.vue";
import SiteSettings from "../components/SiteSettings.vue";
export default {
name: "Explore",
components: {
SiteSettings,
Socket,
DomainBar,
DarkLayout,
RotateSquare2,
contenteditable,
},
props: {
domain: {
type: String,
},
},
data() {
const saved_constructor_params = new SiteConstructorParams(
this.domain,
this.domain
);
return {
result: null,
records: {
wallet: null,
storage: null,
uri: null,
next_resolver: null,
site: null,
},
wallet_rec: null,
site_rec: null,
signingSite: false,
signingWallet: false,
constructor_params: saved_constructor_params,
saved_constructor_params,
interval: null,
};
},
mounted() {
this.interval = setInterval(
() =>
get_domain_result(this.domain, this.$store.getters.address).then(
(r) => {
this.result = Object.assign({}, r);
if (this.isMine) {
get_constr_params(this.domain).then((r) => {
this.constructor_params = r;
this.saved_constructor_params = r;
this.updSettingsComponent();
});
get_records(r.nft_info.address).then((r) => {
this.records = r;
// this.timer = setTimeout(this.updRecords, 10000);
this.updSettingsComponent();
});
}
}
),
20000
);
},
computed: {
core_domain() {
return this.domain.split(".")[0];
},
zone() {
return this.domain.split(".").slice(1).join(".");
},
loading() {
return this.result === null;
},
ownerAddr() {
let owner = this.result.owner;
if (!owner instanceof String) {
return "";
}
let address = convertAddress(owner);
return address.slice(0, 5) + "..." + address.slice(-4);
},
isMine() {
console.log(this.result);
return (
this.result.owner === this.$store.getters.address ||
this.result.nft_info.owner.address === this.$store.getters.address
);
},
walletChanged() {
return this.records && this.wallet_rec !== this.records.wallet;
},
siteChanged() {
let constr_change = false;
if (this.site_rec === config.agorata_adnl) {
constr_change =
this.constructor_params !== this.saved_constructor_params;
}
return (
(this.records &&
(this.site_rec !== this.records.site || constr_change)) ||
(!this.records && constr_change) ||
(this.records !== null && this.site_rec === null)
);
},
settingsCompLoaded() {
return this.$refs.site_settings !== undefined;
},
},
methods: {
async saveWallet() {
if (!this.walletChanged) {
return;
}
// get the message from api at /set-record/site/{site} and sign it with tonconnect (from storage)
let message = await call_api("set-record/wallet/" + this.wallet_rec);
// send the message to tonconnect
let d = new Date();
let validness = parseInt((d.getTime() / 1000).toFixed(0)) + 360000;
let transaction = {
validUntil: validness,
messages: [
{
amount: (0.05 * 1000000000).toString(),
address: this.result.nft_info.address,
payload: message,
},
],
};
this.signingWallet = true;
console.log("Sending transaction", transaction);
const result = await this.$store.state.connector.sendTransaction(
transaction
);
this.signingWallet = false;
if (!result.boc) {
// todo
}
this.records.wallet = this.wallet_rec;
},
async saveSite() {
if (!this.siteChanged) {
return;
}
// get the message from api at /set-record/site/{site} and sign it with tonconnect (from storage)
let message = await call_api("set-record/site/" + this.site_rec);
// send the message to tonconnect
let d = new Date();
let validness = parseInt((d.getTime() / 1000).toFixed(0)) + 360000;
let transaction = {
validUntil: validness,
messages: [
{
amount: (0.05 * 1000000000).toString(),
address: this.result.nft_info.address,
payload: message,
},
],
};
this.signingSite = true;
const result = await this.$store.state.connector.sendTransaction(
transaction
);
this.signingSite = false;
if (!result.boc) {
// todo
}
this.records.site = this.site_rec;
},
async saveSiteConstr() {
if (!this.site_rec) {
this.site_rec = config.agorata_adnl.toLowerCase();
await this.saveSite();
} else if (this.site_rec !== this.records.site) {
await this.saveSite();
}
this.constructor_params.domain = this.domain;
console.log("this.constructor_params", this.constructor_params);
await call_api_post("set-site-data", {
...this.constructor_params,
blog:
this.constructor_params.template_id === "Template #3"
? [
"Ipsum dolor sit amet",
"Consectetur adipiscing elit",
"Sed do eiusmod tempor",
]
: undefined,
});
this.saved_constructor_params = this.constructor_params;
this.updSettingsComponent();
},
updRecords() {
get_records(this.result.nft_info.address).then((r) => {
if (this.records.wallet !== r.wallet || this.records.site !== r.site) {
this.records = r;
}
});
},
updSettingsComponent() {
if (this.$refs.site_settings) {
this.$refs.site_settings.set_site_rec(this.site_rec);
this.$refs.site_settings.saved_constructor_params =
this.saved_constructor_params;
}
},
selectTemplate(template) {
this.constructor_params = {
...this.constructor_params,
template_id: template.name,
};
},
useMyWallet() {
this.wallet_rec = this.$store.getters.address;
},
changeConstructor(evt) {
this.constructor_params = evt;
},
},
watch: {
records: function (val) {
if (val) {
this.wallet_rec = val.wallet;
if (!this.site_rec || val.site) {
this.site_rec = val.site;
}
}
},
site_rec() {
if (this.$refs.site_settings) {
this.$refs.site_settings.set_site_rec(this.site_rec);
}
},
saved_constructor_params() {
this.$refs.site_settings.saved_constructor_params =
this.saved_constructor_params;
},
settingsCompLoaded() {
if (!this.loading && this.settingsCompLoaded) {
this.updSettingsComponent();
}
},
loading() {
if (!this.loading && this.settingsCompLoaded) {
this.updSettingsComponent();
}
},
},
unmounted() {
clearInterval(this.interval);
},
};
</script>
<style scoped>
.domain {
font-family: "Inconsolata", monospace;
font-weight: bold;
font-size: 2rem;
}
@media only screen and (max-width: 800px) {
.domain {
font-size: 1.7rem;
}
}
.owned {
border-radius: 1rem;
background-color: #363e5e;
color: white;
font-size: 1.5rem;
text-align: center;
padding: 2rem;
}
.owner-address {
font-family: "Inconsolata", monospace;
color: #c86161;
font-size: 2rem;
}
.record-inp {
padding: 0.3rem;
border-radius: 0.3rem;
background-color: #4e5a88;
color: white;
}
.rec-field:not(:last-child) {
margin-bottom: 4rem;
}
.get_b.inactive {
opacity: 0.5;
cursor: default;
}
.get_b.signing {
/* Center elements inside horizontally, allow rows */
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: white;
font-size: 1.4rem;
}
.wallet-record-field {
font-family: "Inconsolata", monospace;
}
.button {
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}
.wallet-panel {
display: grid;
grid-template-columns: auto 1fr 183px auto;
align-items: center;
gap: 10px;
& .label {
display: grid;
justify-content: end;
}
}
.use-my-wallet {
border-radius: 0.5rem;
padding: 0.2rem 0.8rem;
cursor: pointer;
background-color: #cdcee8;
color: #282e46;
&.disabled {
cursor: not-allowed;
background-color: #6a6e95;
color: #cecddb;
}
}
</style>

34
src/views/Find.vue

@ -0,0 +1,34 @@
<template>
<DarkLayout>
<div class="center">
<DomainBar :value="query" @search="search()" @input_d="query = $event" />
</div>
<ZoneTable :domain="query" :zones="zones" />
</DarkLayout>
</template>
<script>
import DomainBar from "../components/DomainBar.vue";
import DarkLayout from "../components/DarkLayout.vue";
import ZoneTable from "../components/ZoneTable.vue";
import { get_zones } from "../result";
export default {
name: "Find",
data() {
return {
query: "example",
zones: [],
};
},
async mounted() {
this.zones = await get_zones();
},
methods: {
search() {
this.$router.push({ name: "FindQ", params: { query: this.query } });
},
},
components: { ZoneTable, DarkLayout, DomainBar },
};
</script>

118
src/views/FindQ.vue

@ -0,0 +1,118 @@
<template>
<DarkLayout>
<div class="center">
<DomainBar zone="" :value="query_" @search="search()" @input_d="query_ = $event"/>
<div class="results">
<RotateSquare2 v-if="results === null" style="width: 5rem; height: 5rem"/>
<div v-for="result in results" :key="result" class="mobile-scale">
<div class="search-result">
<span class="domain">{{ result.domain }}</span>
<table>
<thead>
<tr>
<th v-if="result.canAuction()">
<Popper content="Buy on auction" placement="left" :hover="true" :arrow="true">
<img src="@/assets/icons/buy.svg" class="buy_img" alt="Buy on auction"/>
</Popper>
</th>
<th v-if="result.canBuy()">
<Popper content="Buy instantly" placement="right" :hover="true" :arrow="true">
<img src="@/assets/icons/instant_buy.svg" class="buy_img" alt="Instant Buy"/>
</Popper>
</th>
</tr>
</thead>
<tbody>
<tr>
<td v-if="result.canAuction()">
<TonButton>{{ result.auction_price }}</TonButton>
</td>
<td v-if="result.canBuy()">
<TonButton>{{ result.buy_price }}</TonButton>
</td>
</tr>
</tbody>
</table>
<router-link :to="{name: 'Get', params: result.getRouteParams()}">
<div class="get_b">
<template v-if="result.canBuy() || result.canAuction()">Get</template>
<template v-else>See</template>
</div>
</router-link>
</div>
</div>
</div>
</div>
</DarkLayout>
</template>
<script>
import DomainBar from "../components/DomainBar.vue";
import DarkLayout from "../components/DarkLayout.vue";
import RotateSquare2 from "../components/RotateSquare2.vue";
import TonButton from "../components/TonButton.vue";
import Popper from "vue3-popper";
import {get_search_results} from "../result";
export default {
name: "FindQ",
props: {
query: {
type: String,
},
},
data() {
return {
query_: this.query,
results: null,
}
},
mounted() {
this.getResults();
},
components: {RotateSquare2, DarkLayout, DomainBar, TonButton, Popper},
methods: {
search() {
this.$router.push({name: 'FindQ', params: {query: this.query_}});
},
async getResults() {
this.results = await get_search_results(this.query_);
}
},
}
</script>
<style scoped>
.results {
margin-top: 3rem;
}
.search-result {
margin: 1rem 0;
padding: 1rem;
border-radius: 1rem;
background-color: #4e5a88;
color: white;
font-size: 1.5rem;
display: flex;
height: 6rem;
justify-content: space-between;
align-items: center;
}
.search-result > .domain {
font-family: 'Inconsolata', monospace;
}
table {
width: 10rem;
}
.buy_img {
margin-bottom: -15px;
}
tr > th {
padding-bottom: -10px;
}
</style>

64
src/views/Get.vue

@ -0,0 +1,64 @@
<template>
<DarkLayout ref="dl">
<template v-slot:header>
<router-link :to="{ name: 'Find' }">
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
All zones
</button>
</router-link>
</template>
<DomainBar
:zone="'.' + zone"
:value="domain"
@search="search()"
@input_d="handle_input($event)"
/>
<DomainResult
:domain="domain + '.' + zone"
v-if="domain !== ''"
@login="$refs.dl.login()"
/>
</DarkLayout>
</template>
<script>
import DarkLayout from "../components/DarkLayout.vue";
import DomainBar from "../components/DomainBar.vue";
import DomainResult from "../components/DomainResult.vue";
export default {
name: "Get",
props: {
domain_init: {
type: String,
},
zone: {
type: String,
},
},
components: { DomainResult, DomainBar, DarkLayout },
data() {
return {
domain: this.domain_init,
};
},
methods: {
search() {
this.$router.push({
name: "Get",
params: { domain_init: this.domain, zone: this.zone },
});
},
handle_input(val) {
this.domain = val;
},
},
watch: {
domain_init: function (val) {
this.domain = val;
},
},
};
</script>
<style scoped></style>

88
src/views/IHave.vue

@ -0,0 +1,88 @@
<template>
<WhiteLayout next="">
<template v-slot:header>
<img
src="@/assets/headers/i-know.svg"
style="max-height: 100%; max-width: 90%"
alt="TON DNS"
/>
</template>
<template v-slot:content>
<div class="cont2">
<div
class="columns-3"
style="
display: flex;
columns: 3;
justify-content: center;
align-items: center;
"
>
<div></div>
<div id="great">
<p>
Very good!<br />We can help you host a website there or sell
subdomains.<br />
For now, you will have to
<a href="https://t.me/ennucore">contact us on Telegram</a>. <br />
We are especially interested in partnering with different projects
and communities which are interested in distributing domains to
their users.<br />
Also, you can always
<router-link :to="{ name: 'Find' }"
>buy another domain</router-link
>.
</p>
</div>
</div>
</div>
</template>
<template v-slot:buttons>
<a href="https://t.me/ennucore">
<button class="b blue wide">
<span>Contact us</span>
</button>
</a>
<router-link :to="{ name: 'Find' }">
<button class="b blue wide">
<span>Buy a domain</span>
</button>
</router-link>
</template>
</WhiteLayout>
</template>
<script>
import WhiteLayout from "../components/WhiteLayout.vue";
export default {
name: "IHave",
components: { WhiteLayout },
};
</script>
<style scoped>
#great {
font-size: 3rem;
}
/* narrow buttons on mobile */
@media only screen and (max-width: 800px) {
.b.blue.wide {
max-width: 35vw;
min-width: revert;
}
#great {
font-size: 1.5rem;
}
}
.cont2 {
/* vertical centering */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
</style>

78
src/views/IKnow.vue

@ -0,0 +1,78 @@
<template>
<WhiteLayout next="">
<template v-slot:header>
<img
src="@/assets/headers/i-know.svg"
style="max-height: 100%; max-width: 90%"
alt="TON DNS"
/>
</template>
<template v-slot:content>
<div class="cont2">
<div
class="columns-3"
style="
display: flex;
columns: 3;
justify-content: center;
align-items: center;
"
>
<div></div>
<div id="great">
<p>That's great!<br />Maybe, you even have a domain already?</p>
</div>
</div>
</div>
</template>
<template v-slot:buttons>
<router-link :to="{ name: 'IHave' }">
<button class="b blue wide">
<img src="@/assets/icons/ton_top.svg" alt="Next" />
<span>Yes</span>
</button>
</router-link>
<router-link :to="{ name: 'tondns' }">
<button class="b blue wide">
<img src="@/assets/icons/ton_bottom.svg" alt="Next" />
<span>No</span>
</button>
</router-link>
</template>
</WhiteLayout>
</template>
<script>
import WhiteLayout from "../components/WhiteLayout.vue";
export default {
name: "IKnow",
components: { WhiteLayout },
};
</script>
<style scoped>
#great {
font-size: 4rem;
}
/* narrow buttons on mobile */
@media only screen and (max-width: 800px) {
.b.blue.wide {
max-width: 35vw;
min-width: revert;
}
#great {
font-size: 2rem;
}
}
.cont2 {
/* vertical centering */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
</style>

57
src/views/Landing.vue

@ -0,0 +1,57 @@
<script setup lang="ts"></script>
<template>
<main>
<!-- @/assets/logo_single.png centered horizontally and slightly above center vertically -->
<img class="logo" src="@/assets/logo_landing.png" alt="Agorata" />
<p style="font-size: 1.8rem; padding: 2rem">
Helping you <b style="font-weight: bold">be</b> the new internet
</p>
<div id="do-you-know">
<p>Do you know what TON Web is?</p>
<br />
<router-link to="/i-know">
<button class="b white wide">Yes</button>
</router-link>
<br />
<router-link to="/tonweb">
<button class="b white wide">No</button>
</router-link>
</div>
</main>
</template>
<style scoped lang="scss">
// Main has a radial gradient background with #1f2b5c at the center and #3e3e4b at the edges.
main {
background: radial-gradient(ellipse at center, #1f2b5c 0%, #3e3e4b 100%);
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-family: "Raleway", serif;
font-size: 1.5rem;
text-align: center;
}
.logo {
display: block;
margin: 0 auto 2rem;
width: 25%;
min-width: 13rem;
}
#do-you-know {
margin-top: 5rem;
// Center elements horizontally, with each element at its line
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
}
</style>

139
src/views/MintCollection.vue

@ -0,0 +1,139 @@
<template>
<DarkLayout>
<form class="form">
<div class="input-wrapper">
<label for="collection-name">Collection name</label>
<input v-model="name" class="input" id="collection-name" type="text" />
</div>
<div class="input-wrapper">
<label for="first-domen-price">Domen price with 3 symbols</label>
<input
v-model="firstPrice"
class="input"
id="first-domen-price"
type="number"
/>
</div>
<div class="input-wrapper">
<label for="second-domen-price">Domen price with 8 symbols</label>
<input
v-model="secondPrice"
class="input"
id="second-domen-price"
type="number"
/>
</div>
<p class="info" v-show="firstPrice && secondPrice && name">
By pressing the Mint button, you create an NFT collection with the name
"{{ name }}" and the price: {{ firstPrice }} ({{
secondPrice
}}).<br />Any users also will be able to buy domains from this
collection.
</p>
<button @click="mint()" class="button" type="button">Mint</button>
</form>
<template v-slot:header>
<router-link to="/find">
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
Back
</button>
</router-link>
</template>
</DarkLayout>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import DarkLayout from "../components/DarkLayout.vue";
import { useStore } from "vuex";
import { call_api } from "@/api";
const store = useStore();
const mint = async () => {
const message = await call_api(`set-record/wallet/${store.getters.address}`);
const d = new Date();
const validness = parseInt((d.getTime() / 1000).toFixed(0)) + 360000;
const transaction = {
validUntil: validness,
messages: [
{
amount: (0.05 * 1000000000).toString(),
address: store.getters.address,
payload: message,
},
],
};
const result = await store.state.connector.sendTransaction(transaction);
console.log(result);
};
const name = ref("");
const firstPrice = ref(0);
const secondPrice = ref(0);
</script>
<style>
.input-wrapper {
display: flex;
flex-direction: column;
align-items: start;
}
.input {
display: block;
width: 100%;
min-height: 40px;
border: 0;
border-radius: 10px;
padding: 12px 24px;
background-color: #4e5a88;
color: white;
font-size: 24px;
font-family: "Raleway", serif;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.form {
display: grid;
gap: 24px;
min-width: 600px;
}
.button {
border: 0;
border-radius: 0.5rem;
height: 3rem;
background-color: #e36464;
display: flex;
justify-content: center;
align-items: center;
place-items: center;
justify-self: end;
min-width: 150px;
color: #363e5e;
font-size: 1.3rem;
font-weight: 500;
padding: 0 1rem;
cursor: pointer;
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.info {
width: 600px;
font-size: 16px;
text-align: right;
margin-top: 40px;
}
</style>

18
src/views/MyDomains.vue

@ -0,0 +1,18 @@
<template>
<DarkLayout>
<DomainTable />
<template v-slot:header>
<router-link to="/find">
<button class="b darkish back">
<img src="@/assets/icons/ton_left.svg" alt="TON" />
Back
</button>
</router-link>
</template>
</DarkLayout>
</template>
<script setup>
import DarkLayout from "../components/DarkLayout.vue";
import DomainTable from "@/components/DomainTable.vue";
</script>

53
src/views/TonDns.vue

@ -0,0 +1,53 @@
<template>
<WhiteLayout>
<template v-slot:header>
<img src="@/assets/tondns.svg" style="height: 100%" alt="TON DNS" />
</template>
<template v-slot:content>
<div style="justify-content: center; align-items: center">
<div>
<p>
TON domains .ton and .t.me are your names in the TON world.<br />
They can be used to host a website or to transfer money.<br />
You can host a TON website for your personal brand.
</p>
</div>
<div class="columns-3 images">
<img class="img" src="@/assets/images/usage.png" alt="Usage" />
<img class="img" src="@/assets/images/my_name_is.png" alt="lame" />
<img class="img" src="@/assets/images/anton_ton.png" alt="cool" />
</div>
</div>
</template>
<template v-slot:buttons>
<router-link :to="{ name: 'Find' }">
<button class="b blue wide">
<span>Omg, can I get one?</span>
<img src="@/assets/icons/ton_right.svg" alt="Next" />
</button>
</router-link>
</template>
</WhiteLayout>
</template>
<script>
import WhiteLayout from "../components/WhiteLayout.vue";
export default {
name: "TonDns",
components: { WhiteLayout },
};
</script>
<style scoped>
.img {
width: 25%;
margin: 0 1rem;
}
/* Fix width and not height on mobile */
@media only screen and (max-width: 800px) {
.img {
width: 75vw;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save