Browse Source

init commit

main
matthew 2 years ago
commit
213b9a4ebf
  1. 43
      docker-compose.yml
  2. 8
      proxy/Dockerfile
  3. BIN
      proxy/generate-random-id
  4. 890
      proxy/global.config.json
  5. BIN
      proxy/rldp-http-proxy
  6. 3
      proxy/run.sh
  7. 11
      searching-front/.editorconfig
  8. 3
      searching-front/.env
  9. 5
      searching-front/.env.test
  10. 20
      searching-front/.eslintrc.js
  11. 56
      searching-front/.gitignore
  12. 5
      searching-front/.husky/pre-commit
  13. 6
      searching-front/.husky/pre-push
  14. 11
      searching-front/.npmrc
  15. 10
      searching-front/.prettierignore
  16. 12
      searching-front/.vscode/extensions.json
  17. 28
      searching-front/.vscode/launch.json
  18. 14
      searching-front/.vscode/settings.json
  19. 173
      searching-front/README.md
  20. 30
      searching-front/app/auth/components/Button/Button.tsx
  21. 0
      searching-front/app/auth/components/Button/index.ts
  22. 29
      searching-front/app/auth/components/Button/styles.module.css
  23. 59
      searching-front/app/auth/components/LoginForm.tsx
  24. 42
      searching-front/app/auth/components/SignupForm.tsx
  25. 32
      searching-front/app/auth/mutations/changePassword.ts
  26. 59
      searching-front/app/auth/mutations/forgotPassword.test.ts
  27. 42
      searching-front/app/auth/mutations/forgotPassword.ts
  28. 32
      searching-front/app/auth/mutations/login.ts
  29. 5
      searching-front/app/auth/mutations/logout.ts
  30. 82
      searching-front/app/auth/mutations/resetPassword.test.ts
  31. 48
      searching-front/app/auth/mutations/resetPassword.ts
  32. 16
      searching-front/app/auth/mutations/signup.ts
  33. 42
      searching-front/app/auth/validations.ts
  34. 11
      searching-front/app/blitz-client.ts
  35. 16
      searching-front/app/blitz-server.ts
  36. 83
      searching-front/app/core/components/Form.tsx
  37. 27
      searching-front/app/core/components/Header/Header.tsx
  38. 1
      searching-front/app/core/components/Header/index.ts
  39. 35
      searching-front/app/core/components/Header/styles.module.css
  40. 59
      searching-front/app/core/components/LabeledTextField.tsx
  41. 12
      searching-front/app/core/components/Modal/Modal.tsx
  42. 42
      searching-front/app/core/components/Pagination/Pagination.tsx
  43. 1
      searching-front/app/core/components/Pagination/index.ts
  44. 64
      searching-front/app/core/components/Pagination/styles.module.css
  45. 191
      searching-front/app/core/components/SearchForm/SearchForm.tsx
  46. 1
      searching-front/app/core/components/SearchForm/index.ts
  47. 73
      searching-front/app/core/components/SearchForm/styles.module.css
  48. 32
      searching-front/app/core/components/ThemeSwitcher/ThemeSwitcher.tsx
  49. 311
      searching-front/app/core/components/ThemeSwitcher/styles.module.css
  50. 29
      searching-front/app/core/components/TonBrilliant/TonBrillian.tsx
  51. 1
      searching-front/app/core/components/TonBrilliant/index.ts
  52. 39
      searching-front/app/core/components/TonBrilliant/styles.module.css
  53. 17
      searching-front/app/core/components/WebsiteCard/WebsiteCard.tsx
  54. 1
      searching-front/app/core/components/WebsiteCard/index.ts
  55. 31
      searching-front/app/core/components/WebsiteCard/styles.module.css
  56. 0
      searching-front/app/core/constants.ts
  57. 20
      searching-front/app/core/contextProviders/serverSidePropsProvider.ts
  58. 32
      searching-front/app/core/darkTheme.css
  59. 73
      searching-front/app/core/global.css
  60. 10
      searching-front/app/core/helpers/common.ts
  61. 10
      searching-front/app/core/hooks/useCurrentTheme.ts
  62. 7
      searching-front/app/core/hooks/useCurrentUser.ts
  63. 16
      searching-front/app/core/icons/TonLogo.tsx
  64. 36
      searching-front/app/core/layouts/Layout/index.tsx
  65. 23
      searching-front/app/core/layouts/Layout/styles.module.css
  66. 34
      searching-front/app/core/lightTheme.css
  67. 14
      searching-front/app/core/pages/Main/Main.tsx
  68. 1
      searching-front/app/core/pages/Main/index.ts
  69. 8
      searching-front/app/core/pages/Main/styles.module.css
  70. 51
      searching-front/app/core/pages/Search/Search.tsx
  71. 1
      searching-front/app/core/pages/Search/index.ts
  72. 9
      searching-front/app/core/pages/Search/styles.module.css
  73. 70
      searching-front/app/core/variables.css
  74. 8
      searching-front/app/i18n/en.ts
  75. 41
      searching-front/app/i18n/index.ts
  76. 8
      searching-front/app/i18n/ru.ts
  77. 24
      searching-front/app/search-requests/mutations/upsertSearchRequest.ts
  78. 24
      searching-front/app/search-requests/queries/getSearchRequests.ts
  79. 37
      searching-front/app/search-requests/queries/getSearchResult.ts
  80. 15
      searching-front/app/users/queries/getCurrentUser.ts
  81. 9
      searching-front/db/index.ts
  82. 98
      searching-front/db/migrations/20220930173413_a/migration.sql
  83. 2
      searching-front/db/migrations/20220930173504_a/migration.sql
  84. 13
      searching-front/db/migrations/20220930184658_asd/migration.sql
  85. 22
      searching-front/db/migrations/20220930185345_a/migration.sql
  86. 3
      searching-front/db/migrations/migration_lock.toml
  87. 97
      searching-front/db/schema.prisma
  88. 15
      searching-front/db/seeds.ts
  89. 0
      searching-front/integrations/.keep
  90. 11
      searching-front/jest.config.js
  91. 0
      searching-front/mailers/.keep
  92. 45
      searching-front/mailers/forgotPasswordMailer.ts
  93. 5
      searching-front/next-env.d.ts
  94. 9
      searching-front/next.config.js
  95. 32879
      searching-front/package-lock.json
  96. 89
      searching-front/package.json
  97. 20
      searching-front/pages/404.tsx
  98. 41
      searching-front/pages/_app.tsx
  99. 23
      searching-front/pages/_document.tsx
  100. 4
      searching-front/pages/api/rpc/[[...blitz]].ts
  101. Some files were not shown because too many files have changed in this diff Show More

43
docker-compose.yml

@ -0,0 +1,43 @@
version: "3.7"
services:
db:
image: postgres:latest
volumes:
- dbdata:/var/lib/postgresql/data
env_file: ./searching-front/.env.local #Here we are using the already existing .env.local file
ports:
- "5432:5432"
elasticsearch:
container_name: es-container
image: elasticsearch:8.4.0
environment:
- xpack.security.enabled=false
- "discovery.type=single-node"
networks:
- es-net
ports:
- 9200:9200
kibana:
container_name: kb-container
image: kibana:7.17.6
environment:
- ELASTICSEARCH_HOSTS=http://es-container:9200
networks:
- es-net
depends_on:
- elasticsearch
ports:
- 5601:5601
proxy:
platform: linux/x86_64
build: ./proxy
volumes:
- ./proxy:/app
networks:
es-net:
driver: bridge
volumes:
dbdata:

8
proxy/Dockerfile

@ -0,0 +1,8 @@
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libatomic1
USER root
WORKDIR /app
COPY . /app
RUN ["chmod", "+x", "./run.sh"]
CMD ./run.sh

BIN
proxy/generate-random-id

Binary file not shown.

890
proxy/global.config.json

