aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConnectivityChecker.vue52
-rw-r--r--src/components/DarkMode.vue34
-rw-r--r--src/components/DynamicTheme.vue34
-rw-r--r--src/components/Message.vue41
-rw-r--r--src/components/Navbar.vue67
-rw-r--r--src/components/SearchInput.vue42
-rw-r--r--src/components/Service.vue40
-rw-r--r--src/components/SettingToggle.vue40
8 files changed, 350 insertions, 0 deletions
diff --git a/src/components/ConnectivityChecker.vue b/src/components/ConnectivityChecker.vue
new file mode 100644
index 0000000..a91a809
--- /dev/null
+++ b/src/components/ConnectivityChecker.vue
@@ -0,0 +1,52 @@
1<template>
2 <div v-if="offline" class="offline-message">
3 <i class="far fa-dizzy"></i>
4 <h1>
5 You're offline bro.
6 <span @click="checkOffline"> <i class="fas fa-redo-alt"></i></span>
7 </h1>
8 </div>
9</template>
10
11<script>
12export default {
13 name: "ConnectivityChecker",
14 data: function () {
15 return {
16 offline: false,
17 };
18 },
19 created: function () {
20 let that = this;
21 this.checkOffline();
22
23 document.addEventListener(
24 "visibilitychange",
25 function () {
26 if (document.visibilityState == "visible") {
27 that.checkOffline();
28 }
29 },
30 false
31 );
32 },
33 methods: {
34 checkOffline: function () {
35 let that = this;
36 return fetch(window.location.href + "?alive", {
37 method: "HEAD",
38 cache: "no-store",
39 })
40 .then(function () {
41 that.offline = false;
42 })
43 .catch(function () {
44 that.offline = true;
45 })
46 .finally(function () {
47 that.$emit("network:status-update", that.offline);
48 });
49 },
50 },
51};
52</script>
diff --git a/src/components/DarkMode.vue b/src/components/DarkMode.vue
new file mode 100644
index 0000000..0bcde0f
--- /dev/null
+++ b/src/components/DarkMode.vue
@@ -0,0 +1,34 @@
1<template>
2 <a
3 v-on:click="toggleTheme()"
4 aria-label="Toggle dark mode"
5 class="navbar-item is-inline-block-mobile"
6 >
7 <i class="fas fa-adjust"></i>
8 </a>
9</template>
10
11<script>
12export default {
13 name: "Darkmode",
14 data: function () {
15 return {
16 isDark: null,
17 };
18 },
19 created: function () {
20 this.isDark =
21 "overrideDark" in localStorage
22 ? JSON.parse(localStorage.overrideDark)
23 : matchMedia("(prefers-color-scheme: dark)").matches;
24 this.$emit("updated", this.isDark);
25 },
26 methods: {
27 toggleTheme: function () {
28 this.isDark = !this.isDark;
29 localStorage.overrideDark = this.isDark;
30 this.$emit("updated", this.isDark);
31 },
32 },
33};
34</script>
diff --git a/src/components/DynamicTheme.vue b/src/components/DynamicTheme.vue
new file mode 100644
index 0000000..cf9963b
--- /dev/null
+++ b/src/components/DynamicTheme.vue
@@ -0,0 +1,34 @@
1<template>
2 <DynamicStyle>
3 /* light / dark theme switch based on system pref if available */ body #app
4 {
5 {{ getVars(themes.light) }}
6 } @media (prefers-color-scheme: light), (prefers-color-scheme:
7 no-preference) { body #app {
8 {{ getVars(themes.light) }}
9 } } @media (prefers-color-scheme: dark) { body #app { } } /* light / dark
10 theme override base on user choice. */ body #app.is-dark {
11 {{ getVars(themes.dark) }}
12 } body #app.is-light {
13 {{ getVars(themes.light) }}
14 }
15 </DynamicStyle>
16</template>
17
18<script>
19export default {
20 name: "DynamicTheme",
21 props: {
22 themes: Object,
23 },
24 methods: {
25 getVars: function (theme) {
26 let vars = [];
27 for (const themeVars in theme) {
28 vars.push(`--${themeVars}: ${theme[themeVars]}`);
29 }
30 return vars.join(";");
31 },
32 },
33};
34</script>
diff --git a/src/components/Message.vue b/src/components/Message.vue
new file mode 100644
index 0000000..fcb0fbb
--- /dev/null
+++ b/src/components/Message.vue
@@ -0,0 +1,41 @@
1<template>
2 <article v-if="item" class="message" :class="item.style">
3 <div v-if="item.title" class="message-header">
4 <p>{{ item.title }}</p>
5 </div>
6 <div v-if="item.content" class="message-body" v-html="item.content"></div>
7 </article>
8</template>
9
10<script>
11export default {
12 name: "Message",
13 props: {
14 item: Object,
15 },
16 created: function () {
17 // Look for a new message if an endpoint is provided.
18 let that = this;
19 if (this.item && this.item.url) {
20 this.getMessage(this.item.url).then(function (message) {
21 // keep the original config value if no value is provided by the endpoint
22 for (const prop of ["title", "style", "content"]) {
23 if (prop in message && message[prop] !== null) {
24 that.item[prop] = message[prop];
25 }
26 }
27 });
28 }
29 },
30 methods: {
31 getMessage: function (url) {
32 return fetch(url).then(function (response) {
33 if (response.status != 200) {
34 return;
35 }
36 return response.json();
37 });
38 },
39 },
40};
41</script>
diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue
new file mode 100644
index 0000000..d3ceaf8
--- /dev/null
+++ b/src/components/Navbar.vue
@@ -0,0 +1,67 @@
1<template>
2 <div v-cloak v-if="links" class="container-fluid">
3 <nav class="navbar" role="navigation" aria-label="main navigation">
4 <div class="container">
5 <div class="navbar-brand">
6 <a
7 role="button"
8 aria-label="menu"
9 aria-expanded="false"
10 class="navbar-burger"
11 :class="{ 'is-active': showMenu }"
12 v-on:click="$emit('navbar:toggle')"
13 >
14 <span aria-hidden="true"></span>
15 <span aria-hidden="true"></span>
16 <span aria-hidden="true"></span>
17 </a>
18 </div>
19 <div class="navbar-menu" :class="{ 'is-active': showMenu }">
20 <div class="navbar-start">
21 <a
22 class="navbar-item"
23 rel="noreferrer"
24 v-for="link in links"
25 :key="link.url"
26 :href="link.url"
27 :target="link.target"
28 >
29 <i
30 v-if="link.icon"
31 style="margin-right: 6px;"
32 :class="link.icon"
33 ></i>
34 {{ link.name }}
35 </a>
36 </div>
37 <div class="navbar-end">
38 <slot></slot>
39 </div>
40 </div>
41 </div>
42 </nav>
43 </div>
44</template>
45
46<script>
47export default {
48 name: "Navbar",
49 props: {
50 open: {
51 type: Boolean,
52 default: false,
53 },
54 links: Array,
55 },
56 computed: {
57 showMenu: function () {
58 return this.open && this.isSmallScreen();
59 },
60 },
61 methods: {
62 isSmallScreen: function () {
63 return window.matchMedia("screen and (max-width: 1023px)").matches;
64 },
65 },
66};
67</script>
diff --git a/src/components/SearchInput.vue b/src/components/SearchInput.vue
new file mode 100644
index 0000000..22b5eef
--- /dev/null
+++ b/src/components/SearchInput.vue
@@ -0,0 +1,42 @@
1<template>
2 <div class="search-bar">
3 <label for="search" class="search-label"></label>
4 <input
5 type="text"
6 ref="search"
7 :value="value"
8 @input="$emit('input', $event.target.value.toLowerCase())"
9 @keyup.enter.exact="$emit('search:open')"
10 @keyup.alt.enter="$emit('search:open', '_blank')"
11 />
12 </div>
13</template>
14
15<script>
16export default {
17 name: "SearchInput",
18 props: ["value"],
19 mounted() {
20 this._keyListener = function (event) {
21 if (event.key === "/") {
22 event.preventDefault();
23 this.$emit("search:focus");
24 this.$nextTick(() => {
25 this.$refs.search.focus();
26 });
27 }
28 if (event.key === "Escape") {
29 this.$refs.search.value = "";
30 this.$refs.search.blur();
31 this.$emit("search:cancel");
32 }
33 };
34 document.addEventListener("keydown", this._keyListener.bind(this));
35 },
36 beforeDestroy() {
37 document.removeEventListener("keydown", this._keyListener);
38 },
39};
40</script>
41
42<style lang="scss" scoped></style>
diff --git a/src/components/Service.vue b/src/components/Service.vue
new file mode 100644
index 0000000..a2448ca
--- /dev/null
+++ b/src/components/Service.vue
@@ -0,0 +1,40 @@
1<template>
2 <div>
3 <div class="card">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px;" :class="item.icon"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 </div>
22 <div class="tag" :class="item.tagstyle" v-if="item.tag">
23 <strong class="tag-text">#{{ item.tag }}</strong>
24 </div>
25 </div>
26 </a>
27 </div>
28 </div>
29</template>
30
31<script>
32export default {
33 name: "Service",
34 props: {
35 item: Object,
36 },
37};
38</script>
39
40<style scoped lang="scss"></style>
diff --git a/src/components/SettingToggle.vue b/src/components/SettingToggle.vue
new file mode 100644
index 0000000..864a497
--- /dev/null
+++ b/src/components/SettingToggle.vue
@@ -0,0 +1,40 @@
1<template>
2 <a v-on:click="toggleSetting()" class="navbar-item is-inline-block-mobile">
3 <span><i :class="['fas', value ? icon : iconAlt]"></i></span>
4 <slot></slot>
5 </a>
6</template>
7
8<script>
9export default {
10 name: "SettingToggle",
11 props: {
12 name: String,
13 icon: String,
14 iconAlt: String,
15 },
16 data: function () {
17 return {
18 value: true,
19 };
20 },
21 created: function () {
22 if (!this.iconAlt) {
23 this.iconAlt = this.icon;
24 }
25
26 if (this.name in localStorage) {
27 this.value = JSON.parse(localStorage[this.name]);
28 }
29
30 this.$emit("updated", this.value);
31 },
32 methods: {
33 toggleSetting: function () {
34 this.value = !this.value;
35 localStorage[this.name] = this.value;
36 this.$emit("updated", this.value);
37 },
38 },
39};
40</script>