initial commit
This commit is contained in:
parent
d5eb740da9
commit
b8085ee62b
65
README.md
Normal file
65
README.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# HackerNews Fullstack
|
||||||
|
This project is a custom build to show a working implementation of FLASK with VUE.js
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
Backend
|
||||||
|
- Use the unofficial Hackernews REST API to fetch news: https://github.com/HackerNews/API (Only use the API, not a library)
|
||||||
|
- Implement a single REST endpoint using Python (e.g. with Flask) which returns an improved representation of the API response
|
||||||
|
- Provide a query parameter to select a story category
|
||||||
|
- Provide a query parameter to limit the story count
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
- Create a VUE.js project using the VUE cli
|
||||||
|
- Build a Master-Detail view which uses your Python API endpoint using VUE.js
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
The server is seperated into Front- and Backend.
|
||||||
|
The Frontend will be shown to the user and present everything as a Single-Page-Application. The inital call of the webpage will get the bundled frontend into the clients browser.
|
||||||
|
The Backend provides all vital data, like post overviews and rankings, and can communicate with the Frontend.
|
||||||
|
|
||||||
|
### Develping and Running
|
||||||
|
1. Setup a virtual environment `python3 -m venv venv`
|
||||||
|
2. activate your virtual environment `source venv/bin/activate`
|
||||||
|
3. Run `pip3 install -r requirements`
|
||||||
|
4. Run `export FLASK_APP=wsgi:app`
|
||||||
|
5. Run `flask run`
|
||||||
|
|
||||||
|
To deactivate your virtual environment run `deactivate` in your Terminal.
|
||||||
|
|
||||||
|
### Build the Frontend
|
||||||
|
In order to rebuild the web app (Vue)
|
||||||
|
1. Navigate to the `frontend` folder
|
||||||
|
2. Run `yarn install`
|
||||||
|
3. while developing `yarn dev`
|
||||||
|
4. to build for deployment `yarn build`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## About HackerNews
|
||||||
|
HackerNews got a public API at https://hacker-news.firebaseio.com for getting their
|
||||||
|
- Items (`/v0/item/<id>`) like
|
||||||
|
- Stories
|
||||||
|
- Comments
|
||||||
|
- Jobs
|
||||||
|
- Polls and Pollopts
|
||||||
|
- Users (`/v0/user/<username>`)
|
||||||
|
- and spezial live Data
|
||||||
|
- current largest item id (`/v0/maxitem`)
|
||||||
|
- top stories (`/v0/topstories`) (also contains jobs)
|
||||||
|
- new stories (`/v0/newstories`)
|
||||||
|
- best stories (`/v0/beststories`)
|
||||||
|
- ask stories (`/v0/askstories`)
|
||||||
|
- show stories(`/v0/showstories`)
|
||||||
|
- job stories (`/v0/jobstories`)
|
||||||
|
- changed items and profiles (`/v0/updates`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# links and resources
|
||||||
|
- https://github.com/HackerNews/API
|
||||||
|
- https://vuejs.org/
|
||||||
|
- https://github.com/jrbarhydt/FlaskWebAPI
|
||||||
|
- https://dev.to/michaelbukachi/flask-vue-js-integration-tutorial-2g90
|
||||||
|
- https://towardsdatascience.com/creating-a-beautiful-web-api-in-python-6415a40789af
|
||||||
|
- https://gist.github.com/jamescalam/0b309d275999f9df26fa063602753f73
|
||||||
|
- https://www.stackhawk.com/blog/vue-cors-guide-what-it-is-and-how-to-enable-it/
|
97
app.py
Normal file
97
app.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
from flask import Flask, render_template, jsonify, Response, request
|
||||||
|
import requests
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
# configuration
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
# instantiate the app
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
|
||||||
|
# enable CORS
|
||||||
|
CORS(app, resources={r'/*': {'origins': '*'}})
|
||||||
|
|
||||||
|
# sanity check route
|
||||||
|
@app.route('/ping', methods=['GET'])
|
||||||
|
def ping_pong():
|
||||||
|
return jsonify('Pong!')
|
||||||
|
|
||||||
|
# get list of items
|
||||||
|
@app.route("/items", methods=['GET'])
|
||||||
|
def getItems():
|
||||||
|
"""
|
||||||
|
@return items:json
|
||||||
|
"""
|
||||||
|
# get latest item id
|
||||||
|
maxId = requests.get("https://hacker-news.firebaseio.com/v0/maxitem.json")
|
||||||
|
maxId = int(maxId.text)
|
||||||
|
itemlist = []
|
||||||
|
for count in range(10):
|
||||||
|
newdata = requests.get("https://hacker-news.firebaseio.com/v0/item/" + str(maxId) + ".json")
|
||||||
|
# if any error occurrs, jump to next item
|
||||||
|
if(newdata.status_code != 200):
|
||||||
|
continue
|
||||||
|
itemlist.append(newdata.text)
|
||||||
|
maxId-=1
|
||||||
|
response_object = {'status': 200}
|
||||||
|
response_object["items"] = itemlist
|
||||||
|
return jsonify(response_object)
|
||||||
|
|
||||||
|
@app.route("/items", methods=["POST"])
|
||||||
|
def getSpezialItems():
|
||||||
|
"""
|
||||||
|
@param category:string
|
||||||
|
@param count:integer
|
||||||
|
@return items:json
|
||||||
|
"""
|
||||||
|
category = str(request.form["category"])
|
||||||
|
count = int(request.form["count"])
|
||||||
|
println(category)
|
||||||
|
println(count)
|
||||||
|
|
||||||
|
# get latest item id
|
||||||
|
maxId = requests.get("https://hacker-news.firebaseio.com/v0/maxitem.json")
|
||||||
|
maxId = int(maxId.text)
|
||||||
|
itemlist = []
|
||||||
|
# get count amount of items of certain category
|
||||||
|
counter = 0
|
||||||
|
if count <= 0 | count >= 50:
|
||||||
|
count = 10
|
||||||
|
while counter < count:
|
||||||
|
newdata = get("https://hacker-news.firebaseio.com/v0/item/" + str(maxId) + ".json")
|
||||||
|
# if any error occurrs, jump to next item
|
||||||
|
if(newdata.status_code != 200):
|
||||||
|
continue
|
||||||
|
# only if category matches it is pushed to the answer array
|
||||||
|
for dict in newdata.text:
|
||||||
|
if dict['category'] == category:
|
||||||
|
itemlist.append(newdata.text)
|
||||||
|
counter+=1
|
||||||
|
maxId-=1
|
||||||
|
response_object = {'status': 200}
|
||||||
|
response_object["items"] = itemlist
|
||||||
|
return jsonify(response_object)
|
||||||
|
|
||||||
|
# get single item by id
|
||||||
|
@app.route("/item/<itemId>", methods=["GET"])
|
||||||
|
def getItem(itemId):
|
||||||
|
"""
|
||||||
|
@param itemId:integer
|
||||||
|
@return item:json
|
||||||
|
"""
|
||||||
|
data = requests.get("https://hacker-news.firebaseio.com/v0/item/" + itemId + ".json")
|
||||||
|
if(data.status_code != 200):
|
||||||
|
return jsonify({
|
||||||
|
"status_code": 400,
|
||||||
|
"error": "Currently there is a problem with the HackerNews API"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
response_object = {'status': 200}
|
||||||
|
response_object["item"] = data.text
|
||||||
|
return jsonify(response_object)
|
||||||
|
|
||||||
|
# send single-page-app to client
|
||||||
|
@app.route("/")
|
||||||
|
def getIndex():
|
||||||
|
return render_template("index.html")
|
14
frontend/.eslintrc.cjs
Normal file
14
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/eslint-config-prettier"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"vue/setup-compiler-macros": true
|
||||||
|
}
|
||||||
|
}
|
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
35
frontend/README.md
Normal file
35
frontend/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# HackerNewsFullstack
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
16
frontend/index.html
Normal file
16
frontend/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HackerNews</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "hackernewsfullstack",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --port 5050",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.26.1",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"vue": "^3.2.31",
|
||||||
|
"vue-axios": "^3.4.1",
|
||||||
|
"vue-router": "^4.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
"@vitejs/plugin-vue": "^2.2.2",
|
||||||
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-plugin-vue": "^8.2.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"vite": "^2.8.4"
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
128
frontend/src/App.vue
Normal file
128
frontend/src/App.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink, RouterView } from "vue-router";
|
||||||
|
import TitleView from "@/components/TitleView.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="greetings">
|
||||||
|
<RouterLink to="/">
|
||||||
|
<h1 class="green">HackerNews Show</h1>
|
||||||
|
</RouterLink>
|
||||||
|
<h3>
|
||||||
|
This page is only a showcase, to use the unofficial HackerNews API
|
||||||
|
with Python and Vue.
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Look at the
|
||||||
|
<a target="_blank" href="https://github.com/HackerNews/API">
|
||||||
|
HackerNews API</a
|
||||||
|
>
|
||||||
|
used here.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Take a look at the
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/wieerwill/hackernewsfullstack"
|
||||||
|
>source code</a
|
||||||
|
>
|
||||||
|
of this project.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You don't get any news? Check the Ping tool, to verify you can connect
|
||||||
|
to the python backend: <RouterLink to="ping">PingPong</RouterLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<hr class="headerseperator" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "@/assets/base.css";
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerseperator {
|
||||||
|
display: block;
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.headerseperator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
padding-right: calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
header .wrapper {
|
||||||
|
display: flex;
|
||||||
|
place-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
74
frontend/src/assets/base.css
Normal file
74
frontend/src/assets/base.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition: color 0.5s, background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
68
frontend/src/components/ItemOverview.vue
Normal file
68
frontend/src/components/ItemOverview.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
item: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card" :class="parseditem.type">
|
||||||
|
<RouterLink :to="{ path: '/item/' + parseditem.id }">
|
||||||
|
<p v-if="parseditem.type == 'comment'" v-html="parseditem.text.slice(0, 150)" />
|
||||||
|
<h3 v-else>{{ parseditem.title }}</h3>
|
||||||
|
|
||||||
|
<h4>by {{ parseditem.by }}</h4>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
parseditem: JSON.parse(this.item),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
margin: 15px 5px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgb(41, 41, 41);
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgb(109, 109, 109);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story {
|
||||||
|
border: 1px solid rgb(95, 0, 0);
|
||||||
|
}
|
||||||
|
.story:hover {
|
||||||
|
border: 1px solid rgb(201, 0, 0);
|
||||||
|
}
|
||||||
|
.comment {
|
||||||
|
border: 1px solid rgb(0, 106, 106);
|
||||||
|
}
|
||||||
|
.comment:hover {
|
||||||
|
border: 1px solid rgb(0, 189, 189);
|
||||||
|
}
|
||||||
|
.job {
|
||||||
|
border: 1px solid rgb(106, 0, 106);
|
||||||
|
}
|
||||||
|
.job:hover {
|
||||||
|
border: 1px solid rgb(211, 0, 211);
|
||||||
|
}
|
||||||
|
.poll {
|
||||||
|
border: 1px solid rgb(106, 106, 0);
|
||||||
|
}
|
||||||
|
.poll:hover {
|
||||||
|
border: 1px solid rgb(216, 216, 0);
|
||||||
|
}
|
||||||
|
</style>
|
47
frontend/src/components/TitleView.vue
Normal file
47
frontend/src/components/TitleView.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">HackerNews Show</h1>
|
||||||
|
<h3>
|
||||||
|
This page is only a showcase, to use the unofficial HackerNews API with
|
||||||
|
Python and Vue.
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Look at the
|
||||||
|
<a target="_blank" href="https://github.com/HackerNews/API">
|
||||||
|
HackerNews API</a
|
||||||
|
>
|
||||||
|
used here.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Take a look at the
|
||||||
|
<a target="_blank" href="https://github.com/wieerwill/hackernewsfullstack"
|
||||||
|
>source code</a
|
||||||
|
>
|
||||||
|
of this project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
15
frontend/src/main.js
Normal file
15
frontend/src/main.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
createApp
|
||||||
|
} from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import * as Vue from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import VueAxios from 'vue-axios'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(VueAxios, axios)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
34
frontend/src/router/index.js
Normal file
34
frontend/src/router/index.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHistory
|
||||||
|
} from "vue-router"
|
||||||
|
import HomeView from "../views/HomeView.vue"
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(
|
||||||
|
import.meta.env.BASE_URL),
|
||||||
|
routes: [{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: HomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/item/:id",
|
||||||
|
name: "item",
|
||||||
|
// lazy-loaded when the route is visited
|
||||||
|
component: () => import("../views/ItemView.vue"),
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/item",
|
||||||
|
redirect: "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/ping",
|
||||||
|
name: "Ping",
|
||||||
|
component: () => import("../views/PingView.vue")
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
118
frontend/src/views/HomeView.vue
Normal file
118
frontend/src/views/HomeView.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script setup>
|
||||||
|
import ItemOverview from "@/components/ItemOverview.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<div>
|
||||||
|
<form v-on:submit.prevent="submitForm()">
|
||||||
|
<select v-model="form.category">
|
||||||
|
<option disabled value="">select category</option>
|
||||||
|
<option
|
||||||
|
v-for="option in categories"
|
||||||
|
:value="option.value"
|
||||||
|
:key="option.value"
|
||||||
|
>
|
||||||
|
{{ option.text }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input v-model="form.count" type="number" placeholder="Item Amount" />
|
||||||
|
|
||||||
|
<button type="submit" value="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading">Loading Items...</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="items">
|
||||||
|
<div v-for="(item, index) in items" :key="index">
|
||||||
|
<ItemOverview :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Items",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
category: null,
|
||||||
|
count: null,
|
||||||
|
},
|
||||||
|
categories: [
|
||||||
|
{ text: "Story", value: "story" },
|
||||||
|
{ text: "Commment", value: "comment" },
|
||||||
|
{ text: "Job", value: "job" },
|
||||||
|
{ text: "Poll", value: "poll" },
|
||||||
|
],
|
||||||
|
items: null,
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItems() {
|
||||||
|
this.error = this.item = null;
|
||||||
|
this.loading = true;
|
||||||
|
axios
|
||||||
|
.get("http://localhost:5000/items")
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.items = res.data.items;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.loading = false;
|
||||||
|
this.error = error.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.error = this.item = null;
|
||||||
|
this.loading = true;
|
||||||
|
axios
|
||||||
|
.post("http://localhost:5000/items", { count: this.form.count, category: this.form.category })
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.items = res.data.items;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.loading = false;
|
||||||
|
this.error = error.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getItems();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
form select {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
form input {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
form button {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
90
frontend/src/views/ItemView.vue
Normal file
90
frontend/src/views/ItemView.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<RouterLink to="/"><button>back</button></RouterLink>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading">
|
||||||
|
Loading Item {{ this.$route.params.id }}...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="item" class="content">
|
||||||
|
<h2>{{ item.title }}</h2>
|
||||||
|
<p>by: {{ item.by }}</p>
|
||||||
|
<p>time: {{ item.time }}</p>
|
||||||
|
<p>score: {{ item.score }}</p>
|
||||||
|
<p>type: {{ item.type }}</p>
|
||||||
|
<p>
|
||||||
|
Link: <a :href="item.url">{{ item.url }}</a>
|
||||||
|
</p>
|
||||||
|
<p v-html="item.text"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
item: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// watch the params of the route to fetch the data again
|
||||||
|
this.$watch(
|
||||||
|
() => this.$route.params,
|
||||||
|
() => {
|
||||||
|
this.getItem();
|
||||||
|
},
|
||||||
|
// fetch the data when the view is created and the data is
|
||||||
|
// already being observed
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getItem() {
|
||||||
|
this.error = this.item = null;
|
||||||
|
this.loading = true;
|
||||||
|
axios
|
||||||
|
.get(`http://localhost:5000/item/${Number(this.$route.params.id)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.item = JSON.parse(res.data.item);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.loading = false;
|
||||||
|
this.error = error.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
margin: 15px 5px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgb(41, 41, 41);
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgb(109, 109, 109);
|
||||||
|
}
|
||||||
|
</style>
|
55
frontend/src/views/PingView.vue
Normal file
55
frontend/src/views/PingView.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Ping-Pong Test</h2>
|
||||||
|
<p> This pages only purpose is showing a response from the server. </p>
|
||||||
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="msg" class="message">
|
||||||
|
<p>The servers responded </p>
|
||||||
|
<button type="button" class="btn btn-primary">{{ msg }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Ping",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
msg: null,
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getMessage() {
|
||||||
|
this.error = this.item = null;
|
||||||
|
this.loading = true;
|
||||||
|
axios
|
||||||
|
.get("http://localhost:5000/ping")
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.msg = res.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.loading = false;
|
||||||
|
this.error = error.toString();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getMessage();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
49
frontend/vite.config.js
Normal file
49
frontend/vite.config.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
fileURLToPath,
|
||||||
|
URL
|
||||||
|
} from 'url'
|
||||||
|
import {
|
||||||
|
resolve
|
||||||
|
} from 'path'
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineConfig
|
||||||
|
} from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: resolve(__dirname, '../templates'),
|
||||||
|
assetsDir: 'static',
|
||||||
|
manifest: true
|
||||||
|
},
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src',
|
||||||
|
import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "https://127.0.0.1:5000/",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
|
},
|
||||||
|
"/hackernews": {
|
||||||
|
target: "https://hacker-news.firebaseio.com/",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
ws: true
|
||||||
|
//rewrite: (path) => path.replace(/^\/hackernews/, "/v0/item/"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cors: true,
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "https://localhost:3000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
1048
frontend/yarn.lock
Normal file
1048
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Flask==2.1.1
|
||||||
|
Flask-Cors==3.0.10
|
||||||
|
requests2==2.16.0
|
BIN
templates/favicon.ico
Normal file
BIN
templates/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
18
templates/index.html
Normal file
18
templates/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HackerNews</title>
|
||||||
|
<script type="module" crossorigin src="/static/index.1f1b2f0d.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/index.5ececa9d.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
36
templates/manifest.json
Normal file
36
templates/manifest.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"index.html": {
|
||||||
|
"file": "static/index.1f1b2f0d.js",
|
||||||
|
"src": "index.html",
|
||||||
|
"isEntry": true,
|
||||||
|
"dynamicImports": [
|
||||||
|
"src/views/ItemView.vue",
|
||||||
|
"src/views/PingView.vue"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"static/index.5ececa9d.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"src/views/ItemView.vue": {
|
||||||
|
"file": "static/ItemView.b93ae7d5.js",
|
||||||
|
"src": "src/views/ItemView.vue",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"static/ItemView.0860556b.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"src/views/PingView.vue": {
|
||||||
|
"file": "static/PingView.f7a62302.js",
|
||||||
|
"src": "src/views/PingView.vue",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"static/PingView.3d2f7997.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
1
templates/static/AboutView.2868233e.js
Normal file
1
templates/static/AboutView.2868233e.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{_ as e,o as t,c as _,a as o}from"./index.9cf12eac.js";const s={},a={class:"about"},c=o("h1",null,"This is an about page",-1),n=[c];function r(i,u){return t(),_("div",a,n)}var l=e(s,[["render",r]]);export{l as default};
|
1
templates/static/AboutView.ab071ea6.css
Normal file
1
templates/static/AboutView.ab071ea6.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@media (min-width: 1024px){.about{min-height:100vh;display:flex;align-items:center}}
|
1
templates/static/ItemView.0860556b.css
Normal file
1
templates/static/ItemView.0860556b.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.card[data-v-2750d908]{margin:15px 5px;padding:10px;box-shadow:0 4px 8px #0003;border:1px solid rgb(41,41,41);transition:.3s;border-radius:5px}.card[data-v-2750d908]:hover{box-shadow:0 8px 16px #0003;border:1px solid rgb(109,109,109)}
|
1
templates/static/ItemView.b93ae7d5.js
Normal file
1
templates/static/ItemView.b93ae7d5.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{_ as l,r as d,o as i,c as r,a as u,w as m,t as o,b as a,d as t,e as p,f as h,p as c,g as _}from"./index.1f1b2f0d.js";const g=s=>(c("data-v-2750d908"),s=s(),_(),s),f=g(()=>t("button",null,"back",-1)),v={key:0,class:"loading"},y={key:1,class:"error"},k={key:2,class:"content"},I=p(" Link: "),b=["href"],S=["innerHTML"],w={data(){return{loading:!1,error:null,item:null}},created(){this.$watch(()=>this.$route.params,()=>{this.getItem()},{immediate:!0})},methods:{getItem(){this.error=this.item=null,this.loading=!0,h.get(`http://localhost:5000/item/${Number(this.$route.params.id)}`).then(s=>{this.loading=!1,this.item=JSON.parse(s.data.item)}).catch(s=>{this.loading=!1,this.error=s.toString()})}}},L=Object.assign(w,{props:{id:{type:String,required:!0}},setup(s){return(e,N)=>{const n=d("RouterLink");return i(),r("div",null,[u(n,{to:"/"},{default:m(()=>[f]),_:1}),e.loading?(i(),r("div",v," Loading Item "+o(this.$route.params.id)+"... ",1)):a("",!0),e.error?(i(),r("div",y,o(e.error),1)):a("",!0),e.item?(i(),r("div",k,[t("h2",null,o(e.item.title),1),t("p",null,"by: "+o(e.item.by),1),t("p",null,"time: "+o(e.item.time),1),t("p",null,"score: "+o(e.item.score),1),t("p",null,"type: "+o(e.item.type),1),t("p",null,[I,t("a",{href:e.item.url},o(e.item.url),9,b)]),t("p",{innerHTML:e.item.text},null,8,S)])):a("",!0)])}}});var $=l(L,[["__scopeId","data-v-2750d908"]]);export{$ as default};
|
1
templates/static/PingView.3d2f7997.css
Normal file
1
templates/static/PingView.3d2f7997.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.message[data-v-00a5a1ee]{text-align:center}
|
1
templates/static/PingView.f7a62302.js
Normal file
1
templates/static/PingView.f7a62302.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{_ as c,f as l,o as t,c as o,b as r,t as i,d as n,p as _,g as d}from"./index.1f1b2f0d.js";const p={name:"Ping",data(){return{msg:null,loading:null,error:null}},methods:{getMessage(){this.error=this.item=null,this.loading=!0,l.get("http://localhost:5000/ping").then(e=>{this.loading=!1,this.msg=e.data}).catch(e=>{this.loading=!1,this.error=e.toString()})}},created(){this.getMessage()}},a=e=>(_("data-v-00a5a1ee"),e=e(),d(),e),g={class:"container"},h=a(()=>n("h2",null,"Ping-Pong Test",-1)),u=a(()=>n("p",null," This pages only purpose is showing a response from the server. ",-1)),m={key:0,class:"loading"},v={key:1,class:"error"},f={key:2,class:"message"},y=a(()=>n("p",null,"The servers responded ",-1)),b={type:"button",class:"btn btn-primary"};function k(e,x,P,S,s,w){return t(),o("div",g,[h,u,s.loading?(t(),o("div",m,"Loading...")):r("",!0),s.error?(t(),o("div",v,i(s.error),1)):r("",!0),s.msg?(t(),o("div",f,[y,n("button",b,i(s.msg),1)])):r("",!0)])}var V=c(p,[["render",k],["__scopeId","data-v-00a5a1ee"]]);export{V as default};
|
1
templates/static/index.038ca730.css
Normal file
1
templates/static/index.038ca730.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
h1[data-v-9949253a]{font-weight:500;font-size:2.6rem;top:-10px}h3[data-v-9949253a]{font-size:1.2rem}.greetings h1[data-v-9949253a],.greetings h3[data-v-9949253a]{text-align:center}@media (min-width: 1024px){.greetings h1[data-v-9949253a],.greetings h3[data-v-9949253a]{text-align:left}}:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;position:relative;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:2rem;font-weight:400}header{line-height:1.5;max-height:100vh}.logo{display:block;margin:0 auto 2rem}a,.green{text-decoration:none;color:#00bd7e;transition:.4s}@media (hover: hover){a:hover{background-color:#00bd7e33}}nav{width:100%;font-size:12px;text-align:center;margin-top:2rem}nav a.router-link-exact-active{color:var(--color-text)}nav a.router-link-exact-active:hover{background-color:transparent}nav a{display:inline-block;padding:0 1rem;border-left:1px solid var(--color-border)}nav a:first-of-type{border:0}@media (min-width: 1024px){body{display:flex;place-items:center}#app{display:grid;grid-template-columns:1fr 1fr;padding:0 2rem}header{display:flex;place-items:center;padding-right:calc(var(--section-gap) / 2)}header .wrapper{display:flex;place-items:flex-start;flex-wrap:wrap}.logo{margin:0 2rem 0 0}nav{text-align:left;margin-left:-1rem;font-size:1rem;padding:1rem 0;margin-top:1rem}}.item[data-v-977bb0b6]{margin-top:2rem;display:flex}.details[data-v-977bb0b6]{flex:1;margin-left:1rem}i[data-v-977bb0b6]{display:flex;place-items:center;place-content:center;width:32px;height:32px;color:var(--color-text)}h3[data-v-977bb0b6]{font-size:1.2rem;font-weight:500;margin-bottom:.4rem;color:var(--color-heading)}@media (min-width: 1024px){.item[data-v-977bb0b6]{margin-top:0;padding:.4rem 0 1rem calc(var(--section-gap) / 2)}i[data-v-977bb0b6]{top:calc(50% - 25px);left:-26px;position:absolute;border:1px solid var(--color-border);background:var(--color-background);border-radius:8px;width:50px;height:50px}.item[data-v-977bb0b6]:before{content:" ";border-left:1px solid var(--color-border);position:absolute;left:0;bottom:calc(50% + 25px);height:calc(50% - 25px)}.item[data-v-977bb0b6]:after{content:" ";border-left:1px solid var(--color-border);position:absolute;left:0;top:calc(50% + 25px);height:calc(50% - 25px)}.item[data-v-977bb0b6]:first-of-type:before{display:none}.item[data-v-977bb0b6]:last-of-type:after{display:none}}
|
6
templates/static/index.1f1b2f0d.js
Normal file
6
templates/static/index.1f1b2f0d.js
Normal file
File diff suppressed because one or more lines are too long
1
templates/static/index.5ececa9d.css
Normal file
1
templates/static/index.5ececa9d.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
h1[data-v-3bc1e008]{font-weight:500;font-size:2.6rem;top:-10px}h3[data-v-3bc1e008]{font-size:1.2rem}.greetings h1[data-v-3bc1e008],.greetings h3[data-v-3bc1e008]{text-align:center}@media (min-width: 1024px){.greetings h1[data-v-3bc1e008],.greetings h3[data-v-3bc1e008]{text-align:left}}:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;position:relative;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:2rem;font-weight:400}header{line-height:1.5;max-height:100vh}a,.green{text-decoration:none;color:#00bd7e;transition:.4s}h1{font-weight:500;font-size:2.6rem;top:-10px}h3{font-size:1.2rem}.greetings h1,.greetings h3{text-align:center}.headerseperator{display:block;margin:15px}@media (min-width: 1024px){.greetings h1,.greetings h3{text-align:left}.headerseperator{display:none}}@media (hover: hover){a:hover{background-color:#00bd7e33}}@media (min-width: 1024px){body{display:flex;place-items:center}#app{display:grid;grid-template-columns:1fr 1fr;padding:0 2rem}header{display:flex;place-items:center;padding-right:calc(var(--section-gap) / 2)}header .wrapper{display:flex;place-items:flex-start;flex-wrap:wrap}}.card[data-v-773f36a2]{margin:15px 5px;padding:10px;box-shadow:0 4px 8px #0003;border:1px solid rgb(41,41,41);transition:.3s;border-radius:5px}.card[data-v-773f36a2]:hover{box-shadow:0 8px 16px #0003;border:1px solid rgb(109,109,109)}.story[data-v-773f36a2]{border:1px solid rgb(95,0,0)}.story[data-v-773f36a2]:hover{border:1px solid rgb(201,0,0)}.comment[data-v-773f36a2]{border:1px solid rgb(0,106,106)}.comment[data-v-773f36a2]:hover{border:1px solid rgb(0,189,189)}.job[data-v-773f36a2]{border:1px solid rgb(106,0,106)}.job[data-v-773f36a2]:hover{border:1px solid rgb(211,0,211)}.poll[data-v-773f36a2]{border:1px solid rgb(106,106,0)}.poll[data-v-773f36a2]:hover{border:1px solid rgb(216,216,0)}form[data-v-0771414d]{width:100%;margin:10px;padding:5px}form select[data-v-0771414d]{width:40%}form input[data-v-0771414d]{width:30%}form button[data-v-0771414d]{width:20%}hr[data-v-0771414d]{margin:15px 0}
|
5
templates/static/index.9cf12eac.js
Normal file
5
templates/static/index.9cf12eac.js
Normal file
File diff suppressed because one or more lines are too long
1
templates/static/logo.da9b9095.svg
Normal file
1
templates/static/logo.da9b9095.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
Loading…
Reference in New Issue
Block a user