@ -0,0 +1,890 @@
{
"@type": "config.global",
"dht": {
"@type": "dht.config.global",
"k": 6,
"a": 3,
"static_nodes": {
"@type": "dht.nodes",
"nodes": [
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "C1uy64rfGxp10SPSqbsxWhbumy5SM0YbvljCudwpZeI="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1307380867,
"port": 15888
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "s+tnHMTzPYG8abau+1dUs8tBJ+CDt+jIPmGfaVd7nmfb1gt6lL10G2IwkNeWhkxjZcAHRc0azWFVxp+IjIOOBQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "bn8klhFZgE2sfIDfvVI6m6+oVNi1nBRlnHoxKtR9WBU="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1307380860,
"port": 15888
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "fQ5zAa6ot4pfFWzvuJOR8ijM5ELWndSDsRhFKstW1tqVSNfwAdOC7tDC8mc4vgTJ6fSYSWmhnXGK/+T5f6sDCw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "KlNrfVSyO6oISNi4Bx8J2klAN4RnKmEPQpfr1bghGSk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1307380856,
"port": 15888
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "0/1trU+HDc+Co/q8gw5lPrSJH9YCOXxVh0caR2CGqXE5820DguuSmVnnLQ9S2+RmxHv0biYZuH8FiJv2wPwyDA=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "HU0bQDDmXnUENi2qQgGUQSopWz+s0dFA1l6NgB6HdQ0="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1185526389,
"port": 26907
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "RrZILQv72PtJ/oADGh+txXgo6qfUL9RFLU+YjMXsMZTAo2lCWYwNoeFOEZrS5MKfLmkL6O0MmOR/EEAFrr3mAw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "xHfihhu4rFeKUNjxH2aHCJIG1s9PTaypqjocrm82U24="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1560268637,
"port": 29503
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "m+ah3Bn9AMaV13QaXrfT/c/z1fY6DzDMQkCEIgByXygnskwYwTfWxa8Z7DXy80UX2OYpSL8GwTJ4HpQsdcxMCA=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "4R0C/zU56k+x2HGMsLWjX2rP/SpoTPIHSSAmidGlsb8="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1952265919,
"port": 14395
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "0uwWyCFn2KjPnnlbSFYXLZdwIakaSgI9WyRo87J3iCGwb5TvJSztgA224A9kNAXeutOrXMIPYv1b8Zt8ImsrCg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1402455171,
"port": 14432
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "6+oVk6HDtIFbwYi9khCc8B+fTFceBUo1PWZDVTkb4l84tscvr5QpzAkdK7sS5xGzxM7V7YYQ6gUQPrsP9xcLAw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "DA0H568bb+LoO2LGY80PgPee59jTPCqqSJJzt1SH+KE="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1402397332,
"port": 14583
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "cL79gDTrixhaM9AlkCdZWccCts7ieQYQBmPxb/R7d7zHw3bEHL8Le96CFJoB1KHu8C85iDpFK8qlrGl1Yt/ZDg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "fZnkoIAxrTd4xeBgVpZFRm5SvVvSx7eN3Vbe8c83YMk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091897261,
"port": 15813
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "cmaMrV/9wuaHOOyXYjoxBnckJktJqrQZ2i+YaY3ehIyiL3LkW81OQ91vm8zzsx1kwwadGZNzgq4hI4PCB/U5Dw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "zDBLsKjns4bBqQokzY0wOzC2vwbOeiE1J7aOjfCp5mg="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1573440928,
"port": 12821
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "qORMhem9RyG7wnNYF822YL3EXwEoTO82h2TarFbjd0jikMIGizAdir1JyxSfyKkhHdFKGcLMeoPb2dfMIvQwAA=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "CU9ytJok8WBnpl29T740gfC/h69kgvQJp7FJMq/N60g="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 391653587,
"port": 15895
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "DKyGF2nPRxmerpIHxE5FN1Lod3zvJu728NP0iYc1hpNyPvl5epu+7amjimLy1VdzNqFzTJAoJ/gqPPMkXS/kDw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "d1lL6xZO8UWMzkWD++8Yr3hf6585X6qoOZZTeLtGl4o="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1537553981,
"port": 18513
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "sDxyCuvZmi9fJvQl4DEBKZOlt532xflnVD1dvC2ia2Na5MN8dT6x4HizEpS4pUqky8LzR/A/4BCqIQXhD45vDg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "SP2Bjle9u+GoZhlEhm3mIPiND3Yh+Nr7QsgV6bGPw/I="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1591529174,
"port": 11369
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "qbbeE53SOyUvOWD2hIZmTyZGY4k2XHxgldqBIakBma33oC5sWDD/+cApuFLMbi2Gnd6fQtQ3LaZtfQzJrCiYAw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "76SsMv8NrqzkCFEVumZEx2phYYSUZvSH3UlUTuPdZYk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1906451518,
"port": 16937
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "YgrIN/KH3XVPyk09wgZbFaXLZ/BeGjvfO6ohxN4M8Tp/CsYN0G3tNLSjJGeuH22bIKMFRoEpp3v7Sz54Q/FTCg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "IHrCooudVGonmudcmTZYk+Pfdsxz2NGws33bHtXnOv8="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1882838653,
"port": 41623
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "pm3PQn0ZYQH7SkmK/8q4p1igFKxJSQLCmVcFtyaRxEV9ecjvB5PiW3o3wfrTRPk0v0mnFmH8UuYtLEztj+LcBw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "eDPKd9v6aHhutnHU3z2ykV4eUA3LdFI+oSBg6z8JyS0="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1481864647,
"port": 22261
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "InY5jkkQ6fuJtutkmlPLYEhqQ0F4DyEhqoPB4KMBIJc8hhZFzm4jNAxSfy3VVQbACdJ/pj76qbjaktG/m1ipDQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "znOAvy1ECxyzeKishi4PdSO2edaVx78wynVyNKLBmQ8="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1068377703,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "KLH17nNKmOk3carKwbsUcVBc4JZpdAUdUOMxe8FSyqnkOw/lolnltbylJcC+lvPpIV5ySI/Qx8UZdNRV/4HzCA=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "Qjhv9rmeqXm0a+nYYhCJG1AH7C2TM6DAmyIM3FgO0Eo="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1057912003,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "2Gw5eIsZR+SdbWCU139DCuBI8Rv8T9iUioxDkgV6/IjcCHf6hNz8WCyUsKd5l5P1NBs/kdaxUBIybINDpYXoCw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "2YsTRIu3aRYzZe8eoR8PK2N2ydHJyKllwKcLPk676d4="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1057911744,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "9/TJsaj0wELvRKXVIrBdyZWjgLKhfSvl7v0Oqq/9p9MsU/t9iRuGcpAzHqQF4bQAWrN8j9ARwMumRata7dH8Bg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "SHrXmMEEUBGa51TWZwHSA+2RF4Vyavw51jgtnAz1ypU="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -1057911148,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "R4ku8+tvjKSLIGe18zWHBHDv1maQHD5tGbAUOgbldGpBvfqH+/b76XkJjJzDsjnCO/bpxwUZfcI1sM1h6vFJCQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "G+Lr6UtSWUcyYHTUutwbxrIG9GGZan3h96j8qQPLIXQ="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -960017601,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "fWU9hSNLvmaSCQOBW9M4Lja5pIWcqOzU1g9vtSywdgtASj9oQpwAslvr2sjNh9E2Np1c26NW8Sc5gUKf8YY7BA=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "/tp8WsXfk/XpzOyaaxuOlvbOhDoH7/L81eWi0QMn0gg="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 84478511,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "2kA9P0LBI7H8gmmGsnZ2bQPZP3rZDFugrc5zQWlFrPIMLvwH7/J69HIGCVYgcaEsf0HMnIJeUMl5n4qFp0quBQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "fnYl5kAHcbhK65FbYxfwk83X1Sn6ZiuXWMD0F0Rh+v4="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 84478479,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "h+K+YttdhqE4LzihZTnKLFBiXyY79Rqqcx8dCbkDVXu3FD7ZrTBNV5b/bf7BQbuF0PXTc7YqH0jEmqz8aX6aBg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "HwOhm4Vh1YGqBNmUrDwJpeo8kXAPI7J3sSH38JaAyzQ="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -2018145068,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "Ianf1Wm9Y6HT9r32LFNUieKi86cSBbCckczHy+ZyBo15MpIsZxouUgfAyeW20sZm1hN5+Yx4lPwzL+Ovm6KaCw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "CXo+qxdYclubZqoqvVhoeYDdPV+VhlWcurf2OX0iPZs="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -2018145059,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "P72kraIX5pIxQBnh7It4kyK6MPuZ56ZFZKZxegtrxwx/Vpi1wQ4PsfxWf6N0HojbNMYsVZsvwHYTLxj5nhd6Dw=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "KiKtUV+kJWBd+M29zNvtRqdvUrtX4lfi5CyY+DRm+lk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091931625,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "GjarYvxTVPik8m5yI9Eq/1lW/8CuReBdhUdFUb4wJJVVc/EvHf7j47mY5ECskHjeo9MYttgF/9KQaf8KNea1Dg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "o15mg8SB9CY2m971NvU+aCzAEnZFg3iAnIsqBMmqnj0="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091931590,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "6mJPM7RZMOL5uCMRCGINjxAG7L7LHt7o89caD7Kk75ahpwAhqJ3ri9zL1rzJZjmyOMLkPoGcckJsG8phCRbVDQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "VCu471G41Hj8onyyeJdq8t6AZu3SR7BoGuCLs8SppBk="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091931623,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "7cOhypsjGb4xczR20M6eg7ly8sdvzdodkKVXzvr00FsXHcguz6bP0zm/dwhiQgsJgSosYypCk/LvKQrMy+C3AQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "0uEnHB6Rg4sVjiepDgHoZ3CuKjCRjU3ul4IGmmZZoig="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091931589,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "ju40qeS5mgbJDMLUxL7qSquSdqgo3Uib4Z/Va/bpIWJJA0W3VQStJMBbV/pQySi6MoM794Du3o8Gl1bjdpwDAg=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "MJr8xja0xpu9DoisFXBrkNHNx1XozR7HHw9fJdSyEdo="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -2018147130,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "XcR5JaWcf4QMdI8urLSc1zwv5+9nCuItSE1EDa0dSwYF15R/BtJoKU5YHA4/T8SiO18aVPQk2SL1pbhevuMrAQ=="
},
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "Fhldu4zlnb20/TUj9TXElZkiEmbndIiE/DXrbGKu+0c="
},
"addr_list": {
"@type": "adnl.addressList",
"addrs": [
{
"@type": "adnl.address.udp",
"ip": -2018147075,
"port": 6302
}
],
"version": 0,
"reinit_date": 0,
"priority": 0,
"expire_at": 0
},
"version": -1,
"signature": "nUGB77UAkd2+ZAL5PgInb3TvtuLLXJEJ2icjAUKLv4qIGB3c/O9k/v0NKwSzhsMP0ljeTGbcIoMDw24qf3goCg=="
}
]
}
},
"liteservers": [
{
"ip": 84478511,
"port": 19949,
"id": {
"@type": "pub.ed25519",
"key": "n4VDnSCUuSpjnCyUk9e3QOOd6o0ItSWYbTnW3Wnn8wk="
}
},
{
"ip": 84478479,
"port": 48014,
"id": {
"@type": "pub.ed25519",
"key": "3XO67K/qi+gu3T9v8G2hx1yNmWZhccL3O7SoosFo8G0="
}
},
{
"ip": -2018135749,
"port": 53312,
"id": {
"@type": "pub.ed25519",
"key": "aF91CuUHuuOv9rm2W5+O/4h38M3sRm40DtSdRxQhmtQ="
}
},
{
"ip": -2018145068,
"port": 13206,
"id": {
"@type": "pub.ed25519",
"key": "K0t3+IWLOXHYMvMcrGZDPs+pn58a17LFbnXoQkKc2xw="
}
},
{
"ip": -2018145059,
"port": 46995,
"id": {
"@type": "pub.ed25519",
"key": "wQE0MVhXNWUXpWiW5Bk8cAirIh5NNG3cZM1/fSVKIts="
}
},
{
"ip": 1091931625,
"port": 30131,
"id": {
"@type": "pub.ed25519",
"key": "wrQaeIFispPfHndEBc0s0fx7GSp8UFFvebnytQQfc6A="
}
},
{
"ip": 1091931590,
"port": 47160,
"id": {
"@type": "pub.ed25519",
"key": "vOe1Xqt/1AQ2Z56Pr+1Rnw+f0NmAA7rNCZFIHeChB7o="
}
},
{
"ip": 1091931623,
"port": 17728,
"id": {
"@type": "pub.ed25519",
"key": "BYSVpL7aPk0kU5CtlsIae/8mf2B/NrBi7DKmepcjX6Q="
}
},
{
"ip": 1091931589,
"port": 13570,
"id": {
"@type": "pub.ed25519",
"key": "iVQH71cymoNgnrhOT35tl/Y7k86X5iVuu5Vf68KmifQ="
}
},
{
"ip": -1539021362,
"port": 52995,
"id": {
"@type": "pub.ed25519",
"key": "QnGFe9kihW+TKacEvvxFWqVXeRxCB6ChjjhNTrL7+/k="
}
},
{
"ip": -1539021936,
"port": 20334,
"id": {
"@type": "pub.ed25519",
"key": "gyLh12v4hBRtyBygvvbbO2HqEtgl+ojpeRJKt4gkMq0="
}
},
{
"ip": -1136338705,
"port": 19925,
"id": {
"@type": "pub.ed25519",
"key": "ucho5bEkufbKN1JR1BGHpkObq602whJn3Q3UwhtgSo4="
}
},
{
"ip": 868465979,
"port": 19434,
"id": {
"@type": "pub.ed25519",
"key": "J5CwYXuCZWVPgiFPW+NY2roBwDWpRRtANHSTYTRSVtI="
}
},
{
"ip": 868466060,
"port": 23067,
"id": {
"@type": "pub.ed25519",
"key": "vX8d0i31zB0prVuZK8fBkt37WnEpuEHrb7PElk4FJ1o="
}
},
{
"ip": -2018147130,
"port": 53560,
"id": {
"@type": "pub.ed25519",
"key": "NlYhh/xf4uQpE+7EzgorPHqIaqildznrpajJTRRH2HU="
}
},
{
"ip": -2018147075,
"port": 46529,
"id": {
"@type": "pub.ed25519",
"key": "jLO6yoooqUQqg4/1QXflpv2qGCoXmzZCR+bOsYJ2hxw="
}
},
{
"ip": 908566172,
"port": 51565,
"id": {
"@type": "pub.ed25519",
"key": "TDg+ILLlRugRB4Kpg3wXjPcoc+d+Eeb7kuVe16CS9z8="
}
}
],
"validator": {
"@type": "validator.config.global",
"zero_state": {
"workchain": -1,
"shard": -9223372036854775808,
"seqno": 0,
"root_hash": "F6OpKZKqvqeFp6CQmFomXNMfMj2EnaUSOXN+Mh+wVWk=",
"file_hash": "XplPz01CXAps5qeSWUtxcyBfdAo5zVb1N979KLSKD24="
},
"init_block": {
"root_hash": "iyOany4cPE2u6h/Um7OAmHDQ+Nba8Am+g/qZJ5M4P9M=",
"seqno": 18155329,
"file_hash": "Yqmli3gIUgt3KjeU4n2d1ZmcJ3R4zJBMYWhM+tZF4V8=",
"workchain": -1,
"shard": -9223372036854775808
},
"hardforks": [
{
"file_hash": "t/9VBPODF7Zdh4nsnA49dprO69nQNMqYL+zk5bCjV/8=",
"seqno": 8536841,
"root_hash": "08Kpc9XxrMKC6BF/FeNHPS3MEL1/Vi/fQU/C9ELUrkc=",
"workchain": -1,
"shard": -9223372036854775808
}
]
}
}

BIN
proxy/rldp-http-proxy

Binary file not shown.

3
proxy/run.sh

@ -0,0 +1,3 @@
#!/bin/bash
./generate-random-id -m adnlid

11
searching-front/.editorconfig

@ -0,0 +1,11 @@
# https://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

3
searching-front/.env

@ -0,0 +1,3 @@
# This env file should be checked into source control
# This is the place for default values for all environments
# Values in `.env.local` and `.env.production` will override these values

5
searching-front/.env.test

@ -0,0 +1,5 @@
# SQLite is ready to go out of the box, but you can switch to Postgres
# by first changing the provider from "sqlite" to "postgres" in the Prisma
# schema file and by second swapping the DATABASE_URL below.
DATABASE_URL="file:./db_test.sqlite"
# DATABASE_URL=postgresql://matthew@localhost:5432/searching-front_test

20
searching-front/.eslintrc.js

@ -0,0 +1,20 @@
const common = require("@blitzjs/next/eslint")
common.overrides = [
{
files: ["**/*.ts?(x)"],
plugins: ["@typescript-eslint"],
parserOptions: {
project: "./tsconfig.json",
},
rules: {
"@typescript-eslint/no-floating-promises": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["off"],
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"],
"react/display-name": "off",
},
},
],
module.exports = common;
// @typescript-eslint/no-floating-promises

