mirror of
https://github.com/Findus23/HNReader.git
synced 2024-09-10 04:53:45 +02:00
initial version
This commit is contained in:
commit
0c4bb83b98
38 changed files with 5243 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea/
|
||||
__pycache__/
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Lukas Winkler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# HNReader
|
||||
|
||||
A Hacker News reader optimized for tablets.
|
||||
|
||||
strongly inspired by and based on [hn.premii.com](https://hn.premii.com) by
|
||||
Dharmesh Patel (licensed under the MIT license)
|
1
config.py
Normal file
1
config.py
Normal file
|
@ -0,0 +1 @@
|
|||
user_agent = "HNClient (in development)"
|
3
hnapi/README.md
Normal file
3
hnapi/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## Python client to HackerNews API
|
||||
|
||||
strongly inspired by [haxor](https://github.com/avinassh/haxor) created by Avinash Sajjanshetty (MIT licenced)
|
74
hnapi/__init__.py
Normal file
74
hnapi/__init__.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from redis import Redis
|
||||
from requests import Session
|
||||
|
||||
API_BASEURL = "https://hacker-news.firebaseio.com/v0/"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
id: int
|
||||
type: str
|
||||
by: str
|
||||
time: int
|
||||
url: str
|
||||
score: int
|
||||
title: str
|
||||
# parts: ??
|
||||
deleted: bool = None
|
||||
text: str = None # HTML
|
||||
dead: bool = None
|
||||
parent: int = None
|
||||
poll: int = None
|
||||
kids: List[int] = None
|
||||
descendants: int = None
|
||||
|
||||
|
||||
class HNClient:
|
||||
def __init__(self, requests_session: Session, redis: Redis):
|
||||
self.s = requests_session
|
||||
self.r = redis
|
||||
|
||||
def get_item(self, item_id: int, remove_kids=True):
|
||||
key = f"hnclient_item_{item_id}"
|
||||
cache = self.r.get(key)
|
||||
if cache:
|
||||
return json.loads(cache)
|
||||
url = f"{API_BASEURL}item/{item_id}.json"
|
||||
response = self.s.get(url)
|
||||
response.raise_for_status()
|
||||
item = response.json()
|
||||
self.r.set(key, response.text, ex=60 * 15)
|
||||
if "kids" in item and remove_kids:
|
||||
del item["kids"]
|
||||
return item
|
||||
|
||||
def get_full_item(self, item_id: int):
|
||||
item = self.get_item(item_id, remove_kids=False)
|
||||
if "kids" in item:
|
||||
kids = list(self.get_items_by_id(item["kids"], full=True))
|
||||
item["kids"] = kids
|
||||
return item
|
||||
|
||||
def get_items_by_id(self, ids: List[int], full=False):
|
||||
for id in ids:
|
||||
if full:
|
||||
yield self.get_full_item(id)
|
||||
else:
|
||||
yield self.get_item(id)
|
||||
|
||||
def get_stories(self, page: str, limit=25, offset=0):
|
||||
key = f"hnclient_stories_{page}_{limit}"
|
||||
cached = self.r.get(key)
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
url = f"{API_BASEURL}{page}.json"
|
||||
response = self.s.get(url)
|
||||
response.raise_for_status()
|
||||
stories = response.json()[offset:limit]
|
||||
full_stories = list(self.get_items_by_id(stories))
|
||||
self.r.set(key, json.dumps(full_stories), ex=60 * 15)
|
||||
return full_stories
|
290
poetry.lock
generated
Normal file
290
poetry.lock
generated
Normal file
|
@ -0,0 +1,290 @@
|
|||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2020.12.5"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "4.0.0"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "7.1.2"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "1.1.2"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=5.1"
|
||||
itsdangerous = ">=0.24"
|
||||
Jinja2 = ">=2.10.1"
|
||||
Werkzeug = ">=0.15"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
|
||||
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "hiredis"
|
||||
version = "2.0.0"
|
||||
description = "Python wrapper for hiredis"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "2.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "1.1.0"
|
||||
description = "Various helpers to pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "2.11.3"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "1.1.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "3.5.3"
|
||||
description = "Python client for Redis key-value store"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
hiredis = ["hiredis (>=0.1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.25.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<5"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.4"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "1.0.1"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "c20d734d8ba300fc5f5d191c7f1091c1bca7e2ae96a4dbf681e8e7ee90d0973f"
|
||||
|
||||
[metadata.files]
|
||||
certifi = [
|
||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
|
||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
]
|
||||
flask = [
|
||||
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
|
||||
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
|
||||
]
|
||||
hiredis = [
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"},
|
||||
{file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"},
|
||||
{file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"},
|
||||
{file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"},
|
||||
{file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"},
|
||||
{file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"},
|
||||
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"},
|
||||
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"},
|
||||
{file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"},
|
||||
{file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"},
|
||||
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"},
|
||||
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"},
|
||||
{file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"},
|
||||
{file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
|
||||
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
|
||||
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
|
||||
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||
]
|
||||
redis = [
|
||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
|
||||
{file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
|
||||
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
|
||||
]
|
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[tool.poetry]
|
||||
name = "hnreader"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Lukas Winkler <git@lw1.at>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
Flask = "^1.1.2"
|
||||
requests = "^2.25.1"
|
||||
redis = "^3.5.3"
|
||||
hiredis = "^2.0.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
1
reader/.gitignore
vendored
Normal file
1
reader/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
vendor/
|
21
reader/__init__.py
Normal file
21
reader/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from redis import Redis
|
||||
from requests import Session
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, url: str, requests_session: Session, redis: Redis):
|
||||
self.url = url
|
||||
self.s = requests_session
|
||||
self.r = redis
|
||||
|
||||
def readable_html(self):
|
||||
php_file = Path(__file__).parent / "reader.php"
|
||||
output = subprocess.run(["php", str(php_file)], input=self.url.encode(), capture_output=True)
|
||||
data = json.loads(output.stdout.decode())
|
||||
del data["headers"]
|
||||
del data["summary"]
|
||||
return json.dumps(data)
|
7
reader/composer.json
Normal file
7
reader/composer.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"require": {
|
||||
"j0k3r/graby": "^2.2",
|
||||
"ext-json": "*",
|
||||
"php-http/guzzle6-adapter": "^2.0"
|
||||
}
|
||||
}
|
2415
reader/composer.lock
generated
Normal file
2415
reader/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
reader/reader.php
Normal file
13
reader/reader.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
use Graby\Graby;
|
||||
|
||||
$url = file_get_contents("php://stdin");
|
||||
|
||||
|
||||
$graby = new Graby();
|
||||
|
||||
$result = $graby->fetchContent($url);
|
||||
echo json_encode($result);
|
43
server.py
Normal file
43
server.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import requests
|
||||
from flask import Flask, jsonify, make_response
|
||||
from redis import Redis
|
||||
|
||||
from config import user_agent
|
||||
from hnapi import HNClient
|
||||
from reader import Reader
|
||||
|
||||
app = Flask(__name__)
|
||||
r = Redis()
|
||||
s = requests.session()
|
||||
s.headers.update({'User-Agent': user_agent})
|
||||
|
||||
api = HNClient(s, r)
|
||||
|
||||
|
||||
@app.route("/api/topstories")
|
||||
def topstories():
|
||||
return jsonify(api.get_stories("topstories"))
|
||||
|
||||
|
||||
@app.route("/api/item/<int:item_id>")
|
||||
def item(item_id):
|
||||
return jsonify(api.get_full_item(item_id))
|
||||
|
||||
|
||||
@app.route("/api/read/<int:item_id>")
|
||||
def read(item_id):
|
||||
item = api.get_item(item_id)
|
||||
if "url" not in item:
|
||||
return "Url not found", 404
|
||||
key = f"hnclient_read_{item_id}"
|
||||
|
||||
cache = r.get(key)
|
||||
if cache:
|
||||
response = make_response(cache)
|
||||
else:
|
||||
readable = Reader(item["url"], s, r)
|
||||
output = readable.readable_html()
|
||||
r.set(key, output, ex=60 * 60 * 24)
|
||||
response = make_response(output)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
2
web/.gitignore
vendored
Normal file
2
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
build
|
19
web/package.json
Normal file
19
web/package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "hnclient-web",
|
||||
"scripts": {
|
||||
"serve": "snowpack dev",
|
||||
"build": "snowpack build"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.0.11",
|
||||
"vue-router": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@snowpack/plugin-sass": "^1.4.0",
|
||||
"@snowpack/plugin-vue": "^2.4.0",
|
||||
"@types/snowpack-env": "^2.3.3",
|
||||
"http-proxy": "^1.18.1",
|
||||
"snowpack": "^3.2.2",
|
||||
"snowpack-plugin-replace": "^1.0.4"
|
||||
}
|
||||
}
|
15
web/public/index.html
Normal file
15
web/public/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Web site created using create-snowpack-app" />
|
||||
<title>Snowpack App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
57
web/snowpack.config.js
Normal file
57
web/snowpack.config.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
const httpProxy = require('http-proxy');
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
target: 'http://localhost:5000',
|
||||
timeout: 30 * 1000
|
||||
});
|
||||
|
||||
/** @type {import("snowpack").SnowpackUserConfig } */
|
||||
module.exports = {
|
||||
mount: {
|
||||
public: {url: '/', static: true},
|
||||
src: {url: '/dist'},
|
||||
},
|
||||
plugins: [
|
||||
'@snowpack/plugin-sass',
|
||||
'@snowpack/plugin-vue',
|
||||
[
|
||||
'snowpack-plugin-replace',
|
||||
{
|
||||
list: [
|
||||
{
|
||||
from: '__VUE_OPTIONS_API__',
|
||||
to: 'false'
|
||||
},
|
||||
{
|
||||
from: '__VUE_PROD_DEVTOOLS__',
|
||||
to: 'true'
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
],
|
||||
routes: [
|
||||
/* Enable an SPA Fallback in development: */
|
||||
{
|
||||
src: '/api/.*',
|
||||
dest: (req, res) => {
|
||||
proxy.web(req, res, e => console.log(e));
|
||||
},
|
||||
},
|
||||
{"match": "routes", "src": ".*", "dest": "/index.html"},
|
||||
],
|
||||
optimize: {
|
||||
/* Example: Bundle your final build: */
|
||||
bundle: true,
|
||||
minify: true,
|
||||
target: "es2018"
|
||||
},
|
||||
packageOptions: {
|
||||
/* ... */
|
||||
},
|
||||
devOptions: {
|
||||
/* ... */
|
||||
},
|
||||
buildOptions: {
|
||||
/* ... */
|
||||
},
|
||||
};
|
33
web/src/App.vue
Normal file
33
web/src/App.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div id="mainwrapper">
|
||||
<section id="sidebar">
|
||||
<div id="sidebar-header">
|
||||
<router-link :to="{name: 'about'}">i</router-link>
|
||||
</div>
|
||||
<Stories></Stories>
|
||||
</section>
|
||||
<section id="mainpane">
|
||||
<div id="mainpane-header"></div>
|
||||
|
||||
<router-view></router-view>
|
||||
<header>
|
||||
<!-- <router-link :to="{name:'stories'}">Go to Home</router-link>-->
|
||||
<!-- <router-link :to="{name:'about'}">Go to About</router-link>-->
|
||||
</header>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import Stories from "./views/Stories.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {Stories},
|
||||
data() {
|
||||
return {
|
||||
message: "ffd Vue"
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
47
web/src/components/Comment.vue
Normal file
47
web/src/components/Comment.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div v-if="!item.deleted">
|
||||
<div :class="{comment:true,fromauthor:item.by===originalAuthor}" v-if="!firstLayer">
|
||||
<div class="comment-header" @click="toogleCollapse">
|
||||
<div class="author">{{ item.by }}</div>
|
||||
<div class="time">{{ dateToText(item.time) }}</div>
|
||||
</div>
|
||||
<div class="text" v-html="item.text" v-if="!collapsed"></div>
|
||||
</div>
|
||||
<div v-for="kid in item.kids" :key="kid.id" class="kids" v-if="!collapsed">
|
||||
<comment :item="kid" :original-author="originalAuthor"></comment>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {Item} from "../interfaces";
|
||||
import {dateToText} from "../utils";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: "Comment",
|
||||
props: {
|
||||
item: {} as PropType<Item>,
|
||||
originalAuthor: String,
|
||||
firstLayer: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toogleCollapse(): void {
|
||||
console.log("toggle")
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
dateToText(num: number): string {
|
||||
return dateToText(num)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
25
web/src/components/Header.vue
Normal file
25
web/src/components/Header.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="item-header">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<a :href="item.url">{{ item.url }}</a>
|
||||
<a :href="hnURL">{{ hnURL }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {Item} from "../interfaces";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
props: {
|
||||
item: {} as PropType<Item>,
|
||||
},
|
||||
computed: {
|
||||
hnURL(): string {
|
||||
return "https://news.ycombinator.com/item?id=" + this.item.id
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
1
web/src/global.d.ts
vendored
Normal file
1
web/src/global.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module '*.scss';
|
22
web/src/index.ts
Normal file
22
web/src/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {createApp} from 'vue';
|
||||
import App from './App.vue';
|
||||
import {router} from "./router";
|
||||
import './style/main.scss'
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
|
||||
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
|
||||
// Learn more: https://www.snowpack.dev/concepts/hot-module-replacement
|
||||
// @ts-ignore
|
||||
if (import.meta.hot) {
|
||||
// @ts-ignore
|
||||
import.meta.hot.accept();
|
||||
// @ts-ignore
|
||||
import.meta.hot.dispose(() => {
|
||||
app.unmount();
|
||||
});
|
||||
}
|
30
web/src/interfaces.ts
Normal file
30
web/src/interfaces.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export interface Item {
|
||||
id: number
|
||||
type: string
|
||||
by: string
|
||||
time: number
|
||||
url: string
|
||||
score: number
|
||||
title: string
|
||||
// parts: ??
|
||||
deleted: boolean
|
||||
|
||||
text?: string // HTML
|
||||
dead?: boolean
|
||||
parent?: number
|
||||
poll?: number
|
||||
kids?: Item[]
|
||||
descendants?: number
|
||||
}
|
||||
|
||||
export interface ReaderData {
|
||||
status: number
|
||||
html: string
|
||||
title: string
|
||||
language: string
|
||||
date: any //TODO
|
||||
authors: string[]
|
||||
url: string
|
||||
image?: string
|
||||
native_ad: boolean
|
||||
}
|
14
web/src/router.ts
Normal file
14
web/src/router.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {createRouter, createWebHistory} from "vue-router";
|
||||
import Stories from './views/Stories.vue';
|
||||
import About from './views/About.vue';
|
||||
import Comments from "./views/Comments.vue";
|
||||
import Reader from "./views/Reader.vue";
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{path: '/', name: "about", component: About},
|
||||
{path: "/comments/:item", name: "comments", component: Comments, props: true},
|
||||
{path: "/read/:item", name: "reader", component: Reader, props: true},
|
||||
]
|
||||
})
|
61
web/src/style/_layout.scss
Normal file
61
web/src/style/_layout.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
#app, #mainwrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar .stories, #mainpane .comments, #mainpane .reader {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - #{$topNavBarHeight});
|
||||
}
|
||||
|
||||
#mainpane .reader {
|
||||
height: calc(100% - #{$topNavBarHeight + 30px});
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
flex-basis: 400px;
|
||||
max-width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#mainpane {
|
||||
max-width: calc(100% - 400px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#sidebar-header, #mainpane-header {
|
||||
height: $topNavBarHeight;
|
||||
background: $color;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.item-header {
|
||||
background: $color;
|
||||
color: white;
|
||||
padding: 15px 10px;
|
||||
padding-top: 5px;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
9
web/src/style/_reboot.scss
Normal file
9
web/src/style/_reboot.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p, pre {
|
||||
margin-top: 0;
|
||||
}
|
10
web/src/style/_variables.scss
Normal file
10
web/src/style/_variables.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
$color: #ff6600;
|
||||
$textColor: #222;
|
||||
$lightTextColor: #888;
|
||||
$backgroundColorLighter: rgb(255, 252, 250);
|
||||
$backgroundColorLighterDark: #fcf9f7;
|
||||
|
||||
$backgroundColorLight: rgb(255, 240, 230);
|
||||
|
||||
|
||||
$topNavBarHeight: 45px;
|
143
web/src/style/main.scss
Normal file
143
web/src/style/main.scss
Normal file
|
@ -0,0 +1,143 @@
|
|||
@import "variables";
|
||||
@import "reboot";
|
||||
@import "layout";
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
overflow-y: auto;
|
||||
|
||||
&.debug {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stories {
|
||||
background: $backgroundColorLighter;
|
||||
|
||||
.storywrapper {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
border-bottom: .5px solid rgba(0, 0, 0, .07);
|
||||
|
||||
&.active {
|
||||
box-shadow: 3px 0 0 $color inset;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.story {
|
||||
//width: 100%;
|
||||
max-width: calc(100% - 50px);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 15px;
|
||||
|
||||
div {
|
||||
color: $lightTextColor;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 5px;
|
||||
color: $textColor;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.link {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.storycomments {
|
||||
width: 50px;
|
||||
padding: 12px 0;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
|
||||
background: $backgroundColorLighterDark;
|
||||
|
||||
.numcomments {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.points {
|
||||
background: lightgray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comments {
|
||||
background: $backgroundColorLighter;
|
||||
|
||||
.kids .kids {
|
||||
border-left: 14px solid rgba(0, 0, 0, .05);
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
border-top: .5px solid rgba(0, 0, 0, .09);
|
||||
|
||||
.text {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
&.fromauthor {
|
||||
box-shadow: 3px 0 0 $color inset;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
||||
.author {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.reader {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.reader-html {
|
||||
font-family: serif;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
}
|
33
web/src/utils.ts
Normal file
33
web/src/utils.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
const MINUTE = 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = HOUR * 24
|
||||
const WEEK = DAY * 7
|
||||
const MONTH = DAY * 30
|
||||
|
||||
export function absround(num: number): number {
|
||||
return Math.round(Math.abs(num))
|
||||
}
|
||||
|
||||
|
||||
export function dateToText(timestamp: number): string {
|
||||
// inspired by https://github.com/premii/hn/blob/76ba1721f0ca5dead6fe9ac62afdf42911ebc244/a/js/helper.js#L292
|
||||
const now = Date.now() / 1000
|
||||
const diff = now - timestamp
|
||||
if (diff <= 60) {
|
||||
return diff / 60 + " seconds"
|
||||
}
|
||||
if (diff < HOUR) {
|
||||
const value = absround(diff / MINUTE)
|
||||
return value + " min" + (value === 1 ? "" : "s")
|
||||
}
|
||||
if (diff < DAY) {
|
||||
const value = absround(diff / HOUR)
|
||||
return value + " hr" + (value === 1 ? "" : "s")
|
||||
}
|
||||
if (diff < MONTH) {
|
||||
const value = absround(diff / DAY)
|
||||
return value + " day" + (value === 1 ? "" : "s")
|
||||
}
|
||||
const date = new Date(timestamp * 1000)
|
||||
return date.toLocaleString()
|
||||
}
|
14
web/src/views/About.vue
Normal file
14
web/src/views/About.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
ABOUT
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "About"
|
||||
})
|
||||
</script>
|
||||
|
61
web/src/views/Comments.vue
Normal file
61
web/src/views/Comments.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="comments">
|
||||
<Header :item="story"></Header>
|
||||
<div v-if="loading">LOADING</div>
|
||||
<comment :item="story" :original-author="story.by" :first-layer="true"></comment>
|
||||
<!-- <pre class="debug"><code>{{ story }}</code></pre>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {Item} from "../interfaces";
|
||||
import Comment from "../components/Comment.vue";
|
||||
import Header from "../components/Header.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Comments",
|
||||
props: {
|
||||
item: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
story: {} as Item,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadComments(id?: string): void {
|
||||
if (typeof id === "undefined") {
|
||||
id = this.item
|
||||
}
|
||||
this.loading = true
|
||||
fetch("/api/item/" + id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
}
|
||||
return Promise.reject(response)
|
||||
})
|
||||
.then(data => (this.story = data))
|
||||
.then(a => {
|
||||
this.loading = false;
|
||||
document.title = this.story.title
|
||||
})
|
||||
|
||||
}
|
||||
},
|
||||
mounted(): void {
|
||||
this.loadComments()
|
||||
},
|
||||
components: {
|
||||
Header,
|
||||
"comment": Comment
|
||||
},
|
||||
watch: {
|
||||
"$route.params.item": function (id: string) {
|
||||
this.loadComments(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
73
web/src/views/Reader.vue
Normal file
73
web/src/views/Reader.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="reader">
|
||||
<div v-if="loading">loading article</div>
|
||||
<h1>{{ readerData.title }}</h1>
|
||||
<div>{{ prettyDate }}</div>
|
||||
<div>{{ prettyAuthors }}</div>
|
||||
<div class="reader-html" v-html="readerData.html"></div>
|
||||
<pre class="debug"><code>{{ readerData }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {ReaderData} from "../interfaces";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Reader",
|
||||
props: {
|
||||
item: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
readerData: {} as ReaderData,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadReader(id?: string): void {
|
||||
if (typeof id === "undefined") {
|
||||
id = this.item
|
||||
}
|
||||
this.loading = true
|
||||
fetch("/api/read/" + id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
}
|
||||
return Promise.reject(response)
|
||||
})
|
||||
.then(data => (this.readerData = data))
|
||||
.then(a => {
|
||||
this.loading = false;
|
||||
document.title = this.readerData.title
|
||||
})
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadReader()
|
||||
},
|
||||
computed: {
|
||||
prettyDate(): string {
|
||||
if (!this.readerData) {
|
||||
return ""
|
||||
}
|
||||
const date = new Date(this.readerData.date)
|
||||
return date.toLocaleString()
|
||||
},
|
||||
prettyAuthors(): string {
|
||||
if (!this.readerData || !this.readerData.authors) {
|
||||
return ""
|
||||
}
|
||||
return this.readerData.authors.join(", ")
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"$route.params.item": function (id: string) {
|
||||
this.loadReader(id)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
52
web/src/views/Stories.vue
Normal file
52
web/src/views/Stories.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="stories">
|
||||
<div v-for="story in stories" :key="story.id" :class="{storywrapper:true, active:isActiveStory(story)}">
|
||||
<router-link :to="{name:'reader',params:{item:story.id}}" class="story">
|
||||
<h3>{{ story.title }}</h3>
|
||||
<div class="info">{{ story.by }} {{ dateToText(story.time) }}</div>
|
||||
<div class="link">{{ story.url }}</div>
|
||||
<div v-if="story.type==='job'">Job</div>
|
||||
</router-link>
|
||||
<router-link :to="{name:'comments',params:{item:story.id}}" class="storycomments">
|
||||
<div class="numcomments">{{ story.descendants }}</div>
|
||||
<div class="points">{{ story.score }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<pre class="debug"><code>{{ stories }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {Item} from "../interfaces";
|
||||
import {dateToText} from "../utils";
|
||||
export default defineComponent({
|
||||
name: "Stories",
|
||||
data() {
|
||||
return {
|
||||
stories: [] as Item[]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadStory(): void {
|
||||
fetch("/api/topstories")
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
}
|
||||
return Promise.reject(response)
|
||||
})
|
||||
.then(data => (this.stories = data))
|
||||
},
|
||||
isActiveStory(item: Item): boolean {
|
||||
return this.$route.params.item === item.id.toString()
|
||||
},
|
||||
dateToText(timestamp: number): string {
|
||||
return dateToText(timestamp)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadStory()
|
||||
},
|
||||
})
|
||||
</script>
|
10
web/tsconfig.json
Normal file
10
web/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
// this enables stricter inference for data properties on `this`
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
6
web/urls.md
Normal file
6
web/urls.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
https://github.com/premii/hn
|
||||
http://127.0.0.1:5000/api/item/26709159
|
||||
http://localhost:8080/item/26709159
|
||||
https://v3.vuejs.org/guide/typescript-support.html#annotating-props
|
||||
https://github.com/vuejs/vue-hackernews-2.0/
|
||||
https://hn.premii.com/
|
832
web/yarn-error.log
Normal file
832
web/yarn-error.log
Normal file
|
@ -0,0 +1,832 @@
|
|||
Arguments:
|
||||
/usr/bin/node /home/lukas/.npm-global/bin/yarn install
|
||||
|
||||
PATH:
|
||||
/home/lukas/.poetry/bin:/home/lukas/.cargo/bin:/home/lukas/.npm-global/bin:/home/lukas/.local/bin:/home/lukas/PycharmProjects/hnreader/web/node_modules/.bin:/home/lukas/.poetry/bin:/home/lukas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/games:/home/lukas/.config/composer/vendor/bin:/home/lukas/git/fzf/bin
|
||||
|
||||
Yarn version:
|
||||
1.22.10
|
||||
|
||||
Node version:
|
||||
14.16.1
|
||||
|
||||
Platform:
|
||||
linux x64
|
||||
|
||||
Trace:
|
||||
SyntaxError: /home/lukas/PycharmProjects/hnreader/web/package.json: Unexpected token } in JSON at position 198
|
||||
at JSON.parse (<anonymous>)
|
||||
at /home/lukas/.config/yarn/global/node_modules/yarn/lib/cli.js:1625:59
|
||||
at Generator.next (<anonymous>)
|
||||
at step (/home/lukas/.config/yarn/global/node_modules/yarn/lib/cli.js:310:30)
|
||||
at /home/lukas/.config/yarn/global/node_modules/yarn/lib/cli.js:321:13
|
||||
|
||||
npm manifest:
|
||||
{
|
||||
"name": "hnclient-web",
|
||||
"scripts": {
|
||||
"serve": "snowpack dev",
|
||||
"build": "snowpack build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"vue": "^3.0.11",
|
||||
"vue-router": "4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@snowpack/plugin-sass": "^1.4.0",
|
||||
"@snowpack/plugin-vue": "^2.4.0",
|
||||
"@types/snowpack-env": "^2.3.3",
|
||||
"http-proxy": "^1.18.1",
|
||||
"snowpack": "^3.2.2",
|
||||
"snowpack-plugin-replace": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
yarn manifest:
|
||||
No manifest
|
||||
|
||||
Lockfile:
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/helper-validator-identifier@^7.12.11":
|
||||
version "7.12.11"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
|
||||
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
|
||||
|
||||
"@babel/parser@^7.12.0", "@babel/parser@^7.13.9":
|
||||
version "7.13.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df"
|
||||
integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==
|
||||
|
||||
"@babel/types@^7.12.0", "@babel/types@^7.13.0":
|
||||
version "7.13.14"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
|
||||
integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.12.11"
|
||||
lodash "^4.17.19"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@snowpack/plugin-sass@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@snowpack/plugin-sass/-/plugin-sass-1.4.0.tgz#faccd5827e4badae3f5ba76cb8f947f15d3faa81"
|
||||
integrity sha512-Hzz/TYt4IKcjrInv+FyujLohtJHadZCUdz5nnfh1N7MwplHFmxgLuKiT8tsiafHFAGsuR+4ZpFTqLeSyQTHAhQ==
|
||||
dependencies:
|
||||
execa "^5.0.0"
|
||||
find-up "^5.0.0"
|
||||
npm-run-path "^4.0.1"
|
||||
sass "^1.3.0"
|
||||
|
||||
"@snowpack/plugin-vue@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@snowpack/plugin-vue/-/plugin-vue-2.4.0.tgz#f7c12831f7a0afba21556519b604165d8e62b4fb"
|
||||
integrity sha512-46EHyTvd7Qo38ShTG7paV/x4b0PRTgOtBegjFL/xyzV5sETeNDuNPfXbT9LaFoQhiW0Q/8eq/peWQlls0l5+uw==
|
||||
dependencies:
|
||||
"@vue/compiler-sfc" "^3.0.10"
|
||||
hash-sum "^2.0.0"
|
||||
|
||||
"@types/snowpack-env@^2.3.3":
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/snowpack-env/-/snowpack-env-2.3.3.tgz#d2dfb1fb8557aa8bb517606d5dfa249cc861c3ff"
|
||||
integrity sha512-riJuu2fR3qhBfpWJtqQtNwYJFvquiXfqdprXvZjSNmscnZbIVyHoM49ZVEM1bciKM1mWOCdjXymOYHyGh2WLtg==
|
||||
|
||||
"@vue/compiler-core@3.0.11":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.11.tgz#5ef579e46d7b336b8735228758d1c2c505aae69a"
|
||||
integrity sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.12.0"
|
||||
"@babel/types" "^7.12.0"
|
||||
"@vue/shared" "3.0.11"
|
||||
estree-walker "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-dom@3.0.11":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz#b15fc1c909371fd671746020ba55b5dab4a730ee"
|
||||
integrity sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.0.11"
|
||||
"@vue/shared" "3.0.11"
|
||||
|
||||
"@vue/compiler-sfc@^3.0.10":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.11.tgz#cd8ca2154b88967b521f5ad3b10f5f8b6b665679"
|
||||
integrity sha512-7fNiZuCecRleiyVGUWNa6pn8fB2fnuJU+3AGjbjl7r1P5wBivfl02H4pG+2aJP5gh2u+0wXov1W38tfWOphsXw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.13.9"
|
||||
"@babel/types" "^7.13.0"
|
||||