mirror of
https://github.com/Lissy93/dashy.git
synced 2025-12-06 16:43:13 +01:00
pin number functionatlity added
This commit is contained in:
parent
9c6686376e
commit
ebf45c5969
4 changed files with 235 additions and 55 deletions
70
src/components/InteractiveEditor/PinInput.vue
Normal file
70
src/components/InteractiveEditor/PinInput.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div class="pin-gate">
|
||||||
|
<div class="pin-head">
|
||||||
|
<i class="far fa-lock" aria-hidden="true"></i>
|
||||||
|
<span>Locked section</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormSchema
|
||||||
|
:schema="schema"
|
||||||
|
v-model="form"
|
||||||
|
name="pinInputForm"
|
||||||
|
class="pin-form"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="pin-actions">
|
||||||
|
<button class="pin-btn" @click="submit">Unlock</button>
|
||||||
|
<button class="pin-reset" @click="lockAgain" type="button">Lock again</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FormSchema from '@formschema/native';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PinInput',
|
||||||
|
components: { FormSchema },
|
||||||
|
props: {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: { pin: '' },
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pin: {
|
||||||
|
title: 'Enter PIN',
|
||||||
|
type: 'string',
|
||||||
|
attrs: {
|
||||||
|
type: 'password',
|
||||||
|
inputmode: 'numeric',
|
||||||
|
autocomplete: 'one-time-code',
|
||||||
|
placeholder: '••••',
|
||||||
|
class: 'pin-input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['pin'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit() {
|
||||||
|
const tried = String(this.form.pin || '');
|
||||||
|
this.$emit('unlock_attempt', {
|
||||||
|
pin: tried,
|
||||||
|
id: this.id,
|
||||||
|
});
|
||||||
|
// clear local field
|
||||||
|
this.form.pin = '';
|
||||||
|
},
|
||||||
|
lockAgain() {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
</style>
|
||||||
|
|
@ -14,63 +14,70 @@
|
||||||
:id="sectionRef"
|
:id="sectionRef"
|
||||||
:ref="sectionRef"
|
:ref="sectionRef"
|
||||||
>
|
>
|
||||||
<!-- If no items, show message -->
|
<PinInput
|
||||||
<div v-if="isEmpty" class="no-items">
|
v-if="showPinRequired"
|
||||||
{{ $t('home.no-items-section') }}
|
:id = "sectionRef"
|
||||||
</div>
|
@unlock_attempt="saveUnlockPins"
|
||||||
<!-- Item Container -->
|
/>
|
||||||
<div v-if="hasItems"
|
<div v-if="!showPinRequired">
|
||||||
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''} inner-size-${itemSize}`"
|
<!-- If no items, show message -->
|
||||||
:style="gridStyle" :id="`section-${groupId}`"
|
<div v-if="isEmpty" class="no-items">
|
||||||
> <!-- Show for each item -->
|
{{ $t('home.no-items-section') }}
|
||||||
<template v-for="(item) in sortedItems">
|
</div>
|
||||||
<SubItemGroup
|
<!-- Item Container -->
|
||||||
v-if="item.subItems"
|
<div v-if="hasItems"
|
||||||
:key="item.id"
|
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''} inner-size-${itemSize}`"
|
||||||
:itemId="item.id"
|
:style="gridStyle" :id="`section-${groupId}`"
|
||||||
:title="item.title"
|
> <!-- Show for each item -->
|
||||||
:subItems="item.subItems"
|
<template v-for="(item) in sortedItems">
|
||||||
@triggerModal="triggerModal"
|
<SubItemGroup
|
||||||
/>
|
v-if="item.subItems"
|
||||||
<Item
|
:key="item.id"
|
||||||
v-else
|
:itemId="item.id"
|
||||||
:item="item"
|
:title="item.title"
|
||||||
:key="item.id"
|
:subItems="item.subItems"
|
||||||
:itemSize="itemSize"
|
@triggerModal="triggerModal"
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
v-else
|
||||||
|
:item="item"
|
||||||
|
:key="item.id"
|
||||||
|
:itemSize="itemSize"
|
||||||
|
:parentSectionTitle="title"
|
||||||
|
@itemClicked="$emit('itemClicked')"
|
||||||
|
@triggerModal="triggerModal"
|
||||||
|
:isAddNew="false"
|
||||||
|
:sectionWidth="sectionWidth"
|
||||||
|
:sectionDisplayData="displayData"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- When in edit mode, show additional item, for Add New item -->
|
||||||
|
<Item v-if="isEditMode"
|
||||||
|
:item="{
|
||||||
|
icon: ':heavy_plus_sign:',
|
||||||
|
title: 'Add New Item',
|
||||||
|
description: 'Click to add new item',
|
||||||
|
id: 'add-new',
|
||||||
|
}"
|
||||||
|
:isAddNew="true"
|
||||||
:parentSectionTitle="title"
|
:parentSectionTitle="title"
|
||||||
@itemClicked="$emit('itemClicked')"
|
key="add-new"
|
||||||
@triggerModal="triggerModal"
|
class="add-new-item"
|
||||||
:isAddNew="false"
|
|
||||||
:sectionWidth="sectionWidth"
|
:sectionWidth="sectionWidth"
|
||||||
:sectionDisplayData="displayData"
|
:itemSize="itemSize"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
<!-- When in edit mode, show additional item, for Add New item -->
|
<div
|
||||||
<Item v-if="isEditMode"
|
v-if="hasWidgets"
|
||||||
:item="{
|
:class="`widget-list ${isWide? 'wide' : ''}`">
|
||||||
icon: ':heavy_plus_sign:',
|
<WidgetBase
|
||||||
title: 'Add New Item',
|
v-for="(widget, widgetIndx) in widgets"
|
||||||
description: 'Click to add new item',
|
:key="widgetIndx"
|
||||||
id: 'add-new',
|
:widget="widget"
|
||||||
}"
|
:index="index"
|
||||||
:isAddNew="true"
|
@navigateToSection="navigateToSection"
|
||||||
:parentSectionTitle="title"
|
/>
|
||||||
key="add-new"
|
</div>
|
||||||
class="add-new-item"
|
|
||||||
:sectionWidth="sectionWidth"
|
|
||||||
:itemSize="itemSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="hasWidgets"
|
|
||||||
:class="`widget-list ${isWide? 'wide' : ''}`">
|
|
||||||
<WidgetBase
|
|
||||||
v-for="(widget, widgetIndx) in widgets"
|
|
||||||
:key="widgetIndx"
|
|
||||||
:widget="widget"
|
|
||||||
:index="index"
|
|
||||||
@navigateToSection="navigateToSection"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Modal for opening in modal view -->
|
<!-- Modal for opening in modal view -->
|
||||||
<IframeModal
|
<IframeModal
|
||||||
|
|
@ -109,6 +116,7 @@ import Collapsable from '@/components/LinkItems/Collapsable.vue';
|
||||||
import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
||||||
import EditSection from '@/components/InteractiveEditor/EditSection.vue';
|
import EditSection from '@/components/InteractiveEditor/EditSection.vue';
|
||||||
import ContextMenu from '@/components/LinkItems/SectionContextMenu.vue';
|
import ContextMenu from '@/components/LinkItems/SectionContextMenu.vue';
|
||||||
|
import PinInput from '@/components/InteractiveEditor/PinInput.vue';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
import StoreKeys from '@/utils/StoreMutations';
|
import StoreKeys from '@/utils/StoreMutations';
|
||||||
import {
|
import {
|
||||||
|
|
@ -117,12 +125,16 @@ import {
|
||||||
modalNames,
|
modalNames,
|
||||||
} from '@/utils/defaults';
|
} from '@/utils/defaults';
|
||||||
|
|
||||||
|
const SECRET_UNLOCKED_KEY = 'dashy.secret.unlocked';
|
||||||
|
const SECRET_PINS_KEY = 'dashy.secret.expectedPins';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Section',
|
name: 'Section',
|
||||||
props: {
|
props: {
|
||||||
groupId: String,
|
groupId: String,
|
||||||
title: String,
|
title: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
|
pin: String,
|
||||||
displayData: Object,
|
displayData: Object,
|
||||||
items: Array,
|
items: Array,
|
||||||
widgets: Array,
|
widgets: Array,
|
||||||
|
|
@ -137,6 +149,7 @@ export default {
|
||||||
WidgetBase,
|
WidgetBase,
|
||||||
IframeModal,
|
IframeModal,
|
||||||
EditSection,
|
EditSection,
|
||||||
|
PinInput,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -148,9 +161,11 @@ export default {
|
||||||
},
|
},
|
||||||
sectionWidth: 0,
|
sectionWidth: 0,
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
|
isUnlocked: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
appConfig() {
|
appConfig() {
|
||||||
return this.$store.getters.appConfig;
|
return this.$store.getters.appConfig;
|
||||||
},
|
},
|
||||||
|
|
@ -209,6 +224,55 @@ export default {
|
||||||
}
|
}
|
||||||
return styles;
|
return styles;
|
||||||
},
|
},
|
||||||
|
shouldRenderSection() {
|
||||||
|
const hidden = this.displayData?.hiddenOnPurpose === true;
|
||||||
|
|
||||||
|
// Always show in Edit Mode so the user can unhide it
|
||||||
|
if (this.isEditMode) return true;
|
||||||
|
|
||||||
|
// If not hidden, render as usual
|
||||||
|
if (!hidden || this.showHiddenMode) return true;
|
||||||
|
|
||||||
|
// If hidden, only show when search is active AND this section has hits
|
||||||
|
const searchActive = (this.searchTerm || '').trim().length > 0;
|
||||||
|
const hasHits = Array.isArray(this.items) && this.items.length > 0;
|
||||||
|
return searchActive && hasHits;
|
||||||
|
},
|
||||||
|
showPinRequired() {
|
||||||
|
if (this.isEditMode) return false;
|
||||||
|
return this.displayData.secret === true && !this.isUnlocked;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
searchTerm: {
|
||||||
|
handler(newSeachTerm) {
|
||||||
|
// find if special code search is used
|
||||||
|
const showHidden = newSeachTerm.trim().toLowerCase() === '<hidden>';
|
||||||
|
this.showHiddenMode = showHidden;
|
||||||
|
|
||||||
|
// check if search is active
|
||||||
|
let searchIsActive = false;
|
||||||
|
if (newSeachTerm && newSeachTerm.trim().length > 0) {
|
||||||
|
searchIsActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if search is hit
|
||||||
|
const hasHits = this.items.length > 0;
|
||||||
|
|
||||||
|
// action if search is active and search hits and it was collapsed
|
||||||
|
if (searchIsActive && hasHits && this.isCollapsed) {
|
||||||
|
this.expandCollapseSection();
|
||||||
|
this.autoOpenedBySearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// action if that search is not a hit anymore
|
||||||
|
if (this.autoOpenedBySearch && (!searchIsActive || !hasHits)) {
|
||||||
|
this.expandCollapseSection();
|
||||||
|
this.autoOpenedBySearch = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/* Opens the iframe modal */
|
/* Opens the iframe modal */
|
||||||
|
|
@ -300,6 +364,21 @@ export default {
|
||||||
const secElem = this.$refs[this.sectionRef];
|
const secElem = this.$refs[this.sectionRef];
|
||||||
if (secElem && secElem.$el.clientWidth) this.sectionWidth = secElem.$el.clientWidth;
|
if (secElem && secElem.$el.clientWidth) this.sectionWidth = secElem.$el.clientWidth;
|
||||||
},
|
},
|
||||||
|
saveUnlockPins({ pin, id }) {
|
||||||
|
const map = JSON.parse(localStorage.getItem(SECRET_UNLOCKED_KEY) || '{}');
|
||||||
|
map[id] = pin;
|
||||||
|
localStorage.setItem(SECRET_UNLOCKED_KEY, JSON.stringify(map));
|
||||||
|
this.updateUnlocked();
|
||||||
|
},
|
||||||
|
updateUnlocked() {
|
||||||
|
const unlockPins = JSON.parse(localStorage.getItem(SECRET_UNLOCKED_KEY) || '{}');
|
||||||
|
const sectionKey = this.sectionRef;
|
||||||
|
if (unlockPins[sectionKey] === this.pin) {
|
||||||
|
this.isUnlocked = true;
|
||||||
|
} else {
|
||||||
|
this.isUnlocked = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Set the section width, and recalculate when section resized
|
// Set the section width, and recalculate when section resized
|
||||||
|
|
@ -307,6 +386,21 @@ export default {
|
||||||
this.resizeObserver = new ResizeObserver(this.calculateSectionWidth)
|
this.resizeObserver = new ResizeObserver(this.calculateSectionWidth)
|
||||||
.observe(this.$refs[this.sectionRef].$el);
|
.observe(this.$refs[this.sectionRef].$el);
|
||||||
}
|
}
|
||||||
|
if (this.displayData?.collapsed) {
|
||||||
|
this.isCollapsed = this.displayData.collapsed;
|
||||||
|
}
|
||||||
|
if (this.displayData?.secret) {
|
||||||
|
if (this.displayData.secret) this.isUnlocked = false;
|
||||||
|
|
||||||
|
const secretPin = String(this.pin || '0000');
|
||||||
|
const sectionKey = this.sectionRef;
|
||||||
|
|
||||||
|
const pins = JSON.parse(localStorage.getItem(SECRET_PINS_KEY) || '{}');
|
||||||
|
if (pins[sectionKey] !== secretPin) {
|
||||||
|
pins[sectionKey] = secretPin;
|
||||||
|
localStorage.setItem(SECRET_PINS_KEY, JSON.stringify(pins));
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// If resize observer set, and element still present, then de-register
|
// If resize observer set, and element still present, then de-register
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
:index="index"
|
:index="index"
|
||||||
:title="section.name"
|
:title="section.name"
|
||||||
:icon="section.icon || undefined"
|
:icon="section.icon || undefined"
|
||||||
|
:pin="section.pin || undefined"
|
||||||
:displayData="getDisplayData(section)"
|
:displayData="getDisplayData(section)"
|
||||||
:groupId="makeSectionId(section)"
|
:groupId="makeSectionId(section)"
|
||||||
:items="section.filteredItems"
|
:items="section.filteredItems"
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,19 @@ sections:
|
||||||
description: Get help with Dashy, raise a bug, or get in contact
|
description: Get help with Dashy, raise a bug, or get in contact
|
||||||
url: https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md
|
url: https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md
|
||||||
icon: far fa-hands-helping
|
icon: far fa-hands-helping
|
||||||
|
- name: Stuff
|
||||||
|
icon: fas fa-rocket
|
||||||
|
pin: "1111"
|
||||||
|
displayData:
|
||||||
|
secret: true
|
||||||
|
items:
|
||||||
|
- title: hidden tile 1
|
||||||
|
description: Development a project management links for Dashy
|
||||||
|
icon: https://i.ibb.co/qWWpD0v/astro-dab-128.png
|
||||||
|
url: https://live.dashy.to/
|
||||||
|
target: newtab
|
||||||
|
- title: hidden tile 2
|
||||||
|
description: Development a project management links for Dashy
|
||||||
|
icon: https://i.ibb.co/qWWpD0v/astro-dab-128.png
|
||||||
|
url: https://live.dashy.to/
|
||||||
|
target: newtab
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue