matthew
2 years ago
commit
213b9a4ebf
133 changed files with 37577 additions and 0 deletions
@ -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: |
@ -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 |
Binary file not shown.
@ -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 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
Binary file not shown.
@ -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 |
@ -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 |
@ -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 |
@ -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
|
@ -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 |
@ -0,0 +1,5 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
npx lint-staged |
||||||
|
npx pretty-quick --staged |
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
npx tsc |
||||||
|
npm run lint |
||||||
|
npm run test |
@ -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. |
@ -0,0 +1,10 @@ |
|||||||
|
.gitkeep |
||||||
|
.env* |
||||||
|
*.ico |
||||||
|
*.lock |
||||||
|
db/migrations |
||||||
|
.next |
||||||
|
.yarn |
||||||
|
.pnp.* |
||||||
|
node_modules |
||||||
|
README.md |
@ -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": [] |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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) |
@ -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,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); |
||||||
|
} |
@ -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 |
@ -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 |
@ -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 |
||||||
|
} |
||||||
|
) |
@ -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() |
||||||
|
}) |
||||||
|
}) |
@ -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 |
||||||
|
}) |
@ -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 |
||||||
|
}) |
@ -0,0 +1,5 @@ |
|||||||
|
import { Ctx } from "blitz" |
||||||
|
|
||||||
|
export default async function logout(_: any, ctx: Ctx) { |
||||||
|
return await ctx.session.$revoke() |
||||||
|
} |
@ -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 |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
@ -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 |
||||||
|
}) |
@ -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 |
||||||
|
}) |
@ -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, |
||||||
|
}) |
@ -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({})], |
||||||
|
}) |
@ -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), |
||||||
|
}) |
@ -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 |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export {default} from './Header' |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export { default } from "./Pagination" |
@ -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; |
||||||
|
} |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export {default} from './SearchForm' |
@ -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; |
||||||
|
} |
@ -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 |
@ -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; |
||||||
|
} */ |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export { default } from "./TonBrillian" |
@ -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% |
||||||
|
); |
||||||
|
} |
@ -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; |
@ -0,0 +1 @@ |
|||||||
|
export { default } from "./WebsiteCard" |
@ -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,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: {} }) |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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" |
||||||
|
} |
@ -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" |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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 |
@ -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 |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export {default} from './Main' |
@ -0,0 +1,8 @@ |
|||||||
|
.root { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
/* justify-content: center; */ |
||||||
|
align-items: center; |
||||||
|
flex: 1; |
||||||
|
margin-bottom: 200px; |
||||||
|
} |
@ -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 |
@ -0,0 +1 @@ |
|||||||
|
export {default} from './Search' |
@ -0,0 +1,9 @@ |
|||||||
|
.root { |
||||||
|
/* display: flex; */ |
||||||
|
/* flex-direction: column; |
||||||
|
justify-content: center; */ |
||||||
|
/* align-items: center; */ |
||||||
|
/* height: 100%; */ |
||||||
|
width: 100%; |
||||||
|
max-width: 604px; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
const en = { |
||||||
|
translation: { |
||||||
|
"search.placeholder": "Find your own ton", |
||||||
|
"search.button": "Search", |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export default en |
@ -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 |
@ -0,0 +1,8 @@ |
|||||||
|
const ru = { |
||||||
|
translation: { |
||||||
|
"search.placeholder": "Найди свой собственный TON", |
||||||
|
"search.button": "Найти", |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export default ru |
@ -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 |
||||||
|
} |
||||||
|
) |
@ -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; |
||||||
|
} |
||||||
|
); |
@ -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), |
||||||
|
} |
||||||
|
}) |
@ -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 |
||||||
|
|
||||||
|
} |
@ -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 |
@ -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; |
@ -0,0 +1,2 @@ |
|||||||
|
-- AlterTable |
||||||
|
ALTER TABLE "Domain" ALTER COLUMN "lastParse" DROP NOT NULL; |
@ -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"); |
@ -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"); |
@ -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" |
@ -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 |
||||||
|
} |
@ -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,0 +1,11 @@ |
|||||||
|
const nextJest = require("@blitzjs/next/jest") |
||||||
|
|
||||||
|
const createJestConfig = nextJest({ |
||||||
|
dir: "./", |
||||||
|
}) |
||||||
|
|
||||||
|
const customJestConfig = { |
||||||
|
testEnvironment: "jest-environment-jsdom", |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = createJestConfig(customJestConfig) |
@ -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) |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
@ -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.
|
@ -0,0 +1,9 @@ |
|||||||
|
// @ts-check
|
||||||
|
const { withBlitz } = require("@blitzjs/next") |
||||||
|
|
||||||
|
/** |
||||||
|
* @type {import('@blitzjs/next').BlitzConfig} |
||||||
|
**/ |
||||||
|
const config = {} |
||||||
|
|
||||||
|
module.exports = withBlitz(config) |
File diff suppressed because it is too large
Load Diff
@ -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 |
||||||
|
} |
@ -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} /> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
@ -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) |
@ -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 |
@ -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…
Reference in new issue