mirror of
https://github.com/taigrr/homer
synced 2025-01-18 04:53:12 -08:00
Build system integration using vue-cli.
This commit is contained in:
214
src/App.vue
Normal file
214
src/App.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div
|
||||
id="app"
|
||||
v-if="config"
|
||||
:class="[
|
||||
`theme-${config.theme}`,
|
||||
isDark ? 'is-dark' : 'is-light',
|
||||
!config.footer ? 'no-footer' : ''
|
||||
]"
|
||||
>
|
||||
<DynamicTheme :themes="config.colors" />
|
||||
<div id="bighead">
|
||||
<section v-if="config.header" class="first-line">
|
||||
<div v-cloak class="container">
|
||||
<div class="logo">
|
||||
<img v-if="config.logo" :src="config.logo" />
|
||||
<i v-if="config.icon" :class="config.icon"></i>
|
||||
</div>
|
||||
<div class="dashboard-title">
|
||||
<span class="headline">{{ config.subtitle }}</span>
|
||||
<h1>{{ config.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Navbar
|
||||
:open="showMenu"
|
||||
:links="config.links"
|
||||
@navbar:toggle="showMenu = !showMenu"
|
||||
>
|
||||
<DarkMode @updated="isDark = $event" />
|
||||
|
||||
<SettingToggle
|
||||
@updated="vlayout = $event"
|
||||
name="vlayout"
|
||||
icon="fa-list"
|
||||
iconAlt="fa-columns"
|
||||
/>
|
||||
|
||||
<SearchInput
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
@input="filterServices"
|
||||
@search:focus="showMenu = true"
|
||||
@search:open="navigateToFirstService"
|
||||
@search:cancel="filterServices"
|
||||
/>
|
||||
</Navbar>
|
||||
</div>
|
||||
|
||||
<section id="main-section" class="section">
|
||||
<div v-cloak class="container">
|
||||
<ConnectivityChecker @network:status-update="offline = $event" />
|
||||
<div v-if="!offline">
|
||||
<!-- Optional messages -->
|
||||
<Message :item="config.message" />
|
||||
|
||||
<!-- Horizontal layout -->
|
||||
<div v-if="!vlayout || filter" class="columns is-multiline">
|
||||
<template v-for="group in services">
|
||||
<h2 v-if="group.name" class="column is-full group-title">
|
||||
<i v-if="group.icon" :class="group.icon"></i>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
v-for="item in group.items"
|
||||
:key="item.url"
|
||||
v-bind:item="item"
|
||||
class="column is-one-third-widescreen"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Vertical layout -->
|
||||
<div
|
||||
v-if="!filter && vlayout"
|
||||
class="columns is-multiline layout-vertical"
|
||||
>
|
||||
<div
|
||||
class="column is-one-third-widescreen"
|
||||
v-for="group in services"
|
||||
:key="group.name"
|
||||
>
|
||||
<h2 v-if="group.name" class="group-title">
|
||||
<i v-if="group.icon" :class="group.icon"></i>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
v-for="item in group.items"
|
||||
v-bind:item="item"
|
||||
:key="item.url"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div
|
||||
class="content has-text-centered"
|
||||
v-if="config.footer"
|
||||
v-html="config.footer"
|
||||
></div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const jsyaml = require("js-yaml");
|
||||
const merge = require("lodash.merge");
|
||||
|
||||
import Navbar from "./components/Navbar.vue";
|
||||
import ConnectivityChecker from "./components/ConnectivityChecker.vue";
|
||||
import Service from "./components/Service.vue";
|
||||
import Message from "./components/Message.vue";
|
||||
import SearchInput from "./components/SearchInput.vue";
|
||||
import SettingToggle from "./components/SettingToggle.vue";
|
||||
import DarkMode from "./components/DarkMode.vue";
|
||||
import DynamicTheme from "./components/DynamicTheme.vue";
|
||||
|
||||
import defaultConfig from "./assets/defaults.yml";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Navbar,
|
||||
ConnectivityChecker,
|
||||
Service,
|
||||
Message,
|
||||
SearchInput,
|
||||
SettingToggle,
|
||||
DarkMode,
|
||||
DynamicTheme
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
config: null,
|
||||
services: null,
|
||||
offline: false,
|
||||
filter: "",
|
||||
vlayout: true,
|
||||
isDark: null,
|
||||
showMenu: false
|
||||
};
|
||||
},
|
||||
created: async function () {
|
||||
try {
|
||||
const defaults = jsyaml.load(defaultConfig);
|
||||
let config = await this.getConfig();
|
||||
|
||||
this.config = merge(defaults, config);
|
||||
|
||||
console.log(this.config);
|
||||
this.services = this.config.services;
|
||||
document.title = `${this.config.title} | ${this.config.subtitle}`;
|
||||
} catch (error) {
|
||||
this.offline = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getConfig: function () {
|
||||
return fetch("config.yml").then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
return response.text().then(function (body) {
|
||||
return jsyaml.load(body);
|
||||
});
|
||||
});
|
||||
},
|
||||
matchesFilter: function (item) {
|
||||
return (
|
||||
item.name.toLowerCase().includes(this.filter) ||
|
||||
(item.tag && item.tag.toLowerCase().includes(this.filter))
|
||||
);
|
||||
},
|
||||
navigateToFirstService: function (target) {
|
||||
try {
|
||||
const service = this.services[0].items[0];
|
||||
window.open(service.url, target || service.target || "_self");
|
||||
} catch (error) {
|
||||
console.warning("fail to open service");
|
||||
}
|
||||
},
|
||||
filterServices: function (filter) {
|
||||
this.filter = filter;
|
||||
|
||||
if (!filter) {
|
||||
this.services = this.config.services;
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResultItems = [];
|
||||
for (const group of this.config.services) {
|
||||
for (const item of group.items) {
|
||||
if (this.matchesFilter(item)) {
|
||||
searchResultItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.services = [
|
||||
{
|
||||
name: filter,
|
||||
icon: "fas fa-search",
|
||||
items: searchResultItems
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
336
src/assets/app.scss
Normal file
336
src/assets/app.scss
Normal file
@@ -0,0 +1,336 @@
|
||||
@charset "utf-8";
|
||||
|
||||
@import url("//fonts.googleapis.com/css?family=Lato:400,700|Pacifico|Raleway&display=swap");
|
||||
@import "bulma";
|
||||
|
||||
// Themes import
|
||||
@import "./themes/sui.scss";
|
||||
|
||||
@mixin ellipsis() {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Raleway", sans-serif;
|
||||
height: 100%;
|
||||
|
||||
#app {
|
||||
min-height: 100%;
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
color: var(--link-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-title);
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--text-subtitle);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-background);
|
||||
box-shadow: 0 2px 15px 0 var(--card-shadow);
|
||||
&:hover {
|
||||
background-color: var(--card-background);
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
background-color: var(--card-background);
|
||||
.message-body {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--card-background);
|
||||
box-shadow: 0 2px 15px 0 var(--card-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Lato", sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.7rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.fas,
|
||||
.fab,
|
||||
.far {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
color: var(--highlight-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#bighead {
|
||||
color: var(--text-header);
|
||||
|
||||
.dashboard-title {
|
||||
padding: 6px 0 0 80px;
|
||||
}
|
||||
|
||||
.first-line {
|
||||
height: 100px;
|
||||
vertical-align: center;
|
||||
background-color: var(--highlight-primary);
|
||||
|
||||
h1 {
|
||||
margin-top: -12px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
margin-top: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 80px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
i {
|
||||
vertical-align: top;
|
||||
padding: 8px 15px;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 10px;
|
||||
max-height: 70px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.navbar,
|
||||
.navbar-menu {
|
||||
background-color: var(--highlight-secondary);
|
||||
|
||||
a {
|
||||
color: var(--text-header);
|
||||
padding: 8px 12px;
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--text-header);
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
.navbar-end {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
#main-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 0;
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0px;
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1em;
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9em;
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1.2rem 0.75rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 45px;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.message-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-content {
|
||||
overflow: hidden;
|
||||
text-overflow: inherit;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--highlight-secondary);
|
||||
background-color: var(--highlight-secondary);
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: -0.2rem;
|
||||
width: 3px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease-out;
|
||||
padding: 0;
|
||||
|
||||
.tag-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||
|
||||
a {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translate(0, -3px);
|
||||
|
||||
.tag {
|
||||
width: auto;
|
||||
color: #ffffff;
|
||||
padding: 0 0.75em;
|
||||
|
||||
.tag-text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
height: 85px;
|
||||
padding: 1.3rem;
|
||||
}
|
||||
|
||||
.layout-vertical {
|
||||
.card {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.column div:first-of-type .card {
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.column div:last-child .card {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
color: #676767;
|
||||
font-size: 0.85rem;
|
||||
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||
}
|
||||
|
||||
.no-footer {
|
||||
#main-section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
input {
|
||||
border: none;
|
||||
background-color: var(--highlight-hover);
|
||||
border-radius: 5px;
|
||||
margin-top: 2px;
|
||||
padding: 2px 12px 2px 30px;
|
||||
transition: all 100ms linear;
|
||||
color: #ffffff;
|
||||
height: 30px;
|
||||
width: 100px;
|
||||
|
||||
&:focus {
|
||||
color: #000000;
|
||||
width: 250px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-label::before {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 16px;
|
||||
content: "\f002";
|
||||
font-weight: 900;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&:focus-within .search-label::before {
|
||||
color: #6e6e6e;
|
||||
}
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
text-align: center;
|
||||
margin: 35px 0;
|
||||
|
||||
svg {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
svg.fa-redo-alt {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1rem;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
color: #3273dc;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/assets/defaults.yml
Normal file
39
src/assets/defaults.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
# Default configuration
|
||||
|
||||
title: "Dashboard"
|
||||
subtitle: "Homer"
|
||||
logo: "logo.png"
|
||||
|
||||
header: true
|
||||
footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a href="https://bulma.io/">bulma</a>, <a href="https://vuejs.org/">vuejs</a> & <a href="https://fontawesome.com/">font awesome</a> // Fork me on <a href="https://github.com/bastienwirtz/homer"><i class="fab fa-github-alt"></i></a></p>' # set false if you want to hide it.
|
||||
|
||||
theme: default
|
||||
colors:
|
||||
light:
|
||||
highlight-primary: "#3367d6"
|
||||
highlight-secondary: "#4285f4"
|
||||
highlight-hover: "#5a95f5"
|
||||
background: "#f5f5f5"
|
||||
card-background: "#ffffff"
|
||||
text: "#363636"
|
||||
text-header: "#ffffff"
|
||||
text-title: "#303030"
|
||||
text-subtitle: "#424242"
|
||||
card-shadow: rgba(0, 0, 0, 0.1)
|
||||
link-hover: "#363636"
|
||||
dark:
|
||||
highlight-primary: "#3367d6"
|
||||
highlight-secondary: "#4285f4"
|
||||
highlight-hover: "#5a95f5"
|
||||
background: "#131313"
|
||||
card-background: "#2b2b2b"
|
||||
text: "#eaeaea"
|
||||
text-header: "#ffffff"
|
||||
text-title: "#fafafa"
|
||||
text-subtitle: "#f5f5f5"
|
||||
card-shadow: rgba(0, 0, 0, 0.4)
|
||||
link-hover: "#ffdd57"
|
||||
|
||||
links: []
|
||||
services: []
|
||||
34
src/assets/themes/sui.scss
Normal file
34
src/assets/themes/sui.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* SUI theme
|
||||
* Inpired by the great https://github.com/jeroenpardon/sui start page
|
||||
* Author: @bastienwirtz
|
||||
*/
|
||||
body #app.theme-sui {
|
||||
#bighead .dashboard-title {
|
||||
padding: 65px 0 0 12px;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-weight: bold;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar .navbar-item:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.card,
|
||||
.card:hover {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/components/ConnectivityChecker.vue
Normal file
52
src/components/ConnectivityChecker.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div v-if="offline" class="offline-message">
|
||||
<i class="far fa-dizzy"></i>
|
||||
<h1>
|
||||
You're offline bro.
|
||||
<span @click="checkOffline"> <i class="fas fa-redo-alt"></i></span>
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ConnectivityChecker",
|
||||
data: function () {
|
||||
return {
|
||||
offline: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
let that = this;
|
||||
this.checkOffline();
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
function () {
|
||||
if (document.visibilityState == "visible") {
|
||||
that.checkOffline();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
checkOffline: function () {
|
||||
let that = this;
|
||||
return fetch(window.location.href + "?alive", {
|
||||
method: "HEAD",
|
||||
cache: "no-store",
|
||||
})
|
||||
.then(function () {
|
||||
that.offline = false;
|
||||
})
|
||||
.catch(function () {
|
||||
that.offline = true;
|
||||
})
|
||||
.finally(function () {
|
||||
that.$emit("network:status-update", that.offline);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
34
src/components/DarkMode.vue
Normal file
34
src/components/DarkMode.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<a
|
||||
v-on:click="toggleTheme()"
|
||||
aria-label="Toggle dark mode"
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
>
|
||||
<i class="fas fa-adjust"></i>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Darkmode",
|
||||
data: function () {
|
||||
return {
|
||||
isDark: null,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.isDark =
|
||||
"overrideDark" in localStorage
|
||||
? JSON.parse(localStorage.overrideDark)
|
||||
: matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
this.$emit("updated", this.isDark);
|
||||
},
|
||||
methods: {
|
||||
toggleTheme: function () {
|
||||
this.isDark = !this.isDark;
|
||||
localStorage.overrideDark = this.isDark;
|
||||
this.$emit("updated", this.isDark);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
34
src/components/DynamicTheme.vue
Normal file
34
src/components/DynamicTheme.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<DynamicStyle>
|
||||
/* light / dark theme switch based on system pref if available */ body #app
|
||||
{
|
||||
{{ getVars(themes.light) }}
|
||||
} @media (prefers-color-scheme: light), (prefers-color-scheme:
|
||||
no-preference) { body #app {
|
||||
{{ getVars(themes.light) }}
|
||||
} } @media (prefers-color-scheme: dark) { body #app { } } /* light / dark
|
||||
theme override base on user choice. */ body #app.is-dark {
|
||||
{{ getVars(themes.dark) }}
|
||||
} body #app.is-light {
|
||||
{{ getVars(themes.light) }}
|
||||
}
|
||||
</DynamicStyle>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DynamicTheme",
|
||||
props: {
|
||||
themes: Object,
|
||||
},
|
||||
methods: {
|
||||
getVars: function (theme) {
|
||||
let vars = [];
|
||||
for (const themeVars in theme) {
|
||||
vars.push(`--${themeVars}: ${theme[themeVars]}`);
|
||||
}
|
||||
return vars.join(";");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
41
src/components/Message.vue
Normal file
41
src/components/Message.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<article v-if="item" class="message" :class="item.style">
|
||||
<div v-if="item.title" class="message-header">
|
||||
<p>{{ item.title }}</p>
|
||||
</div>
|
||||
<div v-if="item.content" class="message-body" v-html="item.content"></div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Message",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
created: function () {
|
||||
// Look for a new message if an endpoint is provided.
|
||||
let that = this;
|
||||
if (this.item && this.item.url) {
|
||||
this.getMessage(this.item.url).then(function (message) {
|
||||
// keep the original config value if no value is provided by the endpoint
|
||||
for (const prop of ["title", "style", "content"]) {
|
||||
if (prop in message && message[prop] !== null) {
|
||||
that.item[prop] = message[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getMessage: function (url) {
|
||||
return fetch(url).then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
66
src/components/Navbar.vue
Normal file
66
src/components/Navbar.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div v-cloak v-if="links" class="container-fluid">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a
|
||||
role="button"
|
||||
aria-label="menu"
|
||||
aria-expanded="false"
|
||||
class="navbar-burger"
|
||||
:class="{ 'is-active': showMenu }"
|
||||
v-on:click="$emit('navbar:toggle')"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu" :class="{ 'is-active': showMenu }">
|
||||
<div class="navbar-start">
|
||||
<a
|
||||
class="navbar-item"
|
||||
v-for="link in links"
|
||||
:key="link.url"
|
||||
:href="link.url"
|
||||
:target="link.target"
|
||||
>
|
||||
<i
|
||||
v-if="link.icon"
|
||||
style="margin-right: 6px;"
|
||||
:class="link.icon"
|
||||
></i>
|
||||
{{ link.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Navbar",
|
||||
props: {
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
links: Array,
|
||||
},
|
||||
computed: {
|
||||
showMenu: function () {
|
||||
return this.open && this.isSmallScreen();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isSmallScreen: function () {
|
||||
return window.matchMedia("screen and (max-width: 1023px)").matches;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
42
src/components/SearchInput.vue
Normal file
42
src/components/SearchInput.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<label for="search" class="search-label"></label>
|
||||
<input
|
||||
type="text"
|
||||
ref="search"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value.toLowerCase())"
|
||||
@keyup.enter.exact="$emit('search:open')"
|
||||
@keyup.alt.enter="$emit('search:open', '_blank')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SearchInput",
|
||||
props: ["value"],
|
||||
mounted() {
|
||||
this._keyListener = function (event) {
|
||||
if (event.key === "/") {
|
||||
event.preventDefault();
|
||||
this.$emit("search:focus");
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus();
|
||||
});
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
this.$refs.search.value = "";
|
||||
this.$refs.search.blur();
|
||||
this.$emit("search:cancel");
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", this._keyListener.bind(this));
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this._keyListener);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
40
src/components/Service.vue
Normal file
40
src/components/Service.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<a :href="item.url" :target="item.target">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div v-if="item.logo" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="item.logo" />
|
||||
</figure>
|
||||
</div>
|
||||
<div v-if="item.icon" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<i style="font-size: 35px;" :class="item.icon"></i>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ item.name }}</p>
|
||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Service",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
41
src/components/SettingToggle.vue
Normal file
41
src/components/SettingToggle.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a v-on:click="toggleSetting()" class="navbar-item is-inline-block-mobile">
|
||||
<span v-show="value"><i :class="['fas', icon]"></i></span>
|
||||
<span v-show="!value"><i :class="['fas', iconAlt]"></i></span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SettingToggle",
|
||||
props: {
|
||||
name: String,
|
||||
icon: String,
|
||||
iconAlt: String,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: true,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
if (!this.iconAlt) {
|
||||
this.iconAlt = this.icon;
|
||||
}
|
||||
|
||||
if (this.name in localStorage) {
|
||||
this.value = JSON.parse(localStorage[this.name]);
|
||||
}
|
||||
|
||||
this.$emit("updated", this.value);
|
||||
},
|
||||
methods: {
|
||||
toggleSetting: function () {
|
||||
this.value = !this.value;
|
||||
localStorage[this.name] = this.value;
|
||||
this.$emit("updated", this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
20
src/main.js
Normal file
20
src/main.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./registerServiceWorker";
|
||||
|
||||
import "@fortawesome/fontawesome-free/css/all.css";
|
||||
import "@fortawesome/fontawesome-free/js/all.js";
|
||||
|
||||
import "./assets/app.scss";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.component("DynamicStyle", {
|
||||
render: function (createElement) {
|
||||
return createElement("style", this.$slots.default);
|
||||
},
|
||||
});
|
||||
|
||||
new Vue({
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
34
src/registerServiceWorker.js
Normal file
34
src/registerServiceWorker.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
"For more details, visit https://goo.gl/AFskqB"
|
||||
);
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user