56
searching-front/.gitignore vendored

@ -0,0 +1,56 @@
# dependencies
node_modules
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
.npm
web_modules/
# blitz
/.blitz/
/.next/
*.sqlite
*.sqlite-journal
.now
.blitz**
blitz-log.log
# misc
.DS_Store
# local env files
.env.local
.env.*.local
.envrc
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Testing
.coverage
*.lcov
.nyc_output
lib-cov
# Caches
*.tsbuildinfo
.eslintcache
.node_repl_history
.yarn-integrity
# Serverless directories
.serverless/
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

5
searching-front/.husky/pre-commit

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npx pretty-quick --staged

6
searching-front/.husky/pre-push

@ -0,0 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx tsc
npm run lint
npm run test

11
searching-front/.npmrc

@ -0,0 +1,11 @@
save-exact=true
legacy-peer-deps=true
strict-peer-dependencies=false
side-effects-cache=false
public-hoist-pattern[]=@tanstack/react-query
public-hoist-pattern[]=next
public-hoist-pattern[]=secure-password
public-hoist-pattern[]=*jest*
public-hoist-pattern[]=@testing-library/*
# Needed for Blitz to work properly. Don't remove the lines above.

10
searching-front/.prettierignore

@ -0,0 +1,10 @@
.gitkeep
.env*
*.ico
*.lock
db/migrations
.next
.yarn
.pnp.*
node_modules
README.md

12
searching-front/.vscode/extensions.json vendored

@ -0,0 +1,12 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"mikestead.dotenv",
"mgmcdermott.vscode-language-babel",
"orta.vscode-jest",
"prisma.prisma"
],
"unwantedRecommendations": []
}

28
searching-front/.vscode/launch.json vendored

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Blitz: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Blitz: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Blitz: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}

14
searching-front/.vscode/settings.json vendored

@ -0,0 +1,14 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"jest.autoRun": "off",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

173
searching-front/README.md

@ -0,0 +1,173 @@
[![Blitz.js](https://raw.githubusercontent.com/blitz-js/art/master/github-cover-photo.png)](https://blitzjs.com)
This is a [Blitz.js](https://github.com/blitz-js/blitz) app.
# ****name****
## Getting Started
Run your app in the development mode.
```
blitz dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Environment Variables
Ensure the `.env.local` file has required environment variables:
```
DATABASE_URL=postgresql://<YOUR_DB_USERNAME>@localhost:5432/searching-front
```
Ensure the `.env.test.local` file has required environment variables:
```
DATABASE_URL=postgresql://<YOUR_DB_USERNAME>@localhost:5432/searching-front_test
```
## Tests
Runs your tests using Jest.
```
yarn test
```
Blitz comes with a test setup using [Jest](https://jestjs.io/) and [react-testing-library](https://testing-library.com/).
## Commands
Blitz comes with a powerful CLI that is designed to make development easy and fast. You can install it with `npm i -g blitz`
```
blitz [COMMAND]
dev Start a development server
build Create a production build
start Start a production server
export Export your Blitz app as a static application
prisma Run prisma commands
generate Generate new files for your Blitz project
console Run the Blitz console REPL
install Install a recipe
help Display help for blitz
test Run project tests
```
You can read more about it on the [CLI Overview](https://blitzjs.com/docs/cli-overview) documentation.
## What's included?
Here is the starting structure of your app.
```
searching-front
├── app/
│ ├── api/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── SignupForm.tsx
│ │ ├── mutations/
│ │ │ ├── changePassword.ts
│ │ │ ├── forgotPassword.test.ts
│ │ │ ├── forgotPassword.ts
│ │ │ ├── login.ts
│ │ │ ├── logout.ts
│ │ │ ├── resetPassword.test.ts
│ │ │ ├── resetPassword.ts
│ │ │ └── signup.ts
│ │ ├── pages/
│ │ │ ├── forgot-password.tsx
│ │ │ ├── login.tsx
│ │ │ ├── reset-password.tsx
│ │ │ └── signup.tsx
│ │ └── validations.ts
│ ├── core/
│ │ ├── components/
│ │ │ ├── Form.tsx
│ │ │ └── LabeledTextField.tsx
│ │ ├── hooks/
│ │ │ └── useCurrentUser.ts
│ │ └── layouts/
│ │ └── Layout.tsx
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── 404.tsx
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ └── users/
│ └── queries/
│ └── getCurrentUser.ts
├── db/
│ ├── migrations/
│ ├── index.ts
│ ├── schema.prisma
│ └── seeds.ts
├── integrations/
├── mailers/
│ └── forgotPasswordMailer.ts
├── public/
│ ├── favicon.ico
│ └── logo.png
├── test/
│ ├── setup.ts
│ └── utils.tsx
├── .eslintrc.js
├── babel.config.js
├── blitz.config.ts
├── jest.config.ts
├── package.json
├── README.md
├── tsconfig.json
└── types.ts
```
These files are:
- The `app/` folder is a container for most of your project. This is where you’ll put any pages or API routes.
- `db/` is where your database configuration goes. If you’re writing models or checking migrations, this is where to go.
- `public/` is a folder where you will put any static assets. If you have images, files, or videos which you want to use in your app, this is where to put them.
- `integrations/` is a folder to put all third-party integrations like with Stripe, Sentry, etc.
- `test/` is a folder where you can put test utilities and integration tests.
- `package.json` contains information about your dependencies and devDependencies. If you’re using a tool like `npm` or `yarn`, you won’t have to worry about this much.
- `tsconfig.json` is our recommended setup for TypeScript.
- `.babel.config.js`, `.eslintrc.js`, `.env`, etc. ("dotfiles") are configuration files for various bits of JavaScript tooling.
- `blitz.config.ts` is for advanced custom configuration of Blitz. [Here you can learn how to use it](https://blitzjs.com/docs/blitz-config).
- `jest.config.js` contains config for Jest tests. You can [customize it if needed](https://jestjs.io/docs/en/configuration).
You can read more about it in the [File Structure](https://blitzjs.com/docs/file-structure) section of the documentation.
### Tools included
Blitz comes with a set of tools that corrects and formats your code, facilitating its future maintenance. You can modify their options and even uninstall them.
- **ESLint**: It lints your code: searches for bad practices and tell you about it. You can customize it via the `.eslintrc.js`, and you can install (or even write) plugins to have it the way you like it. It already comes with the [`blitz`](https://github.com/blitz-js/blitz/tree/canary/packages/eslint-config) config, but you can remove it safely. [Learn More](https://blitzjs.com/docs/eslint-config).
- **Husky**: It adds [githooks](https://git-scm.com/docs/githooks), little pieces of code that get executed when certain Git events are triggerd. For example, `pre-commit` is triggered just before a commit is created. You can see the current hooks inside `.husky/`. If are having problems commiting and pushing, check out ther [troubleshooting](https://typicode.github.io/husky/#/?id=troubleshoot) guide. [Learn More](https://blitzjs.com/docs/husky-config).
- **Prettier**: It formats your code to look the same everywhere. You can configure it via the `.prettierrc` file. The `.prettierignore` contains the files that should be ignored by Prettier; useful when you have large files or when you want to keep a custom formatting. [Learn More](https://blitzjs.com/docs/prettier-config).
## Learn more
Read the [Blitz.js Documentation](https://blitzjs.com/docs/getting-started) to learn more.
The Blitz community is warm, safe, diverse, inclusive, and fun! Feel free to reach out to us in any of our communication channels.
- [Website](https://blitzjs.com)
- [Discord](https://blitzjs.com/discord)
- [Report an issue](https://github.com/blitz-js/blitz/issues/new/choose)
- [Forum discussions](https://github.com/blitz-js/blitz/discussions)
- [How to Contribute](https://blitzjs.com/docs/contributing)
- [Sponsor or donate](https://github.com/blitz-js/blitz#sponsors-and-donations)

30
searching-front/app/auth/components/Button/Button.tsx

@ -0,0 +1,30 @@
import { cn } from "app/core/helpers/common"
import { ReactNode } from "react"
import { AnimatePresence, HTMLMotionProps, motion } from "framer-motion"
import s from "./styles.module.css"
interface Props {
children: ReactNode
theme: "primary"
className?: string
onClick: () => void
}
const Button = ({
children,
theme,
className,
onClick,
...motionpProps
}: Props & HTMLMotionProps<"button">) => {
return (
<motion.button
onClick={onClick}
className={cn(s.root, className, `theme-${theme}`)}
{...motionpProps}
>
{children}
</motion.button>
)
}
export default Button

0
searching-front/app/auth/components/Button/index.ts

29
searching-front/app/auth/components/Button/styles.module.css

@ -0,0 +1,29 @@
.root {
border-radius: 10px;
border: none;
padding: 0 20px;
cursor: pointer;
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, opacity 0.2s ease-in-out,
transform 0.2s ease-in-out;
/* size */
font-size: 18px;
font-weight: 500px;
}
.root:active {
transform: scale(0.95);
}
.root:global(.theme-primary) {
color: var(--default_white);
background-color: var(--button_primary);
}
.root:global(.theme-primary):hover {
background-color: var(--button_primary_hover);
}
.root:global(.theme-primary):active {
background-color: var(--button_primary_pressed);
}

59
searching-front/app/auth/components/LoginForm.tsx

@ -0,0 +1,59 @@
import { AuthenticationError, PromiseReturnType } from "blitz"
import Link from "next/link"
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/core/components/Form"
import login from "app/auth/mutations/login"
import { Login } from "app/auth/validations"
import { useMutation } from "@blitzjs/rpc"
import { Routes } from "@blitzjs/next"
type LoginFormProps = {
onSuccess?: (user: PromiseReturnType<typeof login>) => void
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
<Form
submitText="Login"
schema={Login}
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
const user = await loginMutation(values)
props.onSuccess?.(user)
} catch (error: any) {
if (error instanceof AuthenticationError) {
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
} else {
return {
[FORM_ERROR]:
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
}
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
<div>
<Link href={Routes.ForgotPasswordPage()}>
<a>Forgot your password?</a>
</Link>
</div>
</Form>
<div style={{ marginTop: "1rem" }}>
Or{" "}
<Link href={Routes.SignupPage()}>
<a>Sign Up</a>
</Link>
</div>
</div>
)
}
export default LoginForm

42
searching-front/app/auth/components/SignupForm.tsx

@ -0,0 +1,42 @@
import { LabeledTextField } from "app/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "app/core/components/Form"
import signup from "app/auth/mutations/signup"
import { Signup } from "app/auth/validations"
import { useMutation } from "@blitzjs/rpc"
type SignupFormProps = {
onSuccess?: () => void
}
export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup)
return (
<div>
<h1>Create an Account</h1>
<Form
submitText="Create Account"
schema={Signup}
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
try {
await signupMutation(values)
props.onSuccess?.()
} catch (error: any) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
// This error comes from Prisma
return { email: "This email is already being used" }
} else {
return { [FORM_ERROR]: error.toString() }
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
</Form>
</div>
)
}
export default SignupForm

32
searching-front/app/auth/mutations/changePassword.ts

@ -0,0 +1,32 @@
import { NotFoundError, AuthenticationError } from "blitz"
import { resolver } from "@blitzjs/rpc"
import { SecurePassword } from "@blitzjs/auth"
import db from "db"
import { authenticateUser } from "./login"
import { ChangePassword } from "../validations"
export default resolver.pipe(
resolver.zod(ChangePassword),
resolver.authorize(),
async ({ currentPassword, newPassword }, ctx) => {
const user = await db.user.findFirst({ where: { id: ctx.session.userId as number } })
if (!user) throw new NotFoundError()
try {
await authenticateUser(user.email, currentPassword)
} catch (error: any) {
if (error instanceof AuthenticationError) {
throw new Error("Invalid Password")
}
throw error
}
const hashedPassword = await SecurePassword.hash(newPassword.trim())
await db.user.update({
where: { id: user.id },
data: { hashedPassword },
})
return true
}
)

59
searching-front/app/auth/mutations/forgotPassword.test.ts

