diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/ConnectivityChecker.vue | 52 | ||||
-rw-r--r-- | src/components/DarkMode.vue | 34 | ||||
-rw-r--r-- | src/components/DynamicTheme.vue | 34 | ||||
-rw-r--r-- | src/components/Message.vue | 41 | ||||
-rw-r--r-- | src/components/Navbar.vue | 67 | ||||
-rw-r--r-- | src/components/SearchInput.vue | 42 | ||||
-rw-r--r-- | src/components/Service.vue | 40 | ||||
-rw-r--r-- | src/components/SettingToggle.vue | 40 |
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> | ||
12 | export 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> | ||
12 | export 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> | ||
19 | export 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> | ||
11 | export 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> | ||
47 | export 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> | ||
16 | export 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> | ||
32 | export 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> | ||
9 | export 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> | ||