diff options
Diffstat (limited to 'src')
21 files changed, 1236 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..8185da2 --- /dev/null +++ b/src/App.vue | |||
@@ -0,0 +1,212 @@ | |||
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" alt="dashboard 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> | ||
111 | const jsyaml = require("js-yaml"); | ||
112 | const merge = require("lodash.merge"); | ||
113 | |||
114 | import Navbar from "./components/Navbar.vue"; | ||
115 | import ConnectivityChecker from "./components/ConnectivityChecker.vue"; | ||
116 | import Service from "./components/Service.vue"; | ||
117 | import Message from "./components/Message.vue"; | ||
118 | import SearchInput from "./components/SearchInput.vue"; | ||
119 | import SettingToggle from "./components/SettingToggle.vue"; | ||
120 | import DarkMode from "./components/DarkMode.vue"; | ||
121 | import DynamicTheme from "./components/DynamicTheme.vue"; | ||
122 | |||
123 | import defaultConfig from "./assets/defaults.yml"; | ||
124 | |||
125 | export 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 | this.services = this.config.services; | ||
155 | document.title = `${this.config.title} | ${this.config.subtitle}`; | ||
156 | } catch (error) { | ||
157 | this.offline = true; | ||
158 | } | ||
159 | }, | ||
160 | methods: { | ||
161 | getConfig: function () { | ||
162 | return fetch("config.yml").then(function (response) { | ||
163 | if (response.status != 200) { | ||
164 | return; | ||
165 | } | ||
166 | return response.text().then(function (body) { | ||
167 | return jsyaml.load(body); | ||
168 | }); | ||
169 | }); | ||
170 | }, | ||
171 | matchesFilter: function (item) { | ||
172 | return ( | ||
173 | item.name.toLowerCase().includes(this.filter) || | ||
174 | (item.tag && item.tag.toLowerCase().includes(this.filter)) | ||
175 | ); | ||
176 | }, | ||
177 | navigateToFirstService: function (target) { | ||
178 | try { | ||
179 | const service = this.services[0].items[0]; | ||
180 | window.open(service.url, target || service.target || "_self"); | ||
181 | } catch (error) { | ||
182 | console.warning("fail to open service"); | ||
183 | } | ||
184 | }, | ||
185 | filterServices: function (filter) { | ||
186 | this.filter = filter; | ||
187 | |||
188 | if (!filter) { | ||
189 | this.services = this.config.services; | ||
190 | return; | ||
191 | } | ||
192 | |||
193 | const searchResultItems = []; | ||
194 | for (const group of this.config.services) { | ||
195 | for (const item of group.items) { | ||
196 | if (this.matchesFilter(item)) { | ||
197 | searchResultItems.push(item); | ||
198 | } | ||
199 | } | ||
200 | } | ||
201 | |||
202 | this.services = [ | ||
203 | { | ||
204 | name: filter, | ||
205 | icon: "fas fa-search", | ||
206 | items: searchResultItems, | ||
207 | }, | ||
208 | ]; | ||
209 | }, | ||
210 | }, | ||
211 | }; | ||
212 | </script> | ||
diff --git a/src/assets/app.scss b/src/assets/app.scss new file mode 100644 index 0000000..001e3a4 --- /dev/null +++ b/src/assets/app.scss | |||
@@ -0,0 +1,337 @@ | |||
1 | @charset "utf-8"; | ||
2 | |||
3 | @import "./webfonts/webfonts.scss"; | ||
4 | |||
5 | @import "bulma"; | ||
6 | |||
7 | // Themes import | ||
8 | @import "./themes/sui.scss"; | ||
9 | |||
10 | @mixin ellipsis() { | ||
11 | white-space: nowrap; | ||
12 | overflow: hidden; | ||
13 | text-overflow: ellipsis; | ||
14 | } | ||
15 | |||
16 | html { | ||
17 | height: 100%; | ||
18 | } | ||
19 | |||
20 | body { | ||
21 | font-family: "Raleway", sans-serif; | ||
22 | height: 100%; | ||
23 | |||
24 | #app { | ||
25 | min-height: 100%; | ||
26 | background-color: var(--background); | ||
27 | color: var(--text); | ||
28 | transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms; | ||
29 | |||
30 | a { | ||
31 | &:hover { | ||
32 | color: var(--link-hover); | ||
33 | } | ||
34 | } | ||
35 | |||
36 | .title { | ||
37 | color: var(--text-title); | ||
38 | } | ||
39 | .subtitle { | ||
40 | color: var(--text-subtitle); | ||
41 | } | ||
42 | |||
43 | .card { | ||
44 | background-color: var(--card-background); | ||
45 | box-shadow: 0 2px 15px 0 var(--card-shadow); | ||
46 | &:hover { | ||
47 | background-color: var(--card-background); | ||
48 | } | ||
49 | } | ||
50 | |||
51 | .message { | ||
52 | background-color: var(--card-background); | ||
53 | .message-body { | ||
54 | color: var(--text); | ||
55 | } | ||
56 | } | ||
57 | |||
58 | .footer { | ||
59 | background-color: var(--card-background); | ||
60 | box-shadow: 0 2px 15px 0 var(--card-shadow); | ||
61 | } | ||
62 | } | ||
63 | |||
64 | h1, | ||
65 | h2, | ||
66 | h3, | ||
67 | h4, | ||
68 | h5, | ||
69 | h6 { | ||
70 | font-family: "Lato", sans-serif; | ||
71 | } | ||
72 | |||
73 | h1 { | ||
74 | font-size: 2rem; | ||
75 | } | ||
76 | |||
77 | h2 { | ||
78 | font-size: 1.7rem; | ||
79 | margin-top: 2rem; | ||
80 | margin-bottom: 1rem; | ||
81 | |||
82 | .fas, | ||
83 | .fab, | ||
84 | .far { | ||
85 | margin-right: 10px; | ||
86 | } | ||
87 | |||
88 | span { | ||
89 | font-weight: bold; | ||
90 | color: var(--highlight-secondary); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | [v-cloak] { | ||
95 | display: none; | ||
96 | } | ||
97 | |||
98 | #bighead { | ||
99 | color: var(--text-header); | ||
100 | |||
101 | .dashboard-title { | ||
102 | padding: 6px 0 0 80px; | ||
103 | } | ||
104 | |||
105 | .first-line { | ||
106 | height: 100px; | ||
107 | vertical-align: center; | ||
108 | background-color: var(--highlight-primary); | ||
109 | |||
110 | h1 { | ||
111 | margin-top: -12px; | ||
112 | font-size: 2rem; | ||
113 | } | ||
114 | |||
115 | .headline { | ||
116 | margin-top: 5px; | ||
117 | font-size: 0.9rem; | ||
118 | } | ||
119 | |||
120 | .container { | ||
121 | height: 80px; | ||
122 | padding: 10px 0; | ||
123 | } | ||
124 | |||
125 | .logo { | ||
126 | float: left; | ||
127 | i { | ||
128 | vertical-align: top; | ||
129 | padding: 8px 15px; | ||
130 | font-size: 50px; | ||
131 | } | ||
132 | |||
133 | img { | ||
134 | padding: 10px; | ||
135 | max-height: 70px; | ||
136 | max-width: 70px; | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | .navbar, | ||
141 | .navbar-menu { | ||
142 | background-color: var(--highlight-secondary); | ||
143 | |||
144 | a { | ||
145 | color: var(--text-header); | ||
146 | padding: 8px 12px; | ||
147 | &:hover, | ||
148 | &:focus { | ||
149 | color: var(--text-header); | ||
150 | background-color: var(--highlight-hover); | ||
151 | } | ||
152 | } | ||
153 | } | ||
154 | .navbar-end { | ||
155 | text-align: right; | ||
156 | } | ||
157 | } | ||
158 | |||
159 | #main-section { | ||
160 | margin-bottom: 2rem; | ||
161 | padding: 0; | ||
162 | |||
163 | h2 { | ||
164 | padding-bottom: 0px; | ||
165 | @include ellipsis(); | ||
166 | } | ||
167 | |||
168 | .title { | ||
169 | font-size: 1.1em; | ||
170 | @include ellipsis(); | ||
171 | } | ||
172 | |||
173 | .subtitle { | ||
174 | font-size: 0.9em; | ||
175 | @include ellipsis(); | ||
176 | } | ||
177 | |||
178 | .container { | ||
179 | padding: 1.2rem 0.75rem; | ||
180 | } | ||
181 | |||
182 | .message { | ||
183 | margin-top: 45px; | ||
184 | box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1); | ||
185 | |||
186 | .message-header { | ||
187 | font-weight: bold; | ||
188 | } | ||
189 | |||
190 | .message-body { | ||
191 | border: none; | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | .media-content { | ||
197 | overflow: hidden; | ||
198 | text-overflow: inherit; | ||
199 | } | ||
200 | |||
201 | .tag { | ||
202 | color: var(--highlight-secondary); | ||
203 | background-color: var(--highlight-secondary); | ||
204 | position: absolute; | ||
205 | top: 1rem; | ||
206 | right: -0.2rem; | ||
207 | width: 3px; | ||
208 | overflow: hidden; | ||
209 | transition: all 0.2s ease-out; | ||
210 | padding: 0; | ||
211 | |||
212 | .tag-text { | ||
213 | display: none; | ||
214 | } | ||
215 | } | ||
216 | |||
217 | .card { | ||
218 | border-radius: 5px; | ||
219 | border: none; | ||
220 | box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1); | ||
221 | transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms; | ||
222 | |||
223 | a { | ||
224 | outline: none; | ||
225 | } | ||
226 | } | ||
227 | |||
228 | .card:hover { | ||
229 | transform: translate(0, -3px); | ||
230 | |||
231 | .tag { | ||
232 | width: auto; | ||
233 | color: #ffffff; | ||
234 | padding: 0 0.75em; | ||
235 | |||
236 | .tag-text { | ||
237 | display: block; | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | .card-content { | ||
243 | height: 85px; | ||
244 | padding: 1.3rem; | ||
245 | } | ||
246 | |||
247 | .layout-vertical { | ||
248 | .card { | ||
249 | border-radius: 0; | ||
250 | } | ||
251 | |||
252 | .column div:first-of-type .card { | ||
253 | border-radius: 5px 5px 0 0; | ||
254 | } | ||
255 | |||
256 | .column div:last-child .card { | ||
257 | border-radius: 0 0 5px 5px; | ||
258 | } | ||
259 | } | ||
260 | |||
261 | .footer { | ||
262 | position: fixed; | ||
263 | left: 0; | ||
264 | right: 0; | ||
265 | bottom: 0; | ||
266 | padding: 0.5rem; | ||
267 | text-align: left; | ||
268 | color: #676767; | ||
269 | font-size: 0.85rem; | ||
270 | transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms; | ||
271 | } | ||
272 | |||
273 | .no-footer { | ||
274 | #main-section { | ||
275 | margin-bottom: 0; | ||
276 | } | ||
277 | |||
278 | .footer { | ||
279 | display: none; | ||
280 | } | ||
281 | } | ||
282 | |||
283 | .search-bar { | ||
284 | position: relative; | ||
285 | display: inline-block; | ||
286 | input { | ||
287 | border: none; | ||
288 | background-color: var(--highlight-hover); | ||
289 | border-radius: 5px; | ||
290 | margin-top: 2px; | ||
291 | padding: 2px 12px 2px 30px; | ||
292 | transition: all 100ms linear; | ||
293 | color: #ffffff; | ||
294 | height: 30px; | ||
295 | width: 100px; | ||
296 | |||
297 | &:focus { | ||
298 | color: #000000; | ||
299 | width: 250px; | ||
300 | background-color: #ffffff; | ||
301 | } | ||
302 | } | ||
303 | |||
304 | .search-label::before { | ||
305 | font-family: "Font Awesome 5 Free"; | ||
306 | position: absolute; | ||
307 | top: 14px; | ||
308 | left: 16px; | ||
309 | content: "\f002"; | ||
310 | font-weight: 900; | ||
311 | width: 20px; | ||
312 | height: 20px; | ||
313 | color: #ffffff; | ||
314 | } | ||
315 | |||
316 | &:focus-within .search-label::before { | ||
317 | color: #6e6e6e; | ||
318 | } | ||
319 | } | ||
320 | |||
321 | .offline-message { | ||
322 | text-align: center; | ||
323 | margin: 35px 0; | ||
324 | |||
325 | svg { | ||
326 | font-size: 2rem; | ||
327 | } | ||
328 | |||
329 | svg.fa-redo-alt { | ||
330 | font-size: 1.3rem; | ||
331 | line-height: 1rem; | ||
332 | vertical-align: middle; | ||
333 | cursor: pointer; | ||
334 | color: #3273dc; | ||
335 | } | ||
336 | } | ||
337 | } | ||
diff --git a/src/assets/defaults.yml b/src/assets/defaults.yml new file mode 100644 index 0000000..a699d28 --- /dev/null +++ b/src/assets/defaults.yml | |||
@@ -0,0 +1,39 @@ | |||
1 | --- | ||
2 | # Default configuration | ||
3 | |||
4 | title: "Dashboard" | ||
5 | subtitle: "Homer" | ||
6 | |||
7 | header: true | ||
8 | 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. | ||
9 | |||
10 | theme: default | ||
11 | colors: | ||
12 | light: | ||
13 | highlight-primary: "#3367d6" | ||
14 | highlight-secondary: "#4285f4" | ||
15 | highlight-hover: "#5a95f5" | ||
16 | background: "#f5f5f5" | ||
17 | card-background: "#ffffff" | ||
18 | text: "#363636" | ||
19 | text-header: "#ffffff" | ||
20 | text-title: "#303030" | ||
21 | text-subtitle: "#424242" | ||
22 | card-shadow: rgba(0, 0, 0, 0.1) | ||
23 | link-hover: "#363636" | ||
24 | dark: | ||
25 | highlight-primary: "#3367d6" | ||
26 | highlight-secondary: "#4285f4" | ||
27 | highlight-hover: "#5a95f5" | ||
28 | background: "#131313" | ||
29 | card-background: "#2b2b2b" | ||
30 | text: "#eaeaea" | ||
31 | text-header: "#ffffff" | ||
32 | text-title: "#fafafa" | ||
33 | text-subtitle: "#f5f5f5" | ||
34 | card-shadow: rgba(0, 0, 0, 0.4) | ||
35 | link-hover: "#ffdd57" | ||
36 | |||
37 | message: ~ | ||
38 | links: [] | ||
39 | services: [] | ||
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 | */ | ||
6 | body #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/assets/webfonts/lato/OFL.txt b/src/assets/webfonts/lato/OFL.txt new file mode 100644 index 0000000..dfca0da --- /dev/null +++ b/src/assets/webfonts/lato/OFL.txt | |||
@@ -0,0 +1,93 @@ | |||
1 | Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" | ||
2 | |||
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||
4 | This license is copied below, and is also available with a FAQ at: | ||
5 | http://scripts.sil.org/OFL | ||
6 | |||
7 | |||
8 | ----------------------------------------------------------- | ||
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||
10 | ----------------------------------------------------------- | ||
11 | |||
12 | PREAMBLE | ||
13 | The goals of the Open Font License (OFL) are to stimulate worldwide | ||
14 | development of collaborative font projects, to support the font creation | ||
15 | efforts of academic and linguistic communities, and to provide a free and | ||
16 | open framework in which fonts may be shared and improved in partnership | ||
17 | with others. | ||
18 | |||
19 | The OFL allows the licensed fonts to be used, studied, modified and | ||
20 | redistributed freely as long as they are not sold by themselves. The | ||
21 | fonts, including any derivative works, can be bundled, embedded, | ||
22 | redistributed and/or sold with any software provided that any reserved | ||
23 | names are not used by derivative works. The fonts and derivatives, | ||
24 | however, cannot be released under any other type of license. The | ||
25 | requirement for fonts to remain under this license does not apply | ||
26 | to any document created using the fonts or their derivatives. | ||
27 | |||
28 | DEFINITIONS | ||
29 | "Font Software" refers to the set of files released by the Copyright | ||
30 | Holder(s) under this license and clearly marked as such. This may | ||
31 | include source files, build scripts and documentation. | ||
32 | |||
33 | "Reserved Font Name" refers to any names specified as such after the | ||
34 | copyright statement(s). | ||
35 | |||
36 | "Original Version" refers to the collection of Font Software components as | ||
37 | distributed by the Copyright Holder(s). | ||
38 | |||
39 | "Modified Version" refers to any derivative made by adding to, deleting, | ||
40 | or substituting -- in part or in whole -- any of the components of the | ||
41 | Original Version, by changing formats or by porting the Font Software to a | ||
42 | new environment. | ||
43 | |||
44 | "Author" refers to any designer, engineer, programmer, technical | ||
45 | writer or other person who contributed to the Font Software. | ||
46 | |||
47 | PERMISSION & CONDITIONS | ||
48 | Permission is hereby granted, free of charge, to any person obtaining | ||
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||
50 | redistribute, and sell modified and unmodified copies of the Font | ||
51 | Software, subject to the following conditions: | ||
52 | |||
53 | 1) Neither the Font Software nor any of its individual components, | ||
54 | in Original or Modified Versions, may be sold by itself. | ||
55 | |||
56 | 2) Original or Modified Versions of the Font Software may be bundled, | ||
57 | redistributed and/or sold with any software, provided that each copy | ||
58 | contains the above copyright notice and this license. These can be | ||
59 | included either as stand-alone text files, human-readable headers or | ||
60 | in the appropriate machine-readable metadata fields within text or | ||
61 | binary files as long as those fields can be easily viewed by the user. | ||
62 | |||
63 | 3) No Modified Version of the Font Software may use the Reserved Font | ||
64 | Name(s) unless explicit written permission is granted by the corresponding | ||
65 | Copyright Holder. This restriction only applies to the primary font name as | ||
66 | presented to the users. | ||
67 | |||
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||
69 | Software shall not be used to promote, endorse or advertise any | ||
70 | Modified Version, except to acknowledge the contribution(s) of the | ||
71 | Copyright Holder(s) and the Author(s) or with their explicit written | ||
72 | permission. | ||
73 | |||
74 | 5) The Font Software, modified or unmodified, in part or in whole, | ||
75 | must be distributed entirely under this license, and must not be | ||
76 | distributed under any other license. The requirement for fonts to | ||
77 | remain under this license does not apply to any document created | ||
78 | using the Font Software. | ||
79 | |||
80 | TERMINATION | ||
81 | This license becomes null and void if any of the above conditions are | ||
82 | not met. | ||
83 | |||
84 | DISCLAIMER | ||
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||
93 | OTHER DEALINGS IN THE FONT SOFTWARE. | ||
diff --git a/src/assets/webfonts/lato/lato-v16-latin-regular.woff b/src/assets/webfonts/lato/lato-v16-latin-regular.woff new file mode 100644 index 0000000..189a0fe --- /dev/null +++ b/src/assets/webfonts/lato/lato-v16-latin-regular.woff | |||
Binary files differ | |||
diff --git a/src/assets/webfonts/lato/lato-v16-latin-regular.woff2 b/src/assets/webfonts/lato/lato-v16-latin-regular.woff2 new file mode 100644 index 0000000..6904b66 --- /dev/null +++ b/src/assets/webfonts/lato/lato-v16-latin-regular.woff2 | |||
Binary files differ | |||
diff --git a/src/assets/webfonts/raleway/OFL.txt b/src/assets/webfonts/raleway/OFL.txt new file mode 100644 index 0000000..3219811 --- /dev/null +++ b/src/assets/webfonts/raleway/OFL.txt | |||
@@ -0,0 +1,95 @@ | |||
1 | Copyright (c) 2010, Matt McInerney (matt@pixelspread.com), | ||
2 | Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com), | ||
3 | Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name Raleway | ||
4 | |||
5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||
6 | This license is copied below, and is also available with a FAQ at: | ||
7 | http://scripts.sil.org/OFL | ||
8 | |||
9 | |||
10 | ----------------------------------------------------------- | ||
11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||
12 | ----------------------------------------------------------- | ||
13 | |||
14 | PREAMBLE | ||
15 | The goals of the Open Font License (OFL) are to stimulate worldwide | ||
16 | development of collaborative font projects, to support the font creation | ||
17 | efforts of academic and linguistic communities, and to provide a free and | ||
18 | open framework in which fonts may be shared and improved in partnership | ||
19 | with others. | ||
20 | |||
21 | The OFL allows the licensed fonts to be used, studied, modified and | ||
22 | redistributed freely as long as they are not sold by themselves. The | ||
23 | fonts, including any derivative works, can be bundled, embedded, | ||
24 | redistributed and/or sold with any software provided that any reserved | ||
25 | names are not used by derivative works. The fonts and derivatives, | ||
26 | however, cannot be released under any other type of license. The | ||
27 | requirement for fonts to remain under this license does not apply | ||
28 | to any document created using the fonts or their derivatives. | ||
29 | |||
30 | DEFINITIONS | ||
31 | "Font Software" refers to the set of files released by the Copyright | ||
32 | Holder(s) under this license and clearly marked as such. This may | ||
33 | include source files, build scripts and documentation. | ||
34 | |||
35 | "Reserved Font Name" refers to any names specified as such after the | ||
36 | copyright statement(s). | ||
37 | |||
38 | "Original Version" refers to the collection of Font Software components as | ||
39 | distributed by the Copyright Holder(s). | ||
40 | |||
41 | "Modified Version" refers to any derivative made by adding to, deleting, | ||
42 | or substituting -- in part or in whole -- any of the components of the | ||
43 | Original Version, by changing formats or by porting the Font Software to a | ||
44 | new environment. | ||
45 | |||
46 | "Author" refers to any designer, engineer, programmer, technical | ||
47 | writer or other person who contributed to the Font Software. | ||
48 | |||
49 | PERMISSION & CONDITIONS | ||
50 | Permission is hereby granted, free of charge, to any person obtaining | ||
51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||
52 | redistribute, and sell modified and unmodified copies of the Font | ||
53 | Software, subject to the following conditions: | ||
54 | |||
55 | 1) Neither the Font Software nor any of its individual components, | ||
56 | in Original or Modified Versions, may be sold by itself. | ||
57 | |||
58 | 2) Original or Modified Versions of the Font Software may be bundled, | ||
59 | redistributed and/or sold with any software, provided that each copy | ||
60 | contains the above copyright notice and this license. These can be | ||
61 | included either as stand-alone text files, human-readable headers or | ||
62 | in the appropriate machine-readable metadata fields within text or | ||
63 | binary files as long as those fields can be easily viewed by the user. | ||
64 | |||
65 | 3) No Modified Version of the Font Software may use the Reserved Font | ||
66 | Name(s) unless explicit written permission is granted by the corresponding | ||
67 | Copyright Holder. This restriction only applies to the primary font name as | ||
68 | presented to the users. | ||
69 | |||
70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||
71 | Software shall not be used to promote, endorse or advertise any | ||
72 | Modified Version, except to acknowledge the contribution(s) of the | ||
73 | Copyright Holder(s) and the Author(s) or with their explicit written | ||
74 | permission. | ||
75 | |||
76 | 5) The Font Software, modified or unmodified, in part or in whole, | ||
77 | must be distributed entirely under this license, and must not be | ||
78 | distributed under any other license. The requirement for fonts to | ||
79 | remain under this license does not apply to any document created | ||
80 | using the Font Software. | ||
81 | |||
82 | TERMINATION | ||
83 | This license becomes null and void if any of the above conditions are | ||
84 | not met. | ||
85 | |||
86 | DISCLAIMER | ||
87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||
89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||
90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||
91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||
92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||
93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||
95 | OTHER DEALINGS IN THE FONT SOFTWARE. | ||
diff --git a/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff b/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff new file mode 100644 index 0000000..ab3a51b --- /dev/null +++ b/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff | |||
Binary files differ | |||
diff --git a/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff2 b/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff2 new file mode 100644 index 0000000..86b505e --- /dev/null +++ b/src/assets/webfonts/raleway/raleway-v14-latin-regular.woff2 | |||
Binary files differ | |||
diff --git a/src/assets/webfonts/webfonts.scss b/src/assets/webfonts/webfonts.scss new file mode 100644 index 0000000..df3b56b --- /dev/null +++ b/src/assets/webfonts/webfonts.scss | |||
@@ -0,0 +1,23 @@ | |||
1 | /* raleway-regular - latin */ | ||
2 | @font-face { | ||
3 | font-family: "Raleway"; | ||
4 | font-style: normal; | ||
5 | font-weight: 400; | ||
6 | font-display: swap; | ||
7 | src: local("Raleway"), local("Raleway-Regular"), | ||
8 | url("./webfonts/raleway/raleway-v14-latin-regular.woff2") format("woff2"), | ||
9 | /* Chrome 26+, Opera 23+, Firefox 39+ */ | ||
10 | url("./webfonts/raleway/raleway-v14-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ | ||
11 | } | ||
12 | |||
13 | /* lato-regular - latin */ | ||
14 | @font-face { | ||
15 | font-family: "Lato"; | ||
16 | font-style: normal; | ||
17 | font-weight: 400; | ||
18 | font-display: swap; | ||
19 | src: local("Lato Regular"), local("Lato-Regular"), | ||
20 | url("./webfonts/lato/lato-v16-latin-regular.woff2") format("woff2"), | ||
21 | /* Chrome 26+, Opera 23+, Firefox 39+ */ | ||
22 | url("./webfonts/lato/lato-v16-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ | ||
23 | } | ||
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> | ||
diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..e5995a4 --- /dev/null +++ b/src/main.js | |||
@@ -0,0 +1,19 @@ | |||
1 | import Vue from "vue"; | ||
2 | import App from "./App.vue"; | ||
3 | import "./registerServiceWorker"; | ||
4 | |||
5 | import "@fortawesome/fontawesome-free/css/all.css"; | ||
6 | |||
7 | import "./assets/app.scss"; | ||
8 | |||
9 | Vue.config.productionTip = false; | ||
10 | |||
11 | Vue.component("DynamicStyle", { | ||
12 | render: function (createElement) { | ||
13 | return createElement("style", this.$slots.default); | ||
14 | }, | ||
15 | }); | ||
16 | |||
17 | new Vue({ | ||
18 | render: (h) => h(App), | ||
19 | }).$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 | |||
3 | import { register } from "register-service-worker"; | ||
4 | |||
5 | if (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 | } | ||