initial commit

This commit is contained in:
Robert Jeutter 2022-04-01 17:03:49 +02:00
parent d5eb740da9
commit b8085ee62b
35 changed files with 2086 additions and 0 deletions

65
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

128
frontend/src/App.vue Normal file
View 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>

View 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;
}

View 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

View 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>

View 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
View 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')

View 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

View 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>

View 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>

View 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
View 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

File diff suppressed because it is too large Load Diff

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Flask==2.1.1
Flask-Cors==3.0.10
requests2==2.16.0

BIN
templates/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

18
templates/index.html Normal file
View 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
View 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"
]
}
}

View 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};

View File

@ -0,0 +1 @@
@media (min-width: 1024px){.about{min-height:100vh;display:flex;align-items:center}}

View 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)}

View 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};

View File

@ -0,0 +1 @@
.message[data-v-00a5a1ee]{text-align:center}

View 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};

View 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}}

File diff suppressed because one or more lines are too long

View 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}

File diff suppressed because one or more lines are too long

View 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