@ -0,0 +1,59 @@
import db from "db"
import { hash256 } from "@blitzjs/auth"
import forgotPassword from "./forgotPassword"
import previewEmail from "preview-email"
import { Ctx } from "@blitzjs/next"
beforeEach(async () => {
await db.$reset()
})
const generatedToken = "plain-token"
jest.mock("@blitzjs/auth", () => ({
...jest.requireActual<Record<string, unknown>>("@blitzjs/auth")!,
generateToken: () => generatedToken,
}))
jest.mock("preview-email", () => jest.fn())
describe("forgotPassword mutation", () => {
it("does not throw error if user doesn't exist", async () => {
await expect(forgotPassword({ email: "no-user@email.com" }, {} as Ctx)).resolves.not.toThrow()
})
it("works correctly", async () => {
// Create test user
const user = await db.user.create({
data: {
email: "user@example.com",
tokens: {
// Create old token to ensure it's deleted
create: {
type: "RESET_PASSWORD",
hashedToken: "token",
expiresAt: new Date(),
sentTo: "user@example.com",
},
},
},
include: { tokens: true },
})
// Invoke the mutation
await forgotPassword({ email: user.email }, {} as Ctx)
const tokens = await db.token.findMany({ where: { userId: user.id } })
const token = tokens[0]
if (!user.tokens[0]) throw new Error("Missing user token")
if (!token) throw new Error("Missing token")
// delete's existing tokens
expect(tokens.length).toBe(1)
expect(token.id).not.toBe(user.tokens[0].id)
expect(token.type).toBe("RESET_PASSWORD")
expect(token.sentTo).toBe(user.email)
expect(token.hashedToken).toBe(hash256(generatedToken))
expect(token.expiresAt > new Date()).toBe(true)
expect(previewEmail).toBeCalled()
})
})

42
searching-front/app/auth/mutations/forgotPassword.ts

@ -0,0 +1,42 @@
import { generateToken, hash256 } from "@blitzjs/auth"
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { forgotPasswordMailer } from "mailers/forgotPasswordMailer"
import { ForgotPassword } from "../validations"
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4
export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => {
// 1. Get the user
const user = await db.user.findFirst({ where: { email: email.toLowerCase() } })
// 2. Generate the token and expiration date.
const token = generateToken()
const hashedToken = hash256(token)
const expiresAt = new Date()
expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS)
// 3. If user with this email was found
if (user) {
// 4. Delete any existing password reset tokens
await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } })
// 5. Save this new token in the database.
await db.token.create({
data: {
user: { connect: { id: user.id } },
type: "RESET_PASSWORD",
expiresAt,
hashedToken,
sentTo: user.email,
},
})
// 6. Send the email
await forgotPasswordMailer({ to: user.email, token }).send()
} else {
// 7. If no user found wait the same time so attackers can't tell the difference
await new Promise((resolve) => setTimeout(resolve, 750))
}
// 8. Return the same result whether a password reset email was sent or not
return
})

32
searching-front/app/auth/mutations/login.ts

@ -0,0 +1,32 @@
import { SecurePassword } from "@blitzjs/auth"
import { resolver } from "@blitzjs/rpc"
import { AuthenticationError } from "blitz"
import db from "db"
import { Role } from "types"
import { Login } from "../validations"
export const authenticateUser = async (rawEmail: string, rawPassword: string) => {
const { email, password } = Login.parse({ email: rawEmail, password: rawPassword })
const user = await db.user.findFirst({ where: { email } })
if (!user) throw new AuthenticationError()
const result = await SecurePassword.verify(user.hashedPassword, password)
if (result === SecurePassword.VALID_NEEDS_REHASH) {
// Upgrade hashed password with a more secure hash
const improvedHash = await SecurePassword.hash(password)
await db.user.update({ where: { id: user.id }, data: { hashedPassword: improvedHash } })
}
const { hashedPassword, ...rest } = user
return rest
}
export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => {
// This throws an error if credentials are invalid
const user = await authenticateUser(email, password)
await ctx.session.$create({ userId: user.id, role: user.role as Role })
return user
})

5
searching-front/app/auth/mutations/logout.ts

@ -0,0 +1,5 @@
import { Ctx } from "blitz"
export default async function logout(_: any, ctx: Ctx) {
return await ctx.session.$revoke()
}

82
searching-front/app/auth/mutations/resetPassword.test.ts

@ -0,0 +1,82 @@
import resetPassword from "./resetPassword"
import db from "db"
import { SecurePassword, hash256 } from "@blitzjs/auth"
beforeEach(async () => {
await db.$reset()
})
const mockCtx: any = {
session: {
$create: jest.fn,
},
}
describe("resetPassword mutation", () => {
it("works correctly", async () => {
expect(true).toBe(true)
// Create test user
const goodToken = "randomPasswordResetToken"
const expiredToken = "expiredRandomPasswordResetToken"
const future = new Date()
future.setHours(future.getHours() + 4)
const past = new Date()
past.setHours(past.getHours() - 4)
const user = await db.user.create({
data: {
email: "user@example.com",
tokens: {
// Create old token to ensure it's deleted
create: [
{
type: "RESET_PASSWORD",
hashedToken: hash256(expiredToken),
expiresAt: past,
sentTo: "user@example.com",
},
{
type: "RESET_PASSWORD",
hashedToken: hash256(goodToken),
expiresAt: future,
sentTo: "user@example.com",
},
],
},
},
include: { tokens: true },
})
const newPassword = "newPassword"
// Non-existent token
await expect(
resetPassword({ token: "no-token", password: "", passwordConfirmation: "" }, mockCtx)
).rejects.toThrowError()
// Expired token
await expect(
resetPassword(
{ token: expiredToken, password: newPassword, passwordConfirmation: newPassword },
mockCtx
)
).rejects.toThrowError()
// Good token
await resetPassword(
{ token: goodToken, password: newPassword, passwordConfirmation: newPassword },
mockCtx
)
// Delete's the token
const numberOfTokens = await db.token.count({ where: { userId: user.id } })
expect(numberOfTokens).toBe(0)
// Updates user's password
const updatedUser = await db.user.findFirst({ where: { id: user.id } })
expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe(
SecurePassword.VALID
)
})
})

48
searching-front/app/auth/mutations/resetPassword.ts

@ -0,0 +1,48 @@
import { SecurePassword, hash256 } from "@blitzjs/auth"
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { ResetPassword } from "../validations"
import login from "./login"
export class ResetPasswordError extends Error {
name = "ResetPasswordError"
message = "Reset password link is invalid or it has expired."
}
export default resolver.pipe(resolver.zod(ResetPassword), async ({ password, token }, ctx) => {
// 1. Try to find this token in the database
const hashedToken = hash256(token)
const possibleToken = await db.token.findFirst({
where: { hashedToken, type: "RESET_PASSWORD" },
include: { user: true },
})
// 2. If token not found, error
if (!possibleToken) {
throw new ResetPasswordError()
}
const savedToken = possibleToken
// 3. Delete token so it can't be used again
await db.token.delete({ where: { id: savedToken.id } })
// 4. If token has expired, error
if (savedToken.expiresAt < new Date()) {
throw new ResetPasswordError()
}
// 5. Since token is valid, now we can update the user's password
const hashedPassword = await SecurePassword.hash(password.trim())
const user = await db.user.update({
where: { id: savedToken.userId },
data: { hashedPassword },
})
// 6. Revoke all existing login sessions for this user
await db.session.deleteMany({ where: { userId: user.id } })
// 7. Now log the user in with the new credentials
await login({ email: user.email, password }, ctx)
return true
})

16
searching-front/app/auth/mutations/signup.ts

@ -0,0 +1,16 @@
import { SecurePassword } from "@blitzjs/auth"
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { Role } from "types"
import { Signup } from "../validations"
export default resolver.pipe(resolver.zod(Signup), async ({ email, password }, ctx) => {
const hashedPassword = await SecurePassword.hash(password.trim())
const user = await db.user.create({
data: { email: email.toLowerCase().trim(), hashedPassword, role: "USER" },
select: { id: true, name: true, email: true, role: true },
})
await ctx.session.$create({ userId: user.id, role: user.role as Role })
return user
})

42
searching-front/app/auth/validations.ts

@ -0,0 +1,42 @@
import { z } from "zod"
export const email = z
.string()
.email()
.transform((str) => str.toLowerCase().trim())
export const password = z
.string()
.min(10)
.max(100)
.transform((str) => str.trim())
export const Signup = z.object({
email,
password,
})
export const Login = z.object({
email,
password: z.string(),
})
export const ForgotPassword = z.object({
email,
})
export const ResetPassword = z
.object({
password: password,
passwordConfirmation: password,
token: z.string(),
})
.refine((data) => data.password === data.passwordConfirmation, {
message: "Passwords don't match",
path: ["passwordConfirmation"], // set the path of the error
})
export const ChangePassword = z.object({
currentPassword: z.string(),
newPassword: password,
})

11
searching-front/app/blitz-client.ts

@ -0,0 +1,11 @@
import { AuthClientPlugin } from "@blitzjs/auth"
import { setupBlitzClient } from "@blitzjs/next"
import { BlitzRpcPlugin } from "@blitzjs/rpc"
export const authConfig = {
cookiePrefix: "searching-front-cookie-prefix",
}
export const { withBlitz } = setupBlitzClient({
plugins: [AuthClientPlugin(authConfig), BlitzRpcPlugin({})],
})

16
searching-front/app/blitz-server.ts

@ -0,0 +1,16 @@
import { setupBlitzServer } from "@blitzjs/next"
import { AuthServerPlugin, PrismaStorage } from "@blitzjs/auth"
import { simpleRolesIsAuthorized } from "@blitzjs/auth"
import db from "db"
import { authConfig } from "./blitz-client"
export const { gSSP, gSP, api } = setupBlitzServer({
plugins: [
AuthServerPlugin({
...authConfig,
storage: PrismaStorage(db),
isAuthorized: simpleRolesIsAuthorized,
}),
],
onError: (...args) => console.log("FROM BLITZ SERVER ERROR", ...args),
})

83
searching-front/app/core/components/Form.tsx

@ -0,0 +1,83 @@
import { useState, ReactNode, PropsWithoutRef } from "react"
import { FormProvider, useForm, UseFormProps } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
export interface FormProps<S extends z.ZodType<any, any>>
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
/** All your form fields */
children?: ReactNode
/** Text to display in the submit button */
submitText?: string
schema?: S
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
initialValues?: UseFormProps<z.infer<S>>["defaultValues"]
}
interface OnSubmitResult {
FORM_ERROR?: string
[prop: string]: any
}
export const FORM_ERROR = "FORM_ERROR"
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
const ctx = useForm<z.infer<S>>({
mode: "onBlur",
resolver: schema ? zodResolver(schema) : undefined,
defaultValues: initialValues,
})
const [formError, setFormError] = useState<string | null>(null)
return (
<FormProvider {...ctx}>
<form
onSubmit={ctx.handleSubmit(async (values) => {
const result = (await onSubmit(values)) || {}
for (const [key, value] of Object.entries(result)) {
if (key === FORM_ERROR) {
setFormError(value)
} else {
ctx.setError(key as any, {
type: "submit",
message: value,
})
}
}
})}
className="form"
{...props}
>
{/* Form fields supplied as children are rendered here */}
{children}
{formError && (
<div role="alert" style={{ color: "red" }}>
{formError}
</div>
)}
{submitText && (
<button type="submit" disabled={ctx.formState.isSubmitting}>
{submitText}
</button>
)}
<style global jsx>{`
.form > * + * {
margin-top: 1rem;
}
`}</style>
</form>
</FormProvider>
)
}
export default Form

27
searching-front/app/core/components/Header/Header.tsx

@ -0,0 +1,27 @@
import { Routes } from "@blitzjs/next"
import TonLogo from "app/core/icons/TonLogo"
import { useRouter } from "next/router"
import SearchForm from "../SearchForm"
import ThemeSwitcher from "../ThemeSwitcher/ThemeSwitcher"
import s from "./styles.module.css"
const Header = () => {
const { route } = useRouter()
const shouldShowSearchForm = route === Routes.SearchPage().pathname
const router = useRouter()
const toMain = async () => {
await router.push("/")
}
return (
<div className={s.root}>
<div onClick={toMain} className={s.logoWrapper}>
<TonLogo /> <span>TON SEARCHING</span>
</div>
{shouldShowSearchForm && <SearchForm />}
{/* <ThemeSwitcher /> */}
</div>
)
}
export default Header

1
searching-front/app/core/components/Header/index.ts

@ -0,0 +1 @@
export {default} from './Header'

35
searching-front/app/core/components/Header/styles.module.css

