initial commit
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user