aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorBastien Wirtz <bastien.wirtz@gmail.com>2020-05-30 23:22:02 -0700
committerGitHub <noreply@github.com>2020-05-30 23:22:02 -0700
commit5fa6b6cfa6b3010279ead23088add5c5664e8ac0 (patch)
tree5f3ffa4dc62b4355d38346ef0155878ca6aeedcd /src
parentab7ac44c191e3b7dea696e76b74097e23f73b18c (diff)
parent9052ec59b75a37b4518ad39c493ee6c2d4198b98 (diff)
downloadhomer-5fa6b6cfa6b3010279ead23088add5c5664e8ac0.tar.gz
homer-5fa6b6cfa6b3010279ead23088add5c5664e8ac0.tar.zst
homer-5fa6b6cfa6b3010279ead23088add5c5664e8ac0.zip
Merge pull request #62 from bastienwirtz/dev/build-system120405250
Build system integration using vue-cli.
Diffstat (limited to 'src')
-rw-r--r--src/App.vue212
-rw-r--r--src/assets/app.scss337
-rw-r--r--src/assets/defaults.yml39
-rw-r--r--src/assets/themes/sui.scss34
-rw-r--r--src/assets/webfonts/lato/OFL.txt93
-rw-r--r--src/assets/webfonts/lato/lato-v16-latin-regular.woffbin0 -> 28660 bytes
-rw-r--r--src/assets/webfonts/lato/lato-v16-latin-regular.woff2bin0 -> 23484 bytes
-rw-r--r--src/assets/webfonts/raleway/OFL.txt95
-rw-r--r--src/assets/webfonts/raleway/raleway-v14-latin-regular.woffbin0 -> 25244 bytes
-rw-r--r--src/assets/webfonts/raleway/raleway-v14-latin-regular.woff2bin0 -> 20724 bytes
-rw-r--r--src/assets/webfonts/webfonts.scss23
-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
-rw-r--r--src/main.js19
-rw-r--r--src/registerServiceWorker.js34
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>
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 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
16html {
17 height: 100%;
18}
19
20body {
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
4title: "Dashboard"
5subtitle: "Homer"
6
7header: true
8footer: '<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
10theme: default
11colors:
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
37message: ~
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/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 @@
1Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
2
3This Font Software is licensed under the SIL Open Font License, Version 1.1.
4This license is copied below, and is also available with a FAQ at:
5http://scripts.sil.org/OFL
6
7
8-----------------------------------------------------------
9SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10-----------------------------------------------------------
11
12PREAMBLE
13The goals of the Open Font License (OFL) are to stimulate worldwide
14development of collaborative font projects, to support the font creation
15efforts of academic and linguistic communities, and to provide a free and
16open framework in which fonts may be shared and improved in partnership
17with others.
18
19The OFL allows the licensed fonts to be used, studied, modified and
20redistributed freely as long as they are not sold by themselves. The
21fonts, including any derivative works, can be bundled, embedded,
22redistributed and/or sold with any software provided that any reserved
23names are not used by derivative works. The fonts and derivatives,
24however, cannot be released under any other type of license. The
25requirement for fonts to remain under this license does not apply
26to any document created using the fonts or their derivatives.
27
28DEFINITIONS
29"Font Software" refers to the set of files released by the Copyright
30Holder(s) under this license and clearly marked as such. This may
31include source files, build scripts and documentation.
32
33"Reserved Font Name" refers to any names specified as such after the
34copyright statement(s).
35
36"Original Version" refers to the collection of Font Software components as
37distributed by the Copyright Holder(s).
38
39"Modified Version" refers to any derivative made by adding to, deleting,
40or substituting -- in part or in whole -- any of the components of the
41Original Version, by changing formats or by porting the Font Software to a
42new environment.
43
44"Author" refers to any designer, engineer, programmer, technical
45writer or other person who contributed to the Font Software.
46
47PERMISSION & CONDITIONS
48Permission is hereby granted, free of charge, to any person obtaining
49a copy of the Font Software, to use, study, copy, merge, embed, modify,
50redistribute, and sell modified and unmodified copies of the Font
51Software, subject to the following conditions:
52
531) Neither the Font Software nor any of its individual components,
54in Original or Modified Versions, may be sold by itself.
55
562) Original or Modified Versions of the Font Software may be bundled,
57redistributed and/or sold with any software, provided that each copy
58contains the above copyright notice and this license. These can be
59included either as stand-alone text files, human-readable headers or
60in the appropriate machine-readable metadata fields within text or
61binary files as long as those fields can be easily viewed by the user.
62
633) No Modified Version of the Font Software may use the Reserved Font
64Name(s) unless explicit written permission is granted by the corresponding
65Copyright Holder. This restriction only applies to the primary font name as
66presented to the users.
67
684) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69Software shall not be used to promote, endorse or advertise any
70Modified Version, except to acknowledge the contribution(s) of the
71Copyright Holder(s) and the Author(s) or with their explicit written
72permission.
73
745) The Font Software, modified or unmodified, in part or in whole,
75must be distributed entirely under this license, and must not be
76distributed under any other license. The requirement for fonts to
77remain under this license does not apply to any document created
78using the Font Software.
79
80TERMINATION
81This license becomes null and void if any of the above conditions are
82not met.
83
84DISCLAIMER
85THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93OTHER 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 @@
1Copyright (c) 2010, Matt McInerney (matt@pixelspread.com),
2Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com),
3Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name Raleway
4
5This Font Software is licensed under the SIL Open Font License, Version 1.1.
6This license is copied below, and is also available with a FAQ at:
7http://scripts.sil.org/OFL
8
9
10-----------------------------------------------------------
11SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
12-----------------------------------------------------------
13
14PREAMBLE
15The goals of the Open Font License (OFL) are to stimulate worldwide
16development of collaborative font projects, to support the font creation
17efforts of academic and linguistic communities, and to provide a free and
18open framework in which fonts may be shared and improved in partnership
19with others.
20
21The OFL allows the licensed fonts to be used, studied, modified and
22redistributed freely as long as they are not sold by themselves. The
23fonts, including any derivative works, can be bundled, embedded,
24redistributed and/or sold with any software provided that any reserved
25names are not used by derivative works. The fonts and derivatives,
26however, cannot be released under any other type of license. The
27requirement for fonts to remain under this license does not apply
28to any document created using the fonts or their derivatives.
29
30DEFINITIONS
31"Font Software" refers to the set of files released by the Copyright
32Holder(s) under this license and clearly marked as such. This may
33include source files, build scripts and documentation.
34
35"Reserved Font Name" refers to any names specified as such after the
36copyright statement(s).
37
38"Original Version" refers to the collection of Font Software components as
39distributed by the Copyright Holder(s).
40
41"Modified Version" refers to any derivative made by adding to, deleting,
42or substituting -- in part or in whole -- any of the components of the
43Original Version, by changing formats or by porting the Font Software to a
44new environment.
45
46"Author" refers to any designer, engineer, programmer, technical
47writer or other person who contributed to the Font Software.
48
49PERMISSION & CONDITIONS
50Permission is hereby granted, free of charge, to any person obtaining
51a copy of the Font Software, to use, study, copy, merge, embed, modify,
52redistribute, and sell modified and unmodified copies of the Font
53Software, subject to the following conditions:
54
551) Neither the Font Software nor any of its individual components,
56in Original or Modified Versions, may be sold by itself.
57
582) Original or Modified Versions of the Font Software may be bundled,
59redistributed and/or sold with any software, provided that each copy
60contains the above copyright notice and this license. These can be
61included either as stand-alone text files, human-readable headers or
62in the appropriate machine-readable metadata fields within text or
63binary files as long as those fields can be easily viewed by the user.
64
653) No Modified Version of the Font Software may use the Reserved Font
66Name(s) unless explicit written permission is granted by the corresponding
67Copyright Holder. This restriction only applies to the primary font name as
68presented to the users.
69
704) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
71Software shall not be used to promote, endorse or advertise any
72Modified Version, except to acknowledge the contribution(s) of the
73Copyright Holder(s) and the Author(s) or with their explicit written
74permission.
75
765) The Font Software, modified or unmodified, in part or in whole,
77must be distributed entirely under this license, and must not be
78distributed under any other license. The requirement for fonts to
79remain under this license does not apply to any document created
80using the Font Software.
81
82TERMINATION
83This license becomes null and void if any of the above conditions are
84not met.
85
86DISCLAIMER
87THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
89MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
90OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
91COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
92INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
93DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
94FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
95OTHER 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>
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>
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 @@
1import Vue from "vue";
2import App from "./App.vue";
3import "./registerServiceWorker";
4
5import "@fortawesome/fontawesome-free/css/all.css";
6
7import "./assets/app.scss";
8
9Vue.config.productionTip = false;
10
11Vue.component("DynamicStyle", {
12 render: function (createElement) {
13 return createElement("style", this.$slots.default);
14 },
15});
16
17new 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
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}