@ -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";
|
||||
|
// `
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
|
} |