App.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <script setup>
  2. import { ref, watch, onMounted, computed, onBeforeMount } from 'vue'
  3. import { loadTicker } from './api'
  4. const API_KEY =
  5. '49c2392a697fa3880cecd2a3b029b7e993aa3fd7906502e7634d797e92e5164d'
  6. const ticker = ref('')
  7. const tickers = ref([])
  8. const sel = ref(null)
  9. const graph = ref([])
  10. const err = ref('')
  11. const valutes = ref([])
  12. const page = ref(1)
  13. const filter = ref('')
  14. const windowData = Object.fromEntries(
  15. new URL(window.location).searchParams.entries(),
  16. )
  17. page.value = windowData.page ?? 1
  18. filter.value = windowData.filter ?? ''
  19. function subscribeToUpdates(tickerName) {
  20. setInterval(async () => {
  21. const f = await fetch(
  22. `https://min-api.cryptocompare.com/data/price?fsym=${tickerName}&tsyms=USD&api_key=${API_KEY}`,
  23. )
  24. const data = await f.json()
  25. // тут я добавил проверку data.USD - некоторые валюты возвращают ошибку
  26. if (typeof data.USD != 'undefined') {
  27. tickers.value.find(t => t.name === tickerName).price =
  28. data.USD > 1 ? data.USD.toFixed(2) : data.USD.toPrecision(2)
  29. if (sel.value?.name === tickerName) {
  30. graph.value.push(data.USD)
  31. }
  32. }
  33. }, 5000)
  34. }
  35. function add(nameToAdd) {
  36. const tickerName = nameToAdd.toUpperCase()
  37. const isInvalid = valutes.value.findIndex(v => v === tickerName) == -1
  38. console.log('isInvalid:', isInvalid)
  39. const newTicker = {
  40. name: tickerName,
  41. price: '-',
  42. isInvalid,
  43. }
  44. // Проверяем, существует ли уже тикер с таким названием
  45. if (tickers.value.findIndex(n => n.name === newTicker.name) >= 0) {
  46. // Если тикер уже существует, помечаем его как некорректный
  47. err.value = 1
  48. }
  49. tickers.value = [...tickers.value, newTicker] // Добавляем некорректный тикер в список
  50. filter.value = '' // Сбрасываем фильтр
  51. }
  52. onBeforeMount(() => {
  53. const tickersData = localStorage.getItem('cryptonomicon-list') ?? '[]'
  54. if (tickersData) {
  55. tickers.value = JSON.parse(tickersData)
  56. }
  57. setInterval(() => {
  58. updateTickers()
  59. }, 5000)
  60. })
  61. watch(tickers, () => {
  62. localStorage.setItem('cryptonomicon-list', JSON.stringify(tickers.value))
  63. })
  64. onMounted(async () => {
  65. const f = await fetch(
  66. `https://min-api.cryptocompare.com/data/all/coinlist?summary=true&api_key=${API_KEY}`,
  67. )
  68. const data = await f.json()
  69. if (data.Response == 'Success') {
  70. valutes.value = Object.keys(data.Data)
  71. // console.log(valutes.value)
  72. }
  73. })
  74. const shortValutes = computed(() => {
  75. if (ticker.value !== '')
  76. return valutes.value
  77. .filter(v => v.includes(ticker.value.toUpperCase()))
  78. .slice(0, 4)
  79. })
  80. function handleDelete(tickerToRemove) {
  81. tickers.value = tickers.value.filter(t => t != tickerToRemove)
  82. if (sel.value == tickerToRemove) {
  83. sel.value = null
  84. }
  85. }
  86. async function updateTickers() {
  87. if (!tickers.value.length) return
  88. const data = await loadTicker(tickers.value.map(t => t.name))
  89. tickers.value.forEach(t => {
  90. t.price = data[t.name] ?? '-'
  91. })
  92. if (sel.value?.name) {
  93. graph.value.push(data[sel.value.name])
  94. }
  95. }
  96. const pageStateOptions = computed(() => {
  97. return {
  98. filter: filter.value,
  99. page: page.value,
  100. }
  101. })
  102. function formatPrice(price) {
  103. if (typeof price == 'number') {
  104. return price > 1 ? price.toFixed(2) : price.toPrecision(2)
  105. }
  106. return price
  107. }
  108. watch(filter, () => {
  109. page.value = 1
  110. window.history.pushState(
  111. null,
  112. document.title,
  113. `${window.location.pathname}?filter=${filter.value} `,
  114. )
  115. })
  116. watch(pageStateOptions, value => {
  117. window.history.pushState(
  118. null,
  119. document.title,
  120. `${window.location.pathname}?filter=${value.filter}&page=${value.page}`,
  121. )
  122. })
  123. const startIndex = computed(() => {
  124. return (page.value - 1) * 6
  125. })
  126. const endIndex = computed(() => {
  127. return page.value * 6
  128. })
  129. const filteredTickers = computed(() => {
  130. return tickers.value.filter(t => t.name.includes(filter.value.toUpperCase()))
  131. })
  132. const paginatedTickers = computed(() => {
  133. return filteredTickers.value.slice(startIndex.value, endIndex.value)
  134. })
  135. watch(paginatedTickers, value => {
  136. if (value.length === 0 && page.value > 1) page.value--
  137. })
  138. function select(ticker) {
  139. sel.value = ticker
  140. }
  141. watch(sel, () => {
  142. graph.value = []
  143. })
  144. const hasNextPage = computed(() => {
  145. return filteredTickers.value.length > endIndex.value
  146. })
  147. const normalizedGraph = computed(() => {
  148. const maxValue = Math.max(...graph.value)
  149. const minValue = Math.min(...graph.value)
  150. if (maxValue == minValue) {
  151. return graph.value.map(() => 50)
  152. }
  153. return graph.value.map(
  154. price => 5 + ((price - minValue) * 95) / (maxValue - minValue),
  155. )
  156. })
  157. </script>
  158. <template>
  159. <div class="container mx-auto flex flex-col items-center bg-gray-100 p-4">
  160. <div class="container">
  161. <section>
  162. <div>
  163. Фильтр: <input v-model="filter" @input="page = '1'" />
  164. <button
  165. class="my-4 mx-2 inline-flex items-center py-2 px-4 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-full text-white bg-gray-600 hover:bg-gray-700 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
  166. @click="page = page - 1"
  167. v-if="page > 1"
  168. >
  169. Назад
  170. </button>
  171. <button
  172. class="my-4 mx-2 inline-flex items-center py-2 px-4 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-full text-white bg-gray-600 hover:bg-gray-700 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
  173. @click="page = page + 1"
  174. v-if="hasNextPage"
  175. >
  176. Вперед
  177. </button>
  178. </div>
  179. <div class="flex">
  180. <div class="max-w-xs">
  181. <label for="wallet" class="block text-sm font-medium text-gray-700"
  182. >Тикер
  183. </label>
  184. <div class="mt-1 relative rounded-md shadow-md">
  185. <input
  186. v-model="ticker"
  187. type="text"
  188. name="wallet"
  189. id="wallet"
  190. class="block w-full pr-10 border-gray-300 text-gray-900 focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm rounded-md"
  191. placeholder="Например DOGE"
  192. />
  193. </div>
  194. <div
  195. class="flex bg-white shadow-md p-1 rounded-md shadow-md flex-wrap"
  196. >
  197. <span
  198. v-for="(p, index) in shortValutes"
  199. @click="add(p)"
  200. class="inline-flex items-center px-2 m-1 rounded-md text-xs font-medium bg-gray-300 text-gray-800 cursor-pointer"
  201. >
  202. {{ p }}
  203. </span>
  204. </div>
  205. <div v-if="err === 1" class="text-sm text-red-600">
  206. Такой тикер уже добавлен
  207. </div>
  208. </div>
  209. </div>
  210. <button
  211. @click="add(ticker)"
  212. v-on:keydown.enter="add"
  213. type="button"
  214. class="my-4 inline-flex items-center py-2 px-4 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-full text-white bg-gray-600 hover:bg-gray-700 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
  215. >
  216. <!-- Heroicon name: solid/mail -->
  217. <svg
  218. class="-ml-0.5 mr-2 h-6 w-6"
  219. xmlns="http://www.w3.org/2000/svg"
  220. width="30"
  221. height="30"
  222. viewBox="0 0 24 24"
  223. fill="#ffffff"
  224. >
  225. <path
  226. d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
  227. ></path>
  228. </svg>
  229. Добавить
  230. </button>
  231. </section>
  232. <div v-if="tickers.length > 0">
  233. <hr class="w-full border-t border-gray-600 my-4" />
  234. <dl class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
  235. <div
  236. v-for="(t, index) in paginatedTickers"
  237. :key="index"
  238. @click="select(t)"
  239. :class="{
  240. 'border-4': sel == t,
  241. 'bg-red-100': t.isInvalid,
  242. 'bg-white': !t.isInvalid,
  243. }"
  244. class="overflow-hidden shadow rounded-lg border-purple-800 border-solid cursor-pointer"
  245. >
  246. <div class="px-4 py-5 sm:p-6 text-center">
  247. <dt class="text-sm font-medium text-gray-500 truncate">
  248. {{ t.name }} - USD
  249. </dt>
  250. <dd class="mt-1 text-3xl font-semibold text-gray-900">
  251. {{ t.price }}
  252. </dd>
  253. </div>
  254. <div class="w-full border-t border-gray-200"></div>
  255. <button
  256. @click.stop="handleDelete(t)"
  257. class="flex items-center justify-center font-medium w-full bg-gray-100 px-4 py-4 sm:px-6 text-md text-gray-500 hover:text-gray-600 hover:bg-gray-200 hover:opacity-20 transition-all focus:outline-none"
  258. >
  259. <svg
  260. class="h-5 w-5"
  261. xmlns="http://www.w3.org/2000/svg"
  262. viewBox="0 0 20 20"
  263. fill="#718096"
  264. aria-hidden="true"
  265. >
  266. <path
  267. fill-rule="evenodd"
  268. d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
  269. clip-rule="evenodd"
  270. ></path></svg
  271. >Удалить
  272. </button>
  273. </div>
  274. </dl>
  275. <hr class="w-full border-t border-gray-600 my-4" />
  276. </div>
  277. <section v-if="sel" class="relative">
  278. <h3 class="text-lg leading-6 font-medium text-gray-900 my-8">
  279. {{ sel.name }} - USD
  280. </h3>
  281. <div class="flex items-end border-gray-600 border-b border-l h-64">
  282. <div
  283. v-for="(bar, idx) in normalizedGraph"
  284. :key="idx"
  285. :style="{ height: `${bar}%` }"
  286. class="bg-purple-800 border w-10 h-16"
  287. ></div>
  288. </div>
  289. <button
  290. @click="sel = null"
  291. type="button"
  292. class="absolute top-0 right-0"
  293. >
  294. <svg
  295. xmlns="http://www.w3.org/2000/svg"
  296. xmlns:xlink="http://www.w3.org/1999/xlink"
  297. xmlns:svgjs="http://svgjs.com/svgjs"
  298. version="1.1"
  299. width="30"
  300. height="30"
  301. x="0"
  302. y="0"
  303. viewBox="0 0 511.76 511.76"
  304. style="enable-background: new 0 0 512 512"
  305. xml:space="preserve"
  306. >
  307. <g>
  308. <path
  309. d="M436.896,74.869c-99.84-99.819-262.208-99.819-362.048,0c-99.797,99.819-99.797,262.229,0,362.048 c49.92,49.899,115.477,74.837,181.035,74.837s131.093-24.939,181.013-74.837C536.715,337.099,536.715,174.688,436.896,74.869z M361.461,331.317c8.341,8.341,8.341,21.824,0,30.165c-4.16,4.16-9.621,6.251-15.083,6.251c-5.461,0-10.923-2.091-15.083-6.251 l-75.413-75.435l-75.392,75.413c-4.181,4.16-9.643,6.251-15.083,6.251c-5.461,0-10.923-2.091-15.083-6.251 c-8.341-8.341-8.341-21.845,0-30.165l75.392-75.413l-75.413-75.413c-8.341-8.341-8.341-21.845,0-30.165 c8.32-8.341,21.824-8.341,30.165,0l75.413,75.413l75.413-75.413c8.341-8.341,21.824-8.341,30.165,0 c8.341,8.32,8.341,21.824,0,30.165l-75.413,75.413L361.461,331.317z"
  310. fill="#718096"
  311. data-original="#000000"
  312. ></path>
  313. </g>
  314. </svg>
  315. </button>
  316. </section>
  317. </div>
  318. </div>
  319. </template>