initial commit

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

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>