@ -0,0 +1,21 @@ |
|||
.DS_Store |
|||
node_modules |
|||
/dist |
|||
|
|||
# local env files |
|||
.env.local |
|||
.env.*.local |
|||
|
|||
# Log files |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Editor directories and files |
|||
.idea |
|||
.vscode |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
@ -0,0 +1,21 @@ |
|||
# Umbrel Dashboard UI |
|||
|
|||
## Project setup |
|||
``` |
|||
yarn install |
|||
``` |
|||
|
|||
### Compiles and hot-reloads for development |
|||
``` |
|||
yarn serve |
|||
``` |
|||
|
|||
### Compiles and minifies for production |
|||
``` |
|||
yarn build |
|||
``` |
|||
|
|||
### Lints and fixes files |
|||
``` |
|||
yarn lint |
|||
``` |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
presets: ["@vue/cli-plugin-babel/preset"] |
|||
}; |
@ -0,0 +1,55 @@ |
|||
{ |
|||
"name": "umbrel-dashboard", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"scripts": { |
|||
"serve": "vue-cli-service serve", |
|||
"build": "vue-cli-service build", |
|||
"lint": "vue-cli-service lint" |
|||
}, |
|||
"dependencies": { |
|||
"animate.css": "^3.7.2", |
|||
"bootstrap-vue": "^2.2.2", |
|||
"core-js": "^3.4.4", |
|||
"vue": "^2.6.10", |
|||
"vue-confetti": "^2.0.7", |
|||
"vue-router": "^3.1.3", |
|||
"vuex": "^3.1.2" |
|||
}, |
|||
"devDependencies": { |
|||
"@vue/cli-plugin-babel": "^4.1.0", |
|||
"@vue/cli-plugin-eslint": "^4.1.0", |
|||
"@vue/cli-plugin-router": "^4.1.0", |
|||
"@vue/cli-plugin-vuex": "^4.1.0", |
|||
"@vue/cli-service": "^4.1.0", |
|||
"@vue/eslint-config-prettier": "^5.0.0", |
|||
"babel-eslint": "^10.0.3", |
|||
"eslint": "^5.16.0", |
|||
"eslint-plugin-prettier": "^3.1.1", |
|||
"eslint-plugin-vue": "^5.0.0", |
|||
"prettier": "^1.19.1", |
|||
"sass": "^1.23.7", |
|||
"sass-loader": "^8.0.0", |
|||
"vue-template-compiler": "^2.6.10" |
|||
}, |
|||
"eslintConfig": { |
|||
"root": true, |
|||
"env": { |
|||
"node": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:vue/essential", |
|||
"@vue/prettier" |
|||
], |
|||
"rules": { |
|||
"no-console": "off" |
|||
}, |
|||
"parserOptions": { |
|||
"parser": "babel-eslint" |
|||
} |
|||
}, |
|||
"browserslist": [ |
|||
"> 1%", |
|||
"last 2 versions" |
|||
] |
|||
} |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,23 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
|
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> |
|||
<meta name="robots" content="noindex, nofollow" /> |
|||
<link rel="icon" href="<%= BASE_URL %>favicon.png"> |
|||
<title>Umbrel Dashboard UI</title> |
|||
</head> |
|||
|
|||
<body> |
|||
<noscript> |
|||
<strong>We're sorry but Umbrel |
|||
doesn't work properly without JavaScript enabled. Please enable it to |
|||
continue.</strong> |
|||
</noscript> |
|||
<div id="app"></div> |
|||
<!-- built files will be auto injected --> |
|||
</body> |
|||
|
|||
</html> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<div id="app"> |
|||
<router-view /> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="scss"> |
|||
@import "@/global-styles/design-system.scss"; |
|||
</style> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "App", |
|||
mounted() {} |
|||
}; |
|||
</script> |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,47 @@ |
|||
<template> |
|||
<b-input-group class="card-input-group"> |
|||
<!-- Todo: make it work with b-form-input + v-model --> |
|||
<input |
|||
:class="inputClass" |
|||
:placeholder="placeholder" |
|||
:type="showPassword ? 'text' : 'password'" |
|||
v-bind:value="value" |
|||
v-on:input="$emit('input', $event.target.value)" |
|||
/> |
|||
<b-input-group-append> |
|||
<b-button @click="togglePassword"> |
|||
<b-icon :icon="showPassword ? 'eye-slash-fill' : 'eye-fill'"></b-icon> |
|||
</b-button> |
|||
</b-input-group-append> |
|||
</b-input-group> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
value: String, |
|||
inputClass: String, |
|||
placeholder: String |
|||
}, |
|||
computed: { |
|||
showPassword() { |
|||
return this.state.showPassword; |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
state: { |
|||
showPassword: false |
|||
} |
|||
}; |
|||
}, |
|||
methods: { |
|||
togglePassword() { |
|||
return (this.state.showPassword = !this.state.showPassword); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
</style> |
@ -0,0 +1,17 @@ |
|||
|
|||
$black: #141821; |
|||
$purple: #5351FB; |
|||
$primary: $purple; |
|||
$green: #00CD98; |
|||
$red: #F46E6E; |
|||
$spacer: 1.5rem; |
|||
$body-color: $black; |
|||
$font-family-sans-serif: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
|||
$h1-font-size: $font-size-base * 5; |
|||
$h2-font-size: $font-size-base * 3; |
|||
$h3-font-size: $font-size-base * 2; |
|||
$headings-font-weight: 900; |
|||
$list-group-item-padding-y: 1.5rem; |
|||
$list-group-item-padding-x: 1.5rem; |
|||
$list-group-border-color: rgba($black, .08); |
|||
$link-hover-decoration: none; |
@ -0,0 +1,56 @@ |
|||
// Overrides |
|||
.btn, button { |
|||
outline: 0 !important; |
|||
border: none !important; |
|||
text-transform: uppercase; |
|||
box-shadow: none !important; |
|||
} |
|||
|
|||
.card { |
|||
background: #ffffff; |
|||
box-shadow: 0px 10px 30px rgba(209, 213, 223, 0.5); |
|||
border-radius: 1rem; |
|||
&:hover { |
|||
box-shadow: 0px 10px 40px rgba(209, 213, 223, 0.6); |
|||
} |
|||
border: none !important; |
|||
outline: 0 !important; |
|||
} |
|||
.card-input { |
|||
position: relative; |
|||
background: #ffffff; |
|||
box-shadow: 0px 10px 30px rgba(209, 213, 223, 0.5); |
|||
border-radius: 1rem; |
|||
height: calc(2.25em + 1.125rem + 2px); |
|||
padding: 0.5rem 1.5rem; |
|||
outline: 0 !important; |
|||
border: none !important; |
|||
|
|||
&:hover, |
|||
&:focus, |
|||
&:active { |
|||
box-shadow: 0px 10px 40px rgba(209, 213, 223, 0.6); |
|||
} |
|||
} |
|||
|
|||
.card-input-group { |
|||
.card-input { |
|||
padding-right: 3rem; |
|||
} |
|||
.input-group-append > .btn { |
|||
background: none !important; |
|||
color: #616877; |
|||
border: none !important; |
|||
outline: none !important; |
|||
box-shadow: none !important; |
|||
position: absolute; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
right: 0.5rem; |
|||
} |
|||
} |
|||
|
|||
.card-list { |
|||
max-height: 15rem; |
|||
overflow-y: scroll; |
|||
} |
@ -0,0 +1,10 @@ |
|||
$font-size-base: 1rem; |
|||
|
|||
* { |
|||
transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); |
|||
} |
|||
|
|||
@import './variables'; |
|||
@import 'node_modules/bootstrap/scss/bootstrap'; |
|||
@import 'node_modules/bootstrap-vue/src/index.scss'; |
|||
@import './custom.scss'; |
@ -0,0 +1,22 @@ |
|||
import Vue from "vue"; |
|||
import App from "./App.vue"; |
|||
import router from "./router"; |
|||
import store from "./store"; |
|||
|
|||
import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'; |
|||
|
|||
// import "@/global-styles/designsystem.scss";
|
|||
// import 'bootstrap/dist/css/bootstrap.css'
|
|||
// import 'bootstrap-vue/dist/bootstrap-vue.css'
|
|||
|
|||
|
|||
Vue.use(BootstrapVue) |
|||
Vue.use(BootstrapVueIcons) |
|||
|
|||
Vue.config.productionTip = false; |
|||
|
|||
new Vue({ |
|||
router, |
|||
store, |
|||
render: h => h(App) |
|||
}).$mount("#app"); |
@ -0,0 +1,21 @@ |
|||
import Vue from "vue"; |
|||
import VueRouter from "vue-router"; |
|||
import Home from "../views/Home.vue"; |
|||
|
|||
Vue.use(VueRouter); |
|||
|
|||
const routes = [ |
|||
{ |
|||
path: "/", |
|||
name: "home", |
|||
component: Home |
|||
} |
|||
]; |
|||
|
|||
const router = new VueRouter({ |
|||
mode: "history", |
|||
base: process.env.BASE_URL, |
|||
routes |
|||
}); |
|||
|
|||
export default router; |
@ -0,0 +1,54 @@ |
|||
import Vue from "vue"; |
|||
import Vuex from "vuex"; |
|||
|
|||
Vue.use(Vuex); |
|||
|
|||
// Initial State
|
|||
const state = { |
|||
onboardingStep: 0, |
|||
selectedWifi: "" |
|||
}; |
|||
|
|||
// Getters
|
|||
const getters = { |
|||
onboardingStep(state) { |
|||
return state.onboardingStep |
|||
}, |
|||
selectWifi(state) { |
|||
return state.selectWifi; |
|||
} |
|||
} |
|||
|
|||
// Mutations
|
|||
const mutations = { |
|||
nextStep(state) { |
|||
state.onboardingStep++ |
|||
}, |
|||
prevStep(state) { |
|||
state.onboardingStep-- |
|||
}, |
|||
selectWifi(state, networkName) { |
|||
state.selectedWifi = networkName; |
|||
} |
|||
} |
|||
|
|||
// Actions
|
|||
const actions = { |
|||
nextStep(context) { |
|||
context.commit('nextStep'); |
|||
}, |
|||
prevStep(context) { |
|||
context.commit('prevStep'); |
|||
}, |
|||
selectWifi(context, networkName) { |
|||
context.commit('selectWifi', networkName); |
|||
} |
|||
} |
|||
|
|||
export default new Vuex.Store({ |
|||
state, |
|||
mutations, |
|||
actions, |
|||
getters, |
|||
modules: {} |
|||
}); |
@ -0,0 +1,276 @@ |
|||
<template> |
|||
<div> |
|||
<div class="d-flex flex-column align-items-center justify-content-center min-vh-100 p-2"> |
|||
<img src="@/assets/logo.svg" class="mb-2" /> |
|||
|
|||
<h1 class="text-center mb-2">{{ steps[onboardingStep]["heading"] }}</h1> |
|||
<p class="text-muted w-75 text-center">{{ steps[onboardingStep]["text"] }}</p> |
|||
|
|||
<div class="form-container mt-3 d-flex flex-column form-container w-100 align-items-center"> |
|||
<b-form-input |
|||
v-model="name" |
|||
ref="name" |
|||
placeholder="Enter your name" |
|||
v-if="onboardingStep === 1" |
|||
class="card-input w-100" |
|||
autofocus |
|||
></b-form-input> |
|||
|
|||
<input-password |
|||
v-model="password" |
|||
ref="password" |
|||
v-if="onboardingStep === 2" |
|||
placeholder="Enter your password" |
|||
inputClass="card-input w-100" |
|||
/> |
|||
|
|||
<input-password |
|||
v-model="confirmPassword" |
|||
ref="confirmPassword" |
|||
placeholder="Re-enter your password" |
|||
v-if="onboardingStep === 3" |
|||
inputClass="card-input w-100" |
|||
/> |
|||
|
|||
<b-list-group class="card card-list w-100" v-if="onboardingStep === 4"> |
|||
<b-list-group-item |
|||
class="d-flex" |
|||
button |
|||
v-for="(network) in wifiNetworks" |
|||
v-bind:key="network.name" |
|||
@click="selectWifi(network)" |
|||
> |
|||
<img :src="wifiIcons[network.strength]" class="mr-3" /> |
|||
{{ network.name }} |
|||
</b-list-group-item> |
|||
</b-list-group> |
|||
|
|||
<input-password |
|||
v-model="wifiPassword" |
|||
ref="wifiPassword" |
|||
:placeholder="'Enter WiFi password for ' + selectedWifi" |
|||
v-if="onboardingStep === 5" |
|||
inputClass="card-input w-100" |
|||
/> |
|||
|
|||
<b-button |
|||
variant="success" |
|||
size="lg" |
|||
@click="nextStep" |
|||
v-if="onboardingStep !== 4 && onboardingStep !== 6" |
|||
:disabled="!isStepValid" |
|||
class="mt-3 px-4" |
|||
>{{ onboardingStep === 0 ? 'Start' : 'Next' }}</b-button> |
|||
|
|||
<b-button |
|||
variant="success" |
|||
size="lg" |
|||
@click="finish" |
|||
v-if="onboardingStep === 6" |
|||
class="mt-3 px-4" |
|||
>Continue to Dashboard</b-button> |
|||
|
|||
<b-button |
|||
variant="link" |
|||
size="sm" |
|||
@click="prevStep" |
|||
v-if="onboardingStep > 1 && onboardingStep !== 6" |
|||
class="mt-2" |
|||
>Back</b-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<b-progress :value="progress" height="1rem" class="onboarding-progress"></b-progress> |
|||
|
|||
<p v-if="errorMessage">{{ errorMessage }}</p> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import Vue from "vue"; |
|||
import VueConfetti from "vue-confetti"; |
|||
|
|||
import InputPassword from "@/components/InputPassword"; |
|||
|
|||
Vue.use(VueConfetti); |
|||
|
|||
const onboardingSteps = [ |
|||
{ |
|||
heading: "welcome to umbrel", |
|||
text: "Your journey to become a first-class Bitcoin citizen starts now" |
|||
}, |
|||
{ |
|||
heading: "let's start with your name", |
|||
text: |
|||
"Your name stays on your Umbrel Node and is never shared with a 3rd party." |
|||
}, |
|||
{ |
|||
heading: "set your password", |
|||
text: |
|||
"You'll need this password to securely access your Umbrel Node from anywhere." |
|||
}, |
|||
{ |
|||
heading: "confirm your password", |
|||
text: |
|||
"You'll need this password to securely access your Umbrel Node from anywhere." |
|||
}, |
|||
{ |
|||
heading: "connect umbrel to wifi", |
|||
text: "" |
|||
}, |
|||
{ |
|||
heading: "enter wifi password", |
|||
text: "" |
|||
}, |
|||
{ |
|||
heading: "that's it!", |
|||
text: |
|||
"Congratulations! Your Umbrel Node is now running and synchronizing the Bitcoin blockchain." |
|||
} |
|||
]; |
|||
|
|||
const wifiIcons = [ |
|||
require("@/assets/icon-wifi-1.svg"), |
|||
require("@/assets/icon-wifi-2.svg"), |
|||
require("@/assets/icon-wifi-3.svg") |
|||
]; |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
name: "", |
|||
password: "", |
|||
confirmPassword: "", |
|||
wifiPassword: "", |
|||
errorMessage: "", |
|||
selectedWifi: "", |
|||
steps: onboardingSteps, |
|||
wifiNetworks: [ |
|||
{ |
|||
name: "Interwebs@2.4Ghz", |
|||
strength: 1 |
|||
}, |
|||
{ |
|||
name: "Interwebs@5Ghz", |
|||
strength: 0 |
|||
}, |
|||
{ |
|||
name: "My Personal WiFi", |
|||
strength: 2 |
|||
}, |
|||
{ |
|||
name: "Random Access Network", |
|||
strength: 1 |
|||
}, |
|||
{ |
|||
name: "Interwebs 2020", |
|||
strength: 2 |
|||
}, |
|||
{ |
|||
name: "Sneaky Neighbour 4309", |
|||
strength: 0 |
|||
}, |
|||
{ |
|||
name: "Zoya Home NEW", |
|||
strength: 1 |
|||
} |
|||
], |
|||
wifiIcons |
|||
}; |
|||
}, |
|||
computed: { |
|||
onboardingStep() { |
|||
return this.$store.getters.onboardingStep; |
|||
}, |
|||
isStepValid() { |
|||
const { onboardingStep } = this.$store.getters; |
|||
|
|||
if (onboardingStep === 1) { |
|||
if (!/^[A-Za-z ]+$/.test(this.name)) { |
|||
return false; |
|||
} |
|||
if (this.name.length < 3) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (onboardingStep === 2) { |
|||
if (this.password.length < 6) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (onboardingStep === 3) { |
|||
if (this.confirmPassword !== this.password) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (onboardingStep === 4) { |
|||
if (this.selectedWifi === "") { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (onboardingStep === 5) { |
|||
return !!this.wifiPassword; |
|||
} |
|||
|
|||
return true; |
|||
}, |
|||
progress() { |
|||
const { onboardingStep } = this.$store.getters; |
|||
const totalSteps = onboardingSteps.length; |
|||
|
|||
return onboardingStep === 0 |
|||
? 0 |
|||
: Math.round((onboardingStep * 100) / (totalSteps - 1)); |
|||
} |
|||
}, |
|||
methods: { |
|||
nextStep() { |
|||
if (this.$store.getters.onboardingStep === 5) { |
|||
this.$confetti.start({ |
|||
particles: [ |
|||
{ |
|||
type: "rect" |
|||
} |
|||
] |
|||
}); |
|||
window.setTimeout(() => { |
|||
this.$confetti.stop(); |
|||
}, 3000); |
|||
} |
|||
return this.$store.dispatch("nextStep"); |
|||
}, |
|||
prevStep() { |
|||
return this.$store.dispatch("prevStep"); |
|||
}, |
|||
finish() { |
|||
return alert("You've reached the end of onboarding."); |
|||
}, |
|||
selectWifi({ name }) { |
|||
this.selectedWifi = name; |
|||
this.$store.dispatch("selectWifi", name); |
|||
return this.$store.dispatch("nextStep"); |
|||
} |
|||
}, |
|||
components: { |
|||
InputPassword |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.form-container { |
|||
max-width: 500px; |
|||
} |
|||
.onboarding-progress { |
|||
position: absolute; |
|||
width: 100%; |
|||
top: 0; |
|||
left: 0; |
|||
border-radius: 0; |
|||
background: transparent; |
|||
} |
|||
</style> |
@ -0,0 +1,11 @@ |
|||
module.exports = { |
|||
// css: {
|
|||
// loaderOptions: {
|
|||
// sass: {
|
|||
// prependData: `
|
|||
// @import "@/global-styles/designs-ystem.scss";
|
|||
// `
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
} |