@ -0,0 +1,35 @@
.root {
box-sizing: border-box;
display: flex;
}
@media only screen and (max-width: 900px) {
.root {
flex-direction: column;
}
}
.logoWrapper {
display: flex;
align-items: center;
font-size: 20px;
font-weight: bold;
cursor: pointer;
}
.logoWrapper > span {
margin-left: 10px;
font-weight: 900;
margin-right: 20px;
}
@media only screen and (max-width: 900px) {
.root {
flex-direction: column;
padding: 0;
}
.logoWrapper {
margin-bottom: 10px;
}
}

59
searching-front/app/core/components/LabeledTextField.tsx

@ -0,0 +1,59 @@
import { forwardRef, PropsWithoutRef, ComponentPropsWithoutRef } from "react"
import { useFormContext } from "react-hook-form"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
/** Field name. */
name: string
/** Field label. */
label: string
/** Field type. Doesn't include radio buttons and checkboxes */
type?: "text" | "password" | "email" | "number"
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
labelProps?: ComponentPropsWithoutRef<"label">
}
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({ label, outerProps, labelProps, name, ...props }, ref) => {
const {
register,
formState: { isSubmitting, errors },
} = useFormContext()
const error = Array.isArray(errors[name])
? errors[name].join(", ")
: errors[name]?.message || errors[name]
return (
<div {...outerProps}>
<label {...labelProps}>
{label}
<input disabled={isSubmitting} {...register(name)} {...props} />
</label>
{error && (
<div role="alert" style={{ color: "red" }}>
{error}
</div>
)}
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
input {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid purple;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
}
)
export default LabeledTextField

12
searching-front/app/core/components/Modal/Modal.tsx

@ -0,0 +1,12 @@
import { ReactNode } from "react"
import * as ReactDOM from "react-dom"
interface Props {
children: ReactNode
}
const Modal = ({ children }: Props) => {
return ReactDOM.createPortal(children, document.querySelector("#modal-root"))
}
export default Modal

42
searching-front/app/core/components/Pagination/Pagination.tsx

@ -0,0 +1,42 @@
import { cn } from "app/core/helpers/common"
import ReactPaginate from "react-paginate"
import s from "./styles.module.css"
interface Props {
currentPage: number
pagesCount: number
onPageChange: (page: number) => void
}
const Pagination = ({ currentPage, pagesCount, onPageChange }: Props) => {
return (
<div className={s.pagination}>
<ReactPaginate
breakLabel="..."
nextLabel=">"
onPageChange={(params) => onPageChange(params.selected)}
pageRangeDisplayed={3}
pageCount={pagesCount}
previousLabel="<"
forcePage={currentPage}
// className={className}
// pageLabelBuilder={s.pageLab}
containerClassName={s.container}
pageClassName={s.page}
pageLinkClassName={s.pageLink}
activeClassName={s.active}
activeLinkClassName={s.activeLink}
previousClassName={s.page}
nextClassName={s.page}
previousLinkClassName={s.pageLink}
nextLinkClassName={s.pageLink}
disabledClassName={s.disabled}
disabledLinkClassName={s.disabledLink}
breakClassName={cn(s.break, s.page)}
breakLinkClassName={s.pageLink}
/>
</div>
)
}
export default Pagination

1
searching-front/app/core/components/Pagination/index.ts

@ -0,0 +1 @@
export { default } from "./Pagination"

64
searching-front/app/core/components/Pagination/styles.module.css

@ -0,0 +1,64 @@
.container {
display: flex;
align-items: center;
padding-left: 0;
}
.container > li {
list-style: none;
padding: 0;
margin: 0;
cursor: pointer;
padding-left: 0;
}
.page,
.pageLink {
width: 30px;
height: 30px;
border-radius: 50%;
color: var(--text_light_primary);
display: flex;
justify-content: center;
align-items: center;
transition: background linear 0.5;
}
.page:not(:last-child) {
margin-right: 5px;
}
.pageLink {
opacity: 0.7;
}
.page:hover,
.pageLink:hover {
background: #00000005;
}
.active,
.activeLink {
background: #00000005;
}
.break {
}
.previous {
color: black;
}
.next {
color: black;
}
.previousLink {
color: black;
}
.nextLink {
color: black;
}
.disabled {
opacity: 0;
}
.disabledLink {
color: black;
}

191
searching-front/app/core/components/SearchForm/SearchForm.tsx

@ -0,0 +1,191 @@
import { Routes } from "@blitzjs/next"
import { useMutation, useQuery } from "@blitzjs/rpc"
import Button from "app/auth/components/Button/Button"
import { cn } from "app/core/helpers/common"
import i18n from "app/i18n"
import upsertSearchRequest from "app/search-requests/mutations/upsertSearchRequest"
import getSearchRequests from "app/search-requests/queries/getSearchRequests"
import { AnimatePresence, motion } from "framer-motion"
import { useRouter } from "next/router"
import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useKeyPressEvent } from "react-use"
import useKeypress from "react-use-keypress"
import Modal from "../Modal/Modal"
import s from "./styles.module.css"
const SearchForm = () => {
const router = useRouter()
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement>()
const [value, setValue] = useState(router.query.query as string)
const [focusedSuggestion, setFocusedSuggestion] = useState<number | null>(null)
const [inputIsFocused, setInputFocused] = useState(false)
const onChange = (e) => {
const val = e.target.value
setValue(val)
if (focusedSuggestion) {
setFocusedSuggestion(null)
}
}
let [suggestions] = useQuery(
getSearchRequests,
{ text: value },
{ suspense: false, keepPreviousData: true }
)
const blurInput = () => {
setInputFocused(false)
inputRef.current?.blur()
}
const focusInput = () => {
setInputFocused(true)
inputRef.current?.focus()
}
const onSubmit = async (val?: string) => {
const query = val || value
val && setValue(val)
setFocusedSuggestion(null)
await router.push(Routes.SearchPage({ query }))
}
useKeyPressEvent("Enter", async () => {
if (inputIsFocused) {
blurInput()
if (filteredSuggestion?.length && suggestions && focusedSuggestion) {
await onSubmit(suggestions[focusedSuggestion - 1]?.text)
} else {
await onSubmit(value)
}
} else {
focusInput()
}
})
useKeyPressEvent("ArrowUp", async (event) => {
event.stopImmediatePropagation()
if (inputIsFocused) {
if (focusedSuggestion) {
if (focusedSuggestion === 1) {
return setFocusedSuggestion(null)
}
return setFocusedSuggestion((val) => (val ? val - 1 : null))
} else {
setFocusedSuggestion(suggestions?.length || null)
}
}
})
useKeyPressEvent("ArrowDown", async (event) => {
if (inputIsFocused) {
if (focusedSuggestion) {
if (focusedSuggestion === suggestions?.length) {
return setFocusedSuggestion(null)
}
return setFocusedSuggestion((val) => (val ? val + 1 : null))
} else {
setFocusedSuggestion(1)
}
}
})
useKeyPressEvent("Escape", async (event) => {
if (inputIsFocused) {
blurInput()
}
})
const onSuggestionClick = (val: string) => async () => {
setValue(val)
await onSubmit(val)
}
const onInputBlur = useCallback(() => {
setInputFocused(false)
}, [])
const onInputFocus = useCallback(() => {
setInputFocused(true)
}, [])
const filteredSuggestion = suggestions?.filter((item) => item.text !== value)
const shouldShowSuggestion = filteredSuggestion && !!filteredSuggestion.length
return (
<AnimatePresence>
<motion.div layoutId="searchForm" className={s.root}>
<div className={s.inputWrapper}>
<input
onBlur={onInputBlur}
onFocus={onInputFocus}
onChange={onChange}
value={value}
className={s.input}
placeholder={t("search.placeholder")}
ref={inputRef}
/>
<AnimatePresence>
{value && (
<Button
key="searchButton"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
onClick={() => onSubmit()}
className={s.button}
theme="primary"
>
{t("search.button")}
</Button>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{inputIsFocused && (
<>
{shouldShowSuggestion && (
<motion.div
key="modal"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 10, opacity: 0 }}
transition={{ duration: 0.1 }}
className={s.suggestions}
>
{filteredSuggestion.map((item, index) => (
<div
key={item.text}
onClick={onSuggestionClick(item.text)}
className={cn(s.suggestion, {
[s.focusedSuggestion]: index + 1 === focusedSuggestion,
})}
>
{item.text}
</div>
))}
</motion.div>
)}
<Modal>
<motion.div
key="modal1"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
className={s.suggestionsOverlay}
/>
</Modal>
</>
)}
</AnimatePresence>
</motion.div>
</AnimatePresence>
)
}
export default SearchForm

1
searching-front/app/core/components/SearchForm/index.ts

@ -0,0 +1 @@
export {default} from './SearchForm'

73
searching-front/app/core/components/SearchForm/styles.module.css

@ -0,0 +1,73 @@
.root {
width: 55vw;
font-size: 18px;
position: relative;
z-index: 101;
}
@media only screen and (max-width: 900px) {
.root {
box-sizing: border-box;
width: 100%;
}
}
.inputWrapper {
position: relative;
display: flex;
box-sizing: border-box;
height: 53px;
border: 2px solid var(--button_primary);
border-radius: 0.8rem;
background: var(--background_secondary);
transition: border-color 0.1s ease-in-out;
padding: 4px 4px 4px 20px;
width: 100%;
font-size: inherit;
}
.input {
outline: none;
border: none;
flex: 1;
font-size: inherit;
background: transparent;
color: var(--text-primary);
}
.button {
/* margin: 4px 0; */
}
.suggestions {
margin-top: 5px;
position: absolute;
width: 100%;
background: var(--background_secondary);
border-radius: 6px;
padding-top: 5px;
padding-bottom: 5px;
}
.suggestion {
font-size: inherit;
padding: 7px 20px;
cursor: pointer;
}
.suggestion:hover {
background: var(--background_blue);
}
.suggestionsOverlay {
position: fixed;
top: 0;
right: 0;
width: 100vw;
height: 100vh;
background: #00000050;
z-index: 100;
}
.focusedSuggestion {
background: var(--background_blue) !important;
}

32
searching-front/app/core/components/ThemeSwitcher/ThemeSwitcher.tsx

@ -0,0 +1,32 @@
import s from "./styles.module.css"
const ThemeSwitcher = () => {
return (
<div className={s.root}>
<div className={s.wrapper}>
<input type="checkbox" id="hide-checkbox" className={s.hideCheckbox} />
<label htmlFor="hide-checkbox" className={s.toggle}>
<span className={s.toggleButton}>
<span className={(s.crater, s.crater1)}></span>
<span className={(s.crater, s.crater2)}></span>
<span className={(s.crater, s.crater3)}></span>
<span className={(s.crater, s.crater4)}></span>
<span className={(s.crater, s.crater5)}></span>
<span className={(s.crater, s.crater6)}></span>
<span className={(s.crater, s.crater7)}></span>
</span>
<span className={(s.star, s.star1)}></span>
<span className={(s.star, s.star2)}></span>
<span className={(s.star, s.star3)}></span>
<span className={(s.star, s.star4)}></span>
<span className={(s.star, s.star5)}></span>
<span className={(s.star, s.star6)}></span>
<span className={(s.star, s.star7)}></span>
<span className={(s.star, s.star8)}></span>
</label>
</div>
</div>
)
}
export default ThemeSwitcher

311
searching-front/app/core/components/ThemeSwitcher/styles.module.css

@ -0,0 +1,311 @@
.wrapper {
/* position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); */
}
.hideCheckbox {
opacity: 0;
height: 0;
width: 0;
}
.toggle {
position: relative;
cursor: pointer;
display: inline-block;
width: 200px;
height: 100px;
background: #211042;
border-radius: 50px;
transition: 500ms;
overflow: hidden;
}
.toggleButton {
position: absolute;
display: inline-block;
top: 7px;
left: 6px;
width: 86px;
height: 86px;
border-radius: 50%;
background: #faeaf1;
overflow: hidden;
box-shadow: 0 0 35px 4px rgba(255, 255, 255);
transition: all 500ms ease-out;
}
.crater {
position: absolute;
display: inline-block;
background: #faeaf1;
border-radius: 50%;
transition: 500ms;
}
.crater1 {
background: #fffff9;
width: 86px;
height: 86px;
left: 10px;
bottom: 10px;
}
.crater2 {
width: 20px;
height: 20px;
top: -7px;
left: 44px;
}
.crater3 {
width: 16px;
height: 16px;
top: 20px;
right: -4px;
}
.crater4 {
width: 10px;
height: 10px;
top: 24px;
left: 30px;
}
.crater5 {
width: 15px;
height: 15px;
top: 40px;
left: 48px;
}
.crater6 {
width: 10px;
height: 10px;
top: 48px;
left: 20px;
}
.crater7 {
width: 12px;
height: 12px;
bottom: 5px;
left: 35px;
}
.star {
position: absolute;
display: inline-block;
border-radius: 50%;
background: #fff;
box-shadow: 1px 0 2px 2px rgba(255, 255, 255);
}
.star1 {
width: 6px;
height: 6px;
right: 90px;
bottom: 40px;
}
.star2 {
width: 8px;
height: 8px;
right: 70px;
top: 10px;
}
.star3 {
width: 5px;
height: 5px;
right: 60px;
bottom: 15px;
}
.star4 {
width: 3px;
height: 3px;
right: 40px;
bottom: 50px;
}
.star5 {
width: 4px;
height: 4px;
right: 10px;
bottom: 35px;
}
.star6,
.star7,
.star8 {
width: 10px;
height: 2px;
border-radius: 2px;
transform: rotate(-45deg);
box-shadow: 5px 0px 4px 1px #fff;
animation-name: travel;
animation-duration: 1.5s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.star6 {
right: 30px;
bottom: 30px;
animation-delay: -2s;
}
.star7 {
right: 50px;
bottom: 60px;
}
.star8 {
right: 90px;
top: 10px;
animation-delay: -4s;
}
@keyframes travel {
0% {
transform: rotate(-45deg) translateX(70px);
}
50% {
transform: rotate(-45deg) translateX(-20px);
box-shadow: 5px 0px 6px 1px #fff;
}
100% {
transform: rotate(-45deg) translateX(-30px);
width: 2px;
height: 2px;
opacity: 0;
box-shadow: none;
}
}
.hideCheckbox:checked + .toggle {
background: #24d7f7;
}
.hideCheckbox:checked + .toggle .toggleButton {
background: #f7ffff;
transform: translateX(102px);
box-shadow: 0 0 35px 5px rgba(255, 255, 255);
}
.hideCheckbox:checked + .toggle .toggleButton .crater {
transform: rotate(-45deg) translateX(70px);
}
.hideCheckbox:checked + .toggle .star {
animation: move 2s infinite;
transform: none;
box-shadow: none;
}
.hideCheckbox:checked + .toggle .star1 {
width: 40px;
height: 10px;
border-radius: 10px;
background: #fff;
left: 20px;
top: 25px;
box-shadow: none;
}
.hideCheckbox:checked + .toggle .star2 {
width: 12px;
height: 12px;
background: #fff;
left: 26px;
top: 23px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
.hideCheckbox:checked + .toggle .star3 {
width: 16px;
height: 16px;
background: #fff;
left: 35px;
top: 19px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
.hideCheckbox:checked + .toggle .star4 {
width: 14px;
height: 14px;
background: #fff;
left: 46px;
top: 21px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
.hideCheckbox:checked + .toggle .star5 {
width: 60px;
height: 15px;
border-radius: 15px;
background: #fff;
left: 30px;
bottom: 20px;
box-shadow: none;
}
.hideCheckbox:checked + .toggle .star6 {
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
left: 38px;
bottom: 20px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
.hideCheckbox:checked + .toggle .star7 {
width: 24px;
height: 24px;
background: #fff;
border-radius: 50%;
left: 52px;
bottom: 20px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
.hideCheckbox:checked + .toggle .star8 {
width: 21px;
height: 21px;
background: #fff;
border-radius: 50%;
left: 70px;
top: 59px;
box-shadow: -1px 0 2px 0 rgba(0, 0, 0, 0.1);
}
@keyframes move {
0% {
transform: none;
}
25% {
transform: translateX(2px);
}
100% {
transform: translateX(-2px);
}
}
/* p {
text-align: center;
letter-spacing: 15px;
background: #34495e;
color: #fff;
}
p.morning {
background: #e67e22;
} */

29
searching-front/app/core/components/TonBrilliant/TonBrillian.tsx

@ -0,0 +1,29 @@
import { Routes } from "@blitzjs/next"
import TonLogo from "app/core/icons/TonLogo"
import { useRouter } from "next/router"
import s from "./styles.module.css"
const TonBrilliant = () => {
return (
<div className={s.root}>
<video
className={s.video}
width="300"
height="300"
autoPlay
loop
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII"
preload="auto"
muted
disableremoteplayback
>
<source src="/dimond_2-1.hevc.4d6283ed.mp4" type="video/quicktime" />
<source src="/dimond_1_VP9.29bcaf8e.webm" type="video/webm" />
</video>
</div>
)
}
export default TonBrilliant

1
searching-front/app/core/components/TonBrilliant/index.ts

@ -0,0 +1 @@
export { default } from "./TonBrillian"

39
searching-front/app/core/components/TonBrilliant/styles.module.css

@ -0,0 +1,39 @@
.root {
/* position: relative; */
}
.video {
position: relative;
z-index: 1002;
}
.root:before {
content: "";
z-index: 0;
max-width: 690px;
max-height: 690px;
width: 100vw;
height: 100vh;
position: absolute;
top: -234px;
left: 0;
right: 0;
bottom: 0;
margin: auto;
background: radial-gradient(
circle,
rgba(60, 187, 251, 0.3) 0,
rgba(60, 187, 251, 0.221) 13.11%,
rgba(60, 187, 251, 0.162) 23.46%,
rgba(60, 187, 251, 0.115) 32.43%,
rgba(60, 187, 251, 0.083) 38.985%,
rgba(60, 187, 251, 0.058) 44.85%,
rgba(60, 187, 251, 0.038) 50.37%,
rgba(60, 187, 251, 0.023) 55.338%,
rgba(60, 187, 251, 0.013) 59.409%,
rgba(60, 187, 251, 0.006) 62.79%,
rgba(60, 187, 251, 0.002) 65.688%,
rgba(60, 187, 251, 0.001) 67.758%,
rgba(60, 187, 251, 0) 69%
);
}

17
searching-front/app/core/components/WebsiteCard/WebsiteCard.tsx

@ -0,0 +1,17 @@
import { cleanUrlForUi } from 'app/core/helpers/common';
import s from './styles.module.css'
interface Props {
url: string;
title: string;
description: string;
}
const WebsiteCard = (props:Props) => {
return <div className={s.root}>
<a className={s.titleLink} href={props.url}>{props.title}</a>
<a className={s.miniLink} href={props.url}>{cleanUrlForUi(props.url)}</a>
<div className={s.description}>{props.description}</div>
</div>
}
export default WebsiteCard;

1
searching-front/app/core/components/WebsiteCard/index.ts

@ -0,0 +1 @@
export { default } from "./WebsiteCard"

31
searching-front/app/core/components/WebsiteCard/styles.module.css

@ -0,0 +1,31 @@
.root {
padding: 12px 16px;
background: var(--background_secondary);
border-radius: 12px;
display: flex;
flex-direction: column;
}
.root:not(:last-of-type) {
margin-bottom: 10px;
}
.titleLink {
font-size: 19px;
line-height: 24px;
text-decoration: none;
color: var(--color_link);
}
.titleLink:hover,
.miniLink:hover {
color: var(--hovered_link);
}
.miniLink {
text-decoration: none;
font-size: 14px;
color: var(--text_light_secondary);
}
.description {
font-size: 13px;
}

0
searching-front/app/core/constants.ts

20
searching-front/app/core/contextProviders/serverSidePropsProvider.ts

@ -0,0 +1,20 @@
import { gSSP } from "app/blitz-server"
import React from "react"
interface ContextParams {
cookies: Record<string, string | undefined>
}
interface Props {
props: ContextParams
}
export const serverSideProps = async ({ ctx, req }): Promise<Props> => {
return {
props: {
cookies: req.cookies,
},
}
}
export const ServerSidePropsContext = React.createContext<ContextParams>({ cookies: {} })

32
searching-front/app/core/darkTheme.css

@ -0,0 +1,32 @@
#layout.dark {
--background_main: #232328;
--background_gradient: linear-gradient(0deg, #232328, #343437 101.47%);
--background_secondary: hsl(240 2% 23% / 1);
--background_blue: hsla(200, 100%, 50%, 0.05);
--background_black_mini_opacity: rgb(0, 0, 0, 0.24);
--background_loading_gradient_light: linear-gradient(
90deg,
#f7f9fb -9.69%,
#f2f5f8 -9.68%,
#fff 52.19%,
#f2f5f8 106.56%
);
--button_primary: #08c;
--button_primary_hover: #00a1f1;
--button_primary_pressed: #076c9f;
--button_secondary: hsla(0, 0%, 100%, 0.06);
--button_secondary_hover: hsla(0, 0%, 100%, 0.12);
--button_secondary_pressed: hsla(0, 0%, 100%, 0.03);
--button_text: #07a0ec;
--button_secondary_text_hover: #5bc8ff;
--button_secondary_text_pressed: #0186c9;
--text_primary: #fff;
--text_secondary: hsla(0, 0%, 100%, 0.7);
--text_gradient: linear-gradient(89.92deg, #06a1ef 28.51%, #69cdff 85.79%);
--icon_primary: #02a8fb;
--separator: hsla(0, 0%, 100%, 0.06);
--color_link: hsl(200 50% 70% / 1);
}

73
searching-front/app/core/global.css

@ -0,0 +1,73 @@
body {
margin: 0;
}
@font-face {
font-family: Mulish;
font-style: normal;
font-weight: 800;
font-display: block;
src: url(/Mulish-ExtraBold.woff2) format("woff2");
}
@font-face {
font-family: Mulish;
font-style: normal;
font-weight: 700;
font-display: block;
src: url(/Mulish-Bold.woff2) format("woff2");
}
@font-face {
font-family: Mulish;
font-style: normal;
font-weight: 500;
font-display: block;
src: url(/Mulish-Medium.woff2) format("woff2");
}
@font-face {
font-family: Mulish;
font-style: normal;
font-weight: 400;
font-display: block;
src: url(/Mulish-Regular.woff2) format("woff2");
}
@font-face {
font-family: IBMPlexMono;
font-style: normal;
font-weight: 700;
font-display: block;
src: url(/IBMPlexMono-Bold.woff2) format("woff2");
}
body {
background: #08c;
}
html,
#__next {
position: relative;
min-height: 100vh;
margin: 0;
padding: 0;
cursor: default;
white-space: pre-line;
-webkit-font-smoothing: subpixel-antialiased;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: transparent;
font-family: Mulish, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-style: normal;
}
#__next {
display: flex;
flex-direction: column;
}
#layout {
min-height: 100vh;
color: var(--text_primary);
background-color: var(--background_main);
}

10
searching-front/app/core/helpers/common.ts

@ -0,0 +1,10 @@
import classnames from "classnames"
export const cn = classnames
export const cleanUrlForUi = (url: string) => {
return url.replace("https://", "").replace("http://", "").replace("/", "")
}
export const isNode = () => {
return typeof window !== "object"
}

10
searching-front/app/core/hooks/useCurrentTheme.ts

@ -0,0 +1,10 @@
import { useContext } from "react"
import { ServerSidePropsContext } from "../contextProviders/serverSidePropsProvider"
import jsCookies from "js-cookie"
const COOKIE_NAME = "theme"
export const useCurrentTheme = () => {
const { cookies } = useContext(ServerSidePropsContext)
return jsCookies.get(COOKIE_NAME) || cookies[COOKIE_NAME] || "light"
}

7
searching-front/app/core/hooks/useCurrentUser.ts

@ -0,0 +1,7 @@
import { useQuery } from "@blitzjs/rpc"
import getCurrentUser from "app/users/queries/getCurrentUser"
export const useCurrentUser = () => {
const [user] = useQuery(getCurrentUser,null)
return user
}

16
searching-front/app/core/icons/TonLogo.tsx

@ -0,0 +1,16 @@
const TonLogo = () => (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z"
fill="#0088CC"
></path>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.5489 10.5703H20.5228C20.8403 10.5703 21.1576 10.6169 21.4891 10.7715C21.8865 10.9567 22.0973 11.2488 22.2449 11.4647C22.2564 11.4815 22.2672 11.4988 22.2771 11.5165C22.4507 11.8256 22.5402 12.1592 22.5402 12.5181C22.5402 12.8592 22.459 13.2307 22.2771 13.5545C22.2754 13.5576 22.2736 13.5607 22.2718 13.5638L16.6022 23.3029C16.4772 23.5177 16.2471 23.6495 15.9986 23.6486C15.7501 23.6477 15.5209 23.5143 15.3974 23.2987L9.8319 13.5803C9.8303 13.5777 9.8287 13.575 9.8271 13.5724C9.69973 13.3625 9.50276 13.0379 9.4683 12.619C9.43664 12.2339 9.52321 11.8479 9.71676 11.5133C9.9103 11.1786 10.2017 10.9111 10.5521 10.7473C10.9279 10.5717 11.3087 10.5703 11.5489 10.5703ZM15.3054 11.9616H11.5489C11.3021 11.9616 11.2073 11.9768 11.1411 12.0078C11.0495 12.0505 10.9726 12.1208 10.9212 12.2098C10.8697 12.2988 10.8465 12.4019 10.8549 12.505C10.8598 12.5642 10.8839 12.6319 11.0261 12.8664C11.0291 12.8713 11.032 12.8763 11.0349 12.8813L15.3054 20.3384V11.9616ZM16.6967 11.9616V20.3752L21.0661 12.8695C21.1154 12.7799 21.1489 12.6504 21.1489 12.5181C21.1489 12.4109 21.1266 12.3177 21.0769 12.2217C21.0248 12.1467 20.993 12.107 20.9664 12.0798C20.9436 12.0565 20.9261 12.0441 20.9013 12.0325C20.798 11.9844 20.6922 11.9616 20.5228 11.9616H16.6967Z"
fill="white"
></path>
</svg>
)
export default TonLogo

36
searching-front/app/core/layouts/Layout/index.tsx

@ -0,0 +1,36 @@
import Head from "next/head"
import React, { FC } from "react"
import { BlitzLayout } from "@blitzjs/next"
import s from "./styles.module.css"
import Header from "app/core/components/Header"
import { cn } from "app/core/helpers/common"
import { useCurrentTheme } from "app/core/hooks/useCurrentTheme"
const Layout: BlitzLayout<{
title?: string
children?: React.ReactNode
withoutPaddings?: boolean
}> = ({ title, children, withoutPaddings }) => {
const theme = useCurrentTheme()
return (
<>
<Head>
<title>{title || "searching-front"}</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
</Head>
<div
id="layout"
className={cn(s.root, {
[theme]: theme,
})}
>
<Header />
<div className={cn(s.content, { [s.withoutPaddings]: withoutPaddings })}>{children}</div>
</div>
</>
)
}
export default Layout

23
searching-front/app/core/layouts/Layout/styles.module.css

@ -0,0 +1,23 @@
.root {
background: var(--background_main);
padding: 20px;
}
.content {
margin-top: 20px;
margin-left: var(--logoWrapperWidth);
display: flex;
flex-direction: column;
flex: 1;
/* margin: auto; */
}
.content.withoutPaddings {
margin-left: 0;
}
@media only screen and (max-width: 900px) {
.content {
margin-left: 0;
}
}

34
searching-front/app/core/lightTheme.css

@ -0,0 +1,34 @@
#layout.light {
--background_main: hsla(206, 63%, 97%, 1);
--background_secondary: white;
--background_gradient: linear-gradient(180deg, #f7f9fb, rgba(238, 242, 245, 0.8) 116.16%);
--background_icon: rgba(0, 136, 204, 0.06);
--background_blue: rgba(0, 136, 204, 0.1);
--background_gradient: linear-gradient(180deg, #fff, #f7f9fb 134.8%);
--background_green_light: #829a94;
--background_mini_opacity: rgba(255, 255, 255, 0.75);
--background_loading_gradient_light: linear-gradient(
90deg,
#f7f9fb -9.69%,
#f2f5f8 -9.68%,
#fff 52.19%,
#f2f5f8 106.56%
);
--button_primary: #08c;
--button_primary_hover: #0197e2;
--button_primary_pressed: #0082c2;
--button_secondary: rgba(246, 248, 251, 0.8);
--button_secondary_hover: #f4f7fa;
--button_secondary_pressed: #f4f7fa;
--button_primary_small: rgba(0, 136, 204, 0.06);
--button_primary_small_hover: rgba(0, 136, 204, 0.3);
--button_primary_small_pressed: rgba(0, 136, 204, 0.03);
--text_primary: #161c28;
--text_secondary: #728a96;
--icon_primary: #08c;
--icon_secondary: rgba(236, 240, 243, 0.8);
--icon_thirdly: #98b2bf;
--separator_light: rgba(123, 148, 160, 0.2);
--color_link: #101070;
}

14
searching-front/app/core/pages/Main/Main.tsx

@ -0,0 +1,14 @@
import SearchForm from "app/core/components/SearchForm"
import TonBrilliant from "app/core/components/TonBrilliant"
import s from "./styles.module.css"
const Main = () => {
return (
<div className={s.root}>
<TonBrilliant />
<SearchForm />
</div>
)
}
export default Main

1
searching-front/app/core/pages/Main/index.ts

@ -0,0 +1 @@
export {default} from './Main'

8
searching-front/app/core/pages/Main/styles.module.css

@ -0,0 +1,8 @@
.root {
display: flex;
flex-direction: column;
/* justify-content: center; */
align-items: center;
flex: 1;
margin-bottom: 200px;
}

51
searching-front/app/core/pages/Search/Search.tsx

@ -0,0 +1,51 @@
import { useQuery } from "@blitzjs/rpc"
import Pagination from "app/core/components/Pagination"
import SearchForm from "app/core/components/SearchForm"
import WebsiteCard from "app/core/components/WebsiteCard"
import getSearchResult from "app/search-requests/queries/getSearchResult"
import { useRouter } from "next/router"
import { useState } from "react"
import ReactPaginate from "react-paginate"
import s from "./styles.module.css"
const Search = () => {
const router = useRouter()
const [page, setPage] = useState(0)
const [res] = useQuery(
getSearchResult,
{ text: router.query.query as string, page },
{ suspense: false, keepPreviousData: true }
)
const onPageChange = (page: number) => {
setPage(page)
}
const getContent = () => {
if (res?.hits.length) {
return (
<div className={s.content}>
<div className={s.content}>
{Object.values(res.hits).map((i) => (
<WebsiteCard {...i} />
))}
</div>
<div className={s.pagination}>
<Pagination
onPageChange={onPageChange}
currentPage={page}
pagesCount={res.pagesCount}
/>
</div>
</div>
)
} else if (res && !res.hits.length) {
return "not found"
}
return "loading"
}
return <div className={s.root}>{getContent()}</div>
}
export default Search

1
searching-front/app/core/pages/Search/index.ts

@ -0,0 +1 @@
export {default} from './Search'

9
searching-front/app/core/pages/Search/styles.module.css

@ -0,0 +1,9 @@
.root {
/* display: flex; */
/* flex-direction: column;
justify-content: center; */
/* align-items: center; */
/* height: 100%; */
width: 100%;
max-width: 604px;
}

70
searching-front/app/core/variables.css

@ -0,0 +1,70 @@
:root {
--primary: #fc0;
}
body {
--ton_blue: #08c;
--ton_dark_blue: #019be9;
--default_white: #fff;
--default_black: #161c28;
--toncoin_header: #353538;
--toncoin_gradient: linear-gradient(297.97deg, #232328 9.93%, #343437 76.88%);
--StripeMenuWhite: #fff;
}
body {
/* --background_light_main: #f7f9fb;
--background_light_gradient: linear-gradient(180deg, #f7f9fb, rgba(238, 242, 245, 0.8) 116.16%);
--background_light_icon: rgba(0, 136, 204, 0.06);
--background_light_blue: rgba(0, 136, 204, 0.1);
--background_dark_main: #232328;
--background_dark_gradient: linear-gradient(0deg, #232328, #343437 101.47%);
--background_dark_secondary: hsla(0, 0%, 100%, 0.03);
--background_gradient_light: linear-gradient(180deg, #fff, #f7f9fb 134.8%);
--background_green_light: #829a94;
--background_black_mini_opacity: rgb(0, 0, 0, 0.24);
--background_loading_gradient_light: linear-gradient(
90deg,
#f7f9fb -9.69%,
#f2f5f8 -9.68%,
#fff 52.19%,
#f2f5f8 106.56%
);
--button_light_primary: #08c;
--button_light_primary_hover: #0197e2;
--button_light_primary_pressed: #0082c2;
--button_light_secondary: rgba(246, 248, 251, 0.8);
--button_light_secondary_hover: #f4f7fa;
--button_light_secondary_pressed: #f4f7fa;
--button_dark_primary: #08c;
--button_dark_primary_hover: #00a1f1;
--button_dark_primary_pressed: #076c9f;
--button_dark_secondary: hsla(0, 0%, 100%, 0.06);
--button_dark_secondary_hover: hsla(0, 0%, 100%, 0.12);
--button_dark_secondary_pressed: hsla(0, 0%, 100%, 0.03);
--button_dark_text: #07a0ec;
--button_dark_secondary_text_hover: #5bc8ff;
--button_dark_secondary_text_pressed: #0186c9;
--button_light_primary_small: rgba(0, 136, 204, 0.06);
--button_light_primary_small_hover: rgba(0, 136, 204, 0.3);
--button_light_primary_small_pressed: rgba(0, 136, 204, 0.03);
--text_light_primary: #161c28;
--text_light_secondary: #728a96;
--text_dark_primary: #fff;
--text_dark_secondary: hsla(0, 0%, 100%, 0.7);
--text_dark_gradient: linear-gradient(89.92deg, #06a1ef 28.51%, #69cdff 85.79%);
--icon_light_primary: #08c;
--icon_light_secondary: rgba(236, 240, 243, 0.8);
--icon_light_thirdly: #98b2bf;
--icon_dark_primary: #02a8fb;
--separator_light: rgba(123, 148, 160, 0.2);
--separator_dark: hsla(0, 0%, 100%, 0.06);
--black: #000;
--covers_light_green: #caefd9;
--hovered_link: #08c;
--light_border: rgba(114, 138, 150, 0.24);
not-ton */
--maxWidthMobile: 900px;
--logoWrapperWidth: 230px;
}

8
searching-front/app/i18n/en.ts

@ -0,0 +1,8 @@
const en = {
translation: {
"search.placeholder": "Find your own ton",
"search.button": "Search",
},
}
export default en

41
searching-front/app/i18n/index.ts

@ -0,0 +1,41 @@
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import ru from "./ru"
import en from "./en"
import { isNode } from "app/core/helpers/common"
enum I18nLaguages {
ru = "ru",
en = "en",
}
const languageKey = "i18nlangugage"
export const getI18nLanguage = () => {
let lang
if (!isNode()) {
lang = window.localStorage.getItem(languageKey)
}
return lang || "en"
}
export const setI18nLanguage = (lang: I18nLaguages) => {
if (!isNode()) {
window.localStorage.setItem(languageKey, lang)
}
}
i18n.use(initReactI18next).init({
resources: {
ru,
en,
},
fallbackLng: "en",
lng: getI18nLanguage(),
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
})
export default i18n

8
searching-front/app/i18n/ru.ts

@ -0,0 +1,8 @@
const ru = {
translation: {
"search.placeholder": "Найди свой собственный TON",
"search.button": "Найти",
},
}
export default ru

24
searching-front/app/search-requests/mutations/upsertSearchRequest.ts

@ -0,0 +1,24 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { z } from "zod"
const CreateSearchRequest = z.object({
text: z.string(),
})
export default resolver.pipe(
resolver.zod(CreateSearchRequest),
async ({ text }) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
// const searchRequest = await db.searchRequest.create({ data: input });
const searchRequest = await db.searchRequest.upsert({
where: {
text,
},
update: { count: { increment: 1 } },
create: { text },
})
return searchRequest
}
)

24
searching-front/app/search-requests/queries/getSearchRequests.ts

@ -0,0 +1,24 @@
import { NotFoundError } from "blitz";
import { resolver } from "@blitzjs/rpc";
import db from "db";
import { z } from "zod";
const GetSearchRequest = z.object({
// This accepts type of undefined, but is required at runtime
text: z.string()
});
export default resolver.pipe(
resolver.zod(GetSearchRequest),
async ({ text }) => {
if(!text){
return []
}
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const searchRequest = await db.searchRequest.findMany({ where:{text:{startsWith:text}},take:5,orderBy:[{count:'desc'}] })
// if (!searchRequest) throw new NotFoundError();
return searchRequest;
}
);

37
searching-front/app/search-requests/queries/getSearchResult.ts

@ -0,0 +1,37 @@
import { NotFoundError } from "blitz"
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { z } from "zod"
import Elastic from "services/modules/elastic"
import { SEARCH_PER_PAGE } from "services/commonConstants"
import upsertSearchRequest from "../mutations/upsertSearchRequest"
const GetSearchRequest = z.object({
// This accepts type of undefined, but is required at runtime
text: z.string(),
page: z.number(),
})
const bodyToDescription = (text: string, search) => {
const reg = new RegExp(`(${search}.{100})`)
const justText = new RegExp(`(.{100})`)
return reg.exec(text)?.[0] || justText.exec(text)?.[0]
}
const processResult = ({ bodyText, ...res }: Object, search: string) => {
return {
...res,
description: res.description || bodyToDescription(bodyText, search),
}
}
export default resolver.pipe(resolver.zod(GetSearchRequest), async ({ text, page }, c) => {
upsertSearchRequest({ text }, c).catch(console.log)
const result = await Elastic.search({ text, page })
return {
hits: result.hits.map((i) => processResult(i._source, text)),
pagesCount: Math.ceil(result.total / SEARCH_PER_PAGE),
}
})

15
searching-front/app/users/queries/getCurrentUser.ts

@ -0,0 +1,15 @@
import { Ctx } from "blitz"
import db from "db"
export default async function getCurrentUser(_ = null, { session }: Ctx) {
if (!session.userId) return null
const user = await db.user.findFirst({
where: { id: session.userId as number },
select: { id: true, name: true, email: true, role: true },
})
return user
}

9
searching-front/db/index.ts

@ -0,0 +1,9 @@
import { enhancePrisma } from "blitz"
import { PrismaClient } from "@prisma/client"
const EnhancedPrisma = enhancePrisma(PrismaClient)
export * from "@prisma/client"
const db = new EnhancedPrisma()
export default db

98
searching-front/db/migrations/20220930173413_a/migration.sql

@ -0,0 +1,98 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"hashedPassword" TEXT,
"role" TEXT NOT NULL DEFAULT 'USER',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"expiresAt" TIMESTAMP(3),
"handle" TEXT NOT NULL,
"hashedSessionToken" TEXT,
"antiCSRFToken" TEXT,
"publicData" TEXT,
"privateData" TEXT,
"userId" INTEGER,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Token" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"hashedToken" TEXT NOT NULL,
"type" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"sentTo" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Token_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SearchRequest" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"text" TEXT NOT NULL,
"count" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "SearchRequest_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Webpage" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"path" TEXT NOT NULL,
CONSTRAINT "Webpage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Domain" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"address" TEXT NOT NULL,
"lastParse" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Domain_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_handle_key" ON "Session"("handle");
-- CreateIndex
CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type");
-- CreateIndex
CREATE UNIQUE INDEX "SearchRequest_text_key" ON "SearchRequest"("text");
-- CreateIndex
CREATE UNIQUE INDEX "Webpage_path_key" ON "Webpage"("path");
-- CreateIndex
CREATE UNIQUE INDEX "Domain_address_key" ON "Domain"("address");
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

2
searching-front/db/migrations/20220930173504_a/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Domain" ALTER COLUMN "lastParse" DROP NOT NULL;

13
searching-front/db/migrations/20220930184658_asd/migration.sql

@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "NFTDomain" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"address" TEXT NOT NULL,
"available" BOOLEAN NOT NULL,
CONSTRAINT "NFTDomain_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NFTDomain_address_key" ON "NFTDomain"("address");

22
searching-front/db/migrations/20220930185345_a/migration.sql

@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the `NFTDomain` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "NFTDomain";
-- CreateTable
CREATE TABLE "NftDomain" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"address" TEXT NOT NULL,
"available" BOOLEAN NOT NULL,
CONSTRAINT "NftDomain_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NftDomain_address_key" ON "NftDomain"("address");

3
searching-front/db/migrations/migration_lock.toml

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

97
searching-front/db/schema.prisma

@ -0,0 +1,97 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// --------------------------------------
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String @unique
hashedPassword String?
role String @default("USER")
tokens Token[]
sessions Session[]
}
model Session {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
handle String @unique
hashedSessionToken String?
antiCSRFToken String?
publicData String?
privateData String?
user User? @relation(fields: [userId], references: [id])
userId Int?
}
model Token {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hashedToken String
type String
// See note below about TokenType enum
// type TokenType
expiresAt DateTime
sentTo String
user User @relation(fields: [userId], references: [id])
userId Int
@@unique([hashedToken, type])
}
// NOTE: It's highly recommended to use an enum for the token type
// but enums only work in Postgres.
// See: https://blitzjs.com/docs/database-overview#switch-to-postgre-sql
// enum TokenType {
// RESET_PASSWORD
// }
model SearchRequest {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
text String @unique
count Int @default(0)
}
model Webpage {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
path String @unique
}
model Domain {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
address String @unique
lastParse DateTime?
}
model NftDomain {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
address String @unique
available Boolean
}

15
searching-front/db/seeds.ts

@ -0,0 +1,15 @@
// import db from "./index"
/*
* This seed function is executed when you run `blitz db seed`.
*
* Probably you want to use a library like https://chancejs.com
* to easily generate realistic data.
*/
const seed = async () => {
// for (let i = 0; i < 5; i++) {
// await db.project.create({ data: { name: "Project " + i } })
// }
}
export default seed

0
searching-front/integrations/.keep

11
searching-front/jest.config.js

@ -0,0 +1,11 @@
const nextJest = require("@blitzjs/next/jest")
const createJestConfig = nextJest({
dir: "./",
})
const customJestConfig = {
testEnvironment: "jest-environment-jsdom",
}
module.exports = createJestConfig(customJestConfig)

0
searching-front/mailers/.keep

45
searching-front/mailers/forgotPasswordMailer.ts

@ -0,0 +1,45 @@
/* TODO - You need to add a mailer integration in `integrations/` and import here.
*
* The integration file can be very simple. Instantiate the email client
* and then export it. That way you can import here and anywhere else
* and use it straight away.
*/
type ResetPasswordMailer = {
to: string
token: string
}
export function forgotPasswordMailer({ to, token }: ResetPasswordMailer) {
// In production, set APP_ORIGIN to your production server origin
const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN
const resetUrl = `${origin}/auth/reset-password?token=${token}`
const msg = {
from: "TODO@example.com",
to,
subject: "Your Password Reset Instructions",
html: `
<h1>Reset Your Password</h1>
<h3>NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts</h3>
<a href="${resetUrl}">
Click here to set a new password
</a>
`,
}
return {
async send() {
if (process.env.NODE_ENV === "production") {
// TODO - send the production email, like this:
// await postmark.sendEmail(msg)
throw new Error("No production email implementation in mailers/forgotPasswordMailer")
} else {
// Preview email in the browser
const previewEmail = (await import("preview-email")).default
await previewEmail(msg)
}
},
}
}

5
searching-front/next-env.d.ts vendored

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

9
searching-front/next.config.js

@ -0,0 +1,9 @@
// @ts-check
const { withBlitz } = require("@blitzjs/next")
/**
* @type {import('@blitzjs/next').BlitzConfig}
**/
const config = {}
module.exports = withBlitz(config)

32879
searching-front/package-lock.json generated

File diff suppressed because it is too large Load Diff

89
searching-front/package.json

@ -0,0 +1,89 @@
{
"name": "searching-front",
"version": "1.0.0",
"scripts": {
"watcher": "ts-node-esm ./services/domain-watcher.ts",
"parser": "ts-node-esm ./services/parser.ts",
"dev": "blitz dev",
"build": "blitz build",
"start": "blitz start",
"studio": "blitz prisma studio",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"test": "jest",
"test:watch": "jest --watch",
"prepare": "husky install"
},
"prisma": {
"schema": "db/schema.prisma"
},
"prettier": {
"semi": false,
"printWidth": 100
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix"
]
},
"dependencies": {
"@blitzjs/auth": "2.0.0-beta.3",
"@blitzjs/next": "2.0.0-beta.3",
"@blitzjs/rpc": "2.0.0-beta.3",
"@elastic/elasticsearch": "8.2.1",
"@hookform/resolvers": "2.9.7",
"@prisma/client": "4.2.1",
"@types/html-to-text": "8.1.1",
"@types/textversionjs": "1.1.1",
"axios": "0.27.2",
"blitz": "2.0.0-beta.3",
"cheerio": "1.0.0-rc.12",
"classnames": "2.3.1",
"dotenv": "16.0.1",
"framer-motion": "7.2.1",
"html-to-text": "8.2.1",
"i": "0.3.7",
"i18next": "21.9.1",
"jsdom": "20.0.0",
"next": "12.2.5",
"node-fetch": "3.2.10",
"npm": "8.18.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.34.2",
"react-i18next": "11.18.6",
"react-paginate": "8.1.3",
"react-use": "17.4.0",
"react-use-keypress": "1.3.1",
"sanitize-html": "2.7.1",
"textversionjs": "1.1.3",
"tonapi-sdk-js": "0.18.0",
"tonweb": "0.0.55",
"ts-node": "10.9.1",
"zod": "3.17.3"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",
"@testing-library/jest-dom": "5.16.3",
"@types/jest": "27.4.1",
"@types/jsdom": "20.0.0",
"@types/node": "17.0.16",
"@types/preview-email": "2.0.1",
"@types/react": "18.0.17",
"@types/sanitize-html": "2.6.2",
"@typescript-eslint/eslint-plugin": "5.30.5",
"eslint": "7.32.0",
"eslint-config-next": "12.2.0",
"eslint-config-prettier": "8.5.0",
"husky": "7.0.4",
"jest": "27.5.1",
"lint-staged": "12.1.7",
"prettier": "^2.5.1",
"prettier-plugin-prisma": "3.8.0",
"pretty-quick": "3.1.3",
"preview-email": "3.0.7",
"prisma": "4.2.1",
"ts-jest": "28.0.7",
"typescript": "^4.5.3"
},
"private": true
}

20
searching-front/pages/404.tsx

@ -0,0 +1,20 @@
import Head from "next/head"
import { ErrorComponent } from "@blitzjs/next"
// ------------------------------------------------------
// This page is rendered if a route match is not found
// ------------------------------------------------------
export default function Page404() {
const statusCode = 404
const title = "This page could not be found"
return (
<>
<Head>
<title>
{statusCode}: {title}
</title>
</Head>
<ErrorComponent statusCode={statusCode} title={title} />
</>
)
}

41
searching-front/pages/_app.tsx

@ -0,0 +1,41 @@
import { ErrorFallbackProps, ErrorComponent, ErrorBoundary, AppProps } from "@blitzjs/next"
import { AuthenticationError, AuthorizationError } from "blitz"
import React from "react"
import { withBlitz } from "app/blitz-client"
import "app/core/global.css"
import "app/core/variables.css"
import "app/core/variables.css"
import "app/core/lightTheme.css"
import "app/core/darkTheme.css"
import "app/i18n"
function RootErrorFallback({ error }: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
return <div>Error: You are not authenticated</div>
} else if (error instanceof AuthorizationError) {
return (
<ErrorComponent
statusCode={error.statusCode}
title="Sorry, you are not authorized to access this"
/>
)
} else {
return (
<ErrorComponent
statusCode={(error as any)?.statusCode || 400}
title={error.message || error.name}
/>
)
}
}
function MyApp({ Component, pageProps }: AppProps) {
return (
<ErrorBoundary FallbackComponent={RootErrorFallback}>
<Component {...pageProps} />
</ErrorBoundary>
)
}
export default withBlitz(MyApp)

23
searching-front/pages/_document.tsx

@ -0,0 +1,23 @@
import Document, { Html, Main, NextScript, Head } from "next/document"
class MyDocument extends Document {
// Only uncomment if you need to customize this behaviour
// static async getInitialProps(ctx: DocumentContext) {
// const initialProps = await Document.getInitialProps(ctx)
// return {...initialProps}
// }
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<div id="modal-root"></div>
</body>
</Html>
)
}
}
export default MyDocument

4
searching-front/pages/api/rpc/[[...blitz]].ts

@ -0,0 +1,4 @@
import { rpcHandler } from "@blitzjs/rpc"
import { api } from "app/blitz-server"
export default api(rpcHandler({ onError: (...args) =>console.log('FROM SERVER BLITZ', ...args) }))

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

Loading…
Cancel
Save