aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue214
-rw-r--r--src/assets/app.scss336
-rw-r--r--src/assets/defaults.yml39
-rw-r--r--src/assets/themes/sui.scss34
-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.vue66
-rw-r--r--src/components/SearchInput.vue42
-rw-r--r--src/components/Service.vue40
-rw-r--r--src/components/SettingToggle.vue41
-rw-r--r--src/main.js20
-rw-r--r--src/registerServiceWorker.js34
14 files changed, 1027 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..f7fd34a
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,214 @@
1<template>
2 <div
3 id="app"
4 v-if="config"
5 :class="[
6 `theme-${config.theme}`,
7 isDark ? 'is-dark' : 'is-light',
8 !config.footer ? 'no-footer' : ''
9 ]"
10 >
11 <DynamicTheme :themes="config.colors" />
12 <div id="bighead">
13 <section v-if="config.header" class="first-line">
14 <div v-cloak class="container">
15 <div class="logo">
16 <img v-if="config.logo" :src="config.logo" />
17 <i v-if="config.icon" :class="config.icon"></i>
18 </div>
19 <div class="dashboard-title">
20 <span class="headline">{{ config.subtitle }}</span>
21 <h1>{{ config.title }}</h1>
22 </div>
23 </div>
24 </section>
25
26 <Navbar
27 :open="showMenu"
28 :links="config.links"
29 @navbar:toggle="showMenu = !showMenu"
30 >
31 <DarkMode @updated="isDark = $event" />
32
33 <SettingToggle
34 @updated="vlayout = $event"
35 name="vlayout"
36 icon="fa-list"
37 iconAlt="fa-columns"
38 />
39
40 <SearchInput
41 class="navbar-item is-inline-block-mobile"
42 @input="filterServices"
43 @search:focus="showMenu = true"
44 @search:open="navigateToFirstService"
45 @search:cancel="filterServices"
46 />
47 </Navbar>
48 </div>
49
50 <section id="main-section" class="section">
51 <div v-cloak class="container">
52 <ConnectivityChecker @network:status-update="offline = $event" />
53 <div v-if="!offline">
54 <!-- Optional messages -->
55 <Message :item="config.message" />
56
57 <!-- Horizontal layout -->
58 <div v-if="!vlayout || filter" class="columns is-multiline">
59 <template v-for="group in services">
60 <h2 v-if="group.name" class="column is-full group-title">
61 <i v-if="group.icon" :class="group.icon"></i>
62 {{ group.name }}
63 </h2>
64 <Service
65 v-for="item in group.items"
66 :key="item.url"
67 v-bind:item="item"
68 class="column is-one-third-widescreen"
69 />
70 </template>
71 </div>
72
73 <!-- Vertical layout -->
74 <div
75 v-if="!filter && vlayout"
76 class="columns is-multiline layout-vertical"
77 >
78 <div
79 class="column is-one-third-widescreen"
80 v-for="group in services"
81 :key="group.name"
82 >
83 <h2 v-if="group.name" class="group-title">
84 <i v-if="group.icon" :class="group.icon"></i>
85 {{ group.name }}
86 </h2>
87 <Service
88 v-for="item in group.items"
89 v-bind:item="item"
90 :key="item.url"
91 />
92 </div>
93 </div>
94 </div>
95 </div>
96 </section>
97
98 <footer class="footer">
99 <div class="container">
100 <div
101 class="content has-text-centered"
102 v-if="config.footer"
103 v-html="config.footer"
104 ></div>
105 </div>
106 </footer>
107 </div>
108</template>
109
110<script>
111const jsyaml = require("js-yaml");
112const merge = require("lodash.merge");
113
114import Navbar from "./components/Navbar.vue";
115import ConnectivityChecker from "./components/ConnectivityChecker.vue";
116import Service from "./components/Service.vue";
117import Message from "./components/Message.vue";
118import SearchInput from "./components/SearchInput.vue";
119import SettingToggle from "./components/SettingToggle.vue";
120import DarkMode from "./components/DarkMode.vue";
121import DynamicTheme from "./components/DynamicTheme.vue";
122
123import defaultConfig from "./assets/defaults.yml";
124
125export default {
126 name: "App",
127 components: {
128 Navbar,
129 ConnectivityChecker,
130 Service,
131 Message,
132 SearchInput,
133 SettingToggle,
134 DarkMode,
135 DynamicTheme
136 },
137 data: function () {
138 return {
139 config: null,
140 services: null,
141 offline: false,
142 filter: "",
143 vlayout: true,
144 isDark: null,
145 showMenu: false
146 };
147 },
148 created: async function () {
149 try {
150 const defaults = jsyaml.load(defaultConfig);
151 let config = await this.getConfig();
152
153 this.config = merge(defaults, config);
154
155 console.log(this.config);
156 this.services = this.config.services;
157 document.title = `${this.config.title} | ${this.config.subtitle}`;
158 } catch (error) {
159 this.offline = true;
160 }
161 },
162 methods: {
163 getConfig: function () {
164 return fetch("config.yml").then(function (response) {
165 if (response.status != 200) {
166 return;
167 }
168 return response.text().then(function (body) {
169 return jsyaml.load(body);
170 });
171 });
172 },
173 matchesFilter: function (item) {
174 return (
175 item.name.toLowerCase().includes(this.filter) ||
176 (item.tag && item.tag.toLowerCase().includes(this.filter))
177 );
178 },
179 navigateToFirstService: function (target) {
180 try {
181 const service = this.services[0].items[0];
182 window.open(service.url, target || service.target || "_self");
183 } catch (error) {
184 console.warning("fail to open service");
185 }
186 },
187 filterServices: function (filter) {
188 this.filter = filter;
189
190 if (!filter) {
191 this.services = this.config.services;
192 return;
193 }
194
195 const searchResultItems = [];
196 for (const group of this.config.services) {
197 for (const item of group.items) {
198 if (this.matchesFilter(item)) {
199 searchResultItems.push(item);
200 }
201 }
202 }
203
204 this.services = [
205 {
206 name: filter,
207 icon: "fas fa-search",
208 items: searchResultItems
209 }
210 ];
211 }
212 }
213};
214</script>
diff --git a/src/assets/app.scss b/src/assets/app.scss
new file mode 100644
index 0000000..4b69864
--- /dev/null
+++ b/src/assets/app.scss
@@ -0,0 +1,336 @@
1@charset "utf-8";
2
3@import url("//fonts.googleapis.com/css?family=Lato:400,700|Pacifico|Raleway&display=swap");
4@import "bulma";
5
6// Themes import
7@import "./themes/sui.scss";
8
9@mixin ellipsis() {
10 white-space: nowrap;
11 overflow: hidden;
12 text-overflow: ellipsis;
13}
14
15html {
16 height: 100%;
17}
18
19body {
20 font-family: "Raleway", sans-serif;
21 height: 100%;
22
23 #app {
24 min-height: 100%;
25 background-color: var(--background);
26 color: var(--text);
27 transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
28
29 a {
30 &:hover {
31 color: var(--link-hover);
32 }
33 }
34
35 .title {
36 color: var(--text-title);
37 }
38 .subtitle {
39 color: var(--text-subtitle);
40 }
41
42 .card {
43 background-color: var(--card-background);
44 box-shadow: 0 2px 15px 0 var(--card-shadow);
45 &:hover {
46 background-color: var(--card-background);
47 }
48 }
49
50 .message {
51 background-color: var(--card-background);
52 .message-body {
53 color: var(--text);
54 }
55 }
56
57 .footer {
58 background-color: var(--card-background);
59 box-shadow: 0 2px 15px 0 var(--card-shadow);
60 }
61 }
62
63 h1,
64 h2,
65 h3,
66 h4,
67 h5,
68 h6 {
69 font-family: "Lato", sans-serif;
70 }
71
72 h1 {
73 font-size: 2rem;
74 }
75
76 h2 {
77 font-size: 1.7rem;
78 margin-top: 2rem;
79 margin-bottom: 1rem;
80
81 .fas,
82 .fab,
83 .far {
84 margin-right: 10px;
85 }
86
87 span {
88 font-weight: bold;
89 color: var(--highlight-secondary);
90 }
91 }
92
93 [v-cloak] {
94 display: none;
95 }
96
97 #bighead {
98 color: var(--text-header);
99
100 .dashboard-title {
101 padding: 6px 0 0 80px;
102 }
103
104 .first-line {
105 height: 100px;
106 vertical-align: center;
107 background-color: var(--highlight-primary);
108
109 h1 {
110 margin-top: -12px;
111 font-size: 2rem;
112 }
113
114 .headline {
115 margin-top: 5px;
116 font-size: 0.9rem;
117 }
118
119 .container {
120 height: 80px;
121 padding: 10px 0;
122 }
123
124 .logo {
125 float: left;
126 i {
127 vertical-align: top;
128 padding: 8px 15px;
129 font-size: 50px;
130 }
131
132 img {
133 padding: 10px;
134 max-height: 70px;
135 max-width: 70px;
136 }
137 }
138 }
139 .navbar,
140 .navbar-menu {
141 background-color: var(--highlight-secondary);
142
143 a {
144 color: var(--text-header);
145 padding: 8px 12px;
146 &:hover,
147 &:focus {
148 color: var(--text-header);
149 background-color: var(--highlight-hover);
150 }
151 }
152 }
153 .navbar-end {
154 text-align: right;
155 }
156 }
157
158 #main-section {
159 margin-bottom: 2rem;
160 padding: 0;
161
162 h2 {
163 padding-bottom: 0px;
164 @include ellipsis();
165 }
166
167 .title {
168 font-size: 1.1em;
169 @include ellipsis();
170 }
171
172 .subtitle {
173 font-size: 0.9em;
174 @include ellipsis();
175 }
176
177 .container {
178 padding: 1.2rem 0.75rem;
179 }
180
181 .message {
182 margin-top: 45px;
183 box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
184
185 .message-header {
186 font-weight: bold;
187 }
188
189 .message-body {
190 border: none;
191 }
192 }
193 }
194
195 .media-content {
196 overflow: hidden;
197 text-overflow: inherit;
198 }
199
200 .tag {
201 color: var(--highlight-secondary);
202 background-color: var(--highlight-secondary);
203 position: absolute;
204 top: 1rem;
205 right: -0.2rem;
206 width: 3px;
207 overflow: hidden;
208 transition: all 0.2s ease-out;
209 padding: 0;
210
211 .tag-text {
212 display: none;
213 }
214 }
215
216 .card {
217 border-radius: 5px;
218 border: none;
219 box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
220 transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
221
222 a {
223 outline: none;
224 }
225 }
226
227 .card:hover {
228 transform: translate(0, -3px);
229
230 .tag {
231 width: auto;
232 color: #ffffff;
233 padding: 0 0.75em;
234
235 .tag-text {
236 display: block;
237 }
238 }
239 }
240
241 .card-content {
242 height: 85px;
243 padding: 1.3rem;
244 }
245
246 .layout-vertical {
247 .card {
248 border-radius: 0;
249 }
250
251 .column div:first-of-type .card {
252 border-radius: 5px 5px 0 0;
253 }
254
255 .column div:last-child .card {
256 border-radius: 0 0 5px 5px;
257 }
258 }
259
260 .footer {
261 position: fixed;
262 left: 0;
263 right: 0;
264 bottom: 0;
265 padding: 0.5rem;
266 text-align: left;
267 color: #676767;
268 font-size: 0.85rem;
269 transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
270 }
271
272 .no-footer {
273 #main-section {
274 margin-bottom: 0;
275 }
276
277 .footer {
278 display: none;
279 }
280 }
281
282 .search-bar {
283 position: relative;
284 display: inline-block;
285 input {
286 border: none;
287 background-color: var(--highlight-hover);
288 border-radius: 5px;
289 margin-top: 2px;
290 padding: 2px 12px 2px 30px;
291 transition: all 100ms linear;
292 color: #ffffff;
293 height: 30px;
294 width: 100px;
295
296 &:focus {
297 color: #000000;
298 width: 250px;
299 background-color: #ffffff;
300 }
301 }
302
303 .search-label::before {
304 font-family: "Font Awesome 5 Free";
305 position: absolute;
306 top: 14px;
307 left: 16px;
308 content: "\f002";
309 font-weight: 900;
310 width: 20px;
311 height: 20px;
312 color: #ffffff;
313 }
314
315 &:focus-within .search-label::before {
316 color: #6e6e6e;
317 }
318 }
319
320 .offline-message {
321 text-align: center;
322 margin: 35px 0;
323
324 svg {
325 font-size: 2rem;
326 }
327
328 svg.fa-redo-alt {
329 font-size: 1.3rem;
330 line-height: 1rem;
331 vertical-align: middle;
332 cursor: pointer;
333 color: #3273dc;
334 }
335 }
336}
diff --git a/src/assets/defaults.yml b/src/assets/defaults.yml
new file mode 100644
index 0000000..1909328
--- /dev/null
+++ b/src/assets/defaults.yml
@@ -0,0 +1,39 @@
1---
2# Default configuration
3
4title: "Dashboard"
5subtitle: "Homer"
6logo: "logo.png"
7
8header: true
9footer: '<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.
10
11theme: default
12colors:
13 light:
14 highlight-primary: "#3367d6"
15 highlight-secondary: "#4285f4"
16 highlight-hover: "#5a95f5"
17 background: "#f5f5f5"
18 card-background: "#ffffff"
19 text: "#363636"
20 text-header: "#ffffff"
21 text-title: "#303030"
22 text-subtitle: "#424242"
23 card-shadow: rgba(0, 0, 0, 0.1)
24 link-hover: "#363636"
25 dark:
26 highlight-primary: "#3367d6"
27 highlight-secondary: "#4285f4"
28 highlight-hover: "#5a95f5"
29 background: "#131313"
30 card-background: "#2b2b2b"
31 text: "#eaeaea"
32 text-header: "#ffffff"
33 text-title: "#fafafa"
34 text-subtitle: "#f5f5f5"
35 card-shadow: rgba(0, 0, 0, 0.4)
36 link-hover: "#ffdd57"
37
38links: []
39services: []
diff --git a/src/assets/themes/sui.scss b/src/assets/themes/sui.scss
new file mode 100644
index 0000000..f94433e
--- /dev/null
+++ b/src/assets/themes/sui.scss
@@ -0,0 +1,34 @@
1/*
2 * SUI theme
3 * Inpired by the great https://github.com/jeroenpardon/sui start page
4 * Author: @bastienwirtz
5 */
6body #app.theme-sui {
7 #bighead .dashboard-title {
8 padding: 65px 0 0 12px;
9
10 h1 {
11 margin-top: 0;
12 font-weight: bold;
13 font-size: 2.2rem;
14 }
15 }
16
17 .navbar .navbar-item:hover {
18 background-color: transparent;
19 }
20
21 .card,
22 .card:hover {
23 background-color: transparent;
24 box-shadow: none;
25
26 .title {
27 font-weight: bold;
28 }
29
30 .card-content {
31 padding: 0;
32 }
33 }
34}
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..a64ff3b
--- /dev/null
+++ b/src/components/Navbar.vue
@@ -0,0 +1,66 @@
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 v-for="link in links"
24 :key="link.url"
25 :href="link.url"
26 :target="link.target"
27 >
28 <i
29 v-if="link.icon"
30 style="margin-right: 6px;"
31 :class="link.icon"
32 ></i>
33 {{ link.name }}
34 </a>
35 </div>
36 <div class="navbar-end">
37 <slot></slot>
38 </div>
39 </div>
40 </div>
41 </nav>
42 </div>
43</template>
44
45<script>
46export default {
47 name: "Navbar",
48 props: {
49 open: {
50 type: Boolean,
51 default: false,
52 },
53 links: Array,
54 },
55 computed: {
56 showMenu: function () {
57 return this.open && this.isSmallScreen();
58 },
59 },
60 methods: {
61 isSmallScreen: function () {
62 return window.matchMedia("screen and (max-width: 1023px)").matches;
63 },
64 },
65};
66</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..d27b17b
--- /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">
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" />
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..94655bc
--- /dev/null
+++ b/src/components/SettingToggle.vue
@@ -0,0 +1,41 @@
1<template>
2 <a v-on:click="toggleSetting()" class="navbar-item is-inline-block-mobile">
3 <span v-show="value"><i :class="['fas', icon]"></i></span>
4 <span v-show="!value"><i :class="['fas', iconAlt]"></i></span>
5 <slot></slot>
6 </a>
7</template>
8
9<script>
10export default {
11 name: "SettingToggle",
12 props: {
13 name: String,
14 icon: String,
15 iconAlt: String,
16 },
17 data: function () {
18 return {
19 value: true,
20 };
21 },
22 created: function () {
23 if (!this.iconAlt) {
24 this.iconAlt = this.icon;
25 }
26
27 if (this.name in localStorage) {
28 this.value = JSON.parse(localStorage[this.name]);
29 }
30
31 this.$emit("updated", this.value);
32 },
33 methods: {
34 toggleSetting: function () {
35 this.value = !this.value;
36 localStorage[this.name] = this.value;
37 this.$emit("updated", this.value);
38 },
39 },
40};
41</script>
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..2095acf
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,20 @@
1import Vue from "vue";
2import App from "./App.vue";
3import "./registerServiceWorker";
4
5import "@fortawesome/fontawesome-free/css/all.css";
6import "@fortawesome/fontawesome-free/js/all.js";
7
8import "./assets/app.scss";
9
10Vue.config.productionTip = false;
11
12Vue.component("DynamicStyle", {
13 render: function (createElement) {
14 return createElement("style", this.$slots.default);
15 },
16});
17
18new Vue({
19 render: (h) => h(App),
20}).$mount("#app");
diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js
new file mode 100644
index 0000000..1473a0a
--- /dev/null
+++ b/src/registerServiceWorker.js
@@ -0,0 +1,34 @@
1/* eslint-disable no-console */
2
3import { register } from "register-service-worker";
4
5if (process.env.NODE_ENV === "production") {
6 register(`${process.env.BASE_URL}service-worker.js`, {
7 ready() {
8 console.log(
9 "App is being served from cache by a service worker.\n" +
10 "For more details, visit https://goo.gl/AFskqB"
11 );
12 },
13 registered() {
14 console.log("Service worker has been registered.");
15 },
16 cached() {
17 console.log("Content has been cached for offline use.");
18 },
19 updatefound() {
20 console.log("New content is downloading.");
21 },
22 updated() {
23 console.log("New content is available; please refresh.");
24 },
25 offline() {
26 console.log(
27 "No internet connection found. App is running in offline mode."
28 );
29 },
30 error(error) {
31 console.error("Error during service worker registration:", error);
32 },
33 });
34}