added web-app and landing
This commit is contained in:
parent
af031877a5
commit
769fa105b8
198 changed files with 22132 additions and 10 deletions
112
web-app/src/components/fcomponents/FButton.vue
Normal file
112
web-app/src/components/fcomponents/FButton.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<button class="f-btn" :class="classObject" :style="styleObject">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: string;
|
||||
dense?: boolean;
|
||||
disabled?: boolean;
|
||||
invert?: boolean;
|
||||
block?: boolean;
|
||||
outlined?: boolean;
|
||||
bgColor?: string;
|
||||
light?: boolean;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: "medium",
|
||||
});
|
||||
|
||||
const classObject = computed(() => ({
|
||||
"f-btn--tiny": props.size === "tiny",
|
||||
"f-btn--small": props.size === "small",
|
||||
"f-btn--medium": props.size === "medium",
|
||||
"f-btn--large": props.size === "large",
|
||||
"f-btn--dense": props.dense,
|
||||
"f-btn--disabled": props.disabled,
|
||||
"f-btn--invert": props.invert,
|
||||
"f-btn--block": props.block,
|
||||
"f-btn--outlined": props.outlined,
|
||||
"f-btn--light": props.light,
|
||||
"f-btn--dark": props.dark,
|
||||
}));
|
||||
|
||||
const styleObject = computed(() => {
|
||||
const returnObject: any = {};
|
||||
if (props.bgColor) {
|
||||
returnObject["background-color"] = props.bgColor;
|
||||
}
|
||||
return returnObject;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@use 'sass:color'
|
||||
.f-btn
|
||||
padding: var(--btn-padding, 16px 32px)
|
||||
gap: 8px
|
||||
border-radius: var(--border-radius, 12px)
|
||||
border: none
|
||||
display: inline-flex
|
||||
text-decoration: none
|
||||
background-color: var(--color-button-bg)
|
||||
color: var(--color-button-font, white)
|
||||
letter-spacing: 0.15px
|
||||
transition: color .25s
|
||||
transition: background-color .25s
|
||||
justify-content: center
|
||||
font-weight: bold
|
||||
font-size: var(--font-size-button-medium)
|
||||
&.f-btn--tiny
|
||||
font-size: 13px
|
||||
letter-spacing: 0.46px
|
||||
padding: 4px 16px
|
||||
&.f-btn--small
|
||||
font-size: var(--font-size-button-small)
|
||||
letter-spacing: 0.46px
|
||||
&.f-btn--large
|
||||
font-size: var(--font-size-button-large)
|
||||
&.f-btn--disabled
|
||||
background-color: var(--color-bg-disabled, #D6D6D6)
|
||||
color: var(--color-disabled, #A1A3A7)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: var(--color-bg-disabled, #D6D6D6)
|
||||
color: var(--color-disabled, #A1A3A7)
|
||||
cursor: not-allowed
|
||||
&.f-btn--dense
|
||||
padding: var(--btn-dense-padding, 8px 16px)
|
||||
&.f-btn--invert
|
||||
background-color: var(--btn-color, white)
|
||||
color: var(--color-button-bg)
|
||||
border: 2px solid var(--color-button-bg)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: color.adjust(white, $lightness: -20%)
|
||||
&.f-btn--light
|
||||
background-color: var(--color-white, white)
|
||||
color: var(--color-secondary)
|
||||
border: 2px solid var(--color-white)
|
||||
&:hover,&:focus,&:active
|
||||
background-color: color.adjust(white, $lightness: -20%)
|
||||
border: 2px solid var(--color-white)
|
||||
&.f-btn--block
|
||||
display: flex
|
||||
flex: 1 0 auto
|
||||
min-width: 100%
|
||||
&.f-btn--outlined
|
||||
background-color: transparent
|
||||
color: var(--color-button-bg)
|
||||
border: 1px solid var(--color-button-border--outlined)
|
||||
|
||||
&:hover,&:focus,&:active
|
||||
cursor: pointer
|
||||
background-color: var(--color)
|
||||
&:hover,&:focus,&:active
|
||||
cursor: pointer
|
||||
background-color: #5f4884
|
||||
</style>
|
||||
57
web-app/src/components/fcomponents/FCard.vue
Normal file
57
web-app/src/components/fcomponents/FCard.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="f-card" ref="fCard" :style="computedStyles">
|
||||
<div v-if="props.title" class="f-card__title">
|
||||
<h5>
|
||||
{{ props.title }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="f-card__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
|
||||
const fCard = ref();
|
||||
|
||||
interface Props {
|
||||
bgColor?: string;
|
||||
borderWidth?: number;
|
||||
boxShadow?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const computedStyles = computed(() => ({
|
||||
"border-width": props.borderWidth,
|
||||
"background-color": props.bgColor,
|
||||
"box-shadow": props.boxShadow,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
// if (props.bgcolor) {
|
||||
// fCard.value.style["background-color"] = props.bgcolor;
|
||||
// }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.f-card
|
||||
border-radius: var(--card-border-radius)
|
||||
overflow: hidden
|
||||
height: 100%
|
||||
box-shadow: var(--color-card-box-shadow)
|
||||
background: var(--color-card-bg)
|
||||
color: var(--color-card-font)
|
||||
h6
|
||||
color: var(--color-card-font)
|
||||
.f-card__title
|
||||
border-bottom: 2px solid var(--color-based-blue)
|
||||
color: var(--color-based-blue)
|
||||
padding: 12px
|
||||
.f-card__body
|
||||
padding: 16px
|
||||
</style>
|
||||
97
web-app/src/components/fcomponents/FCollapse.vue
Normal file
97
web-app/src/components/fcomponents/FCollapse.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="f-collapse">
|
||||
<div class="f-collapse-wrapper" :class="{loading: props.loading}">
|
||||
<template v-if="loading">
|
||||
<f-loader></f-loader>
|
||||
</template>
|
||||
<div class="f-collapse-inner">
|
||||
<slot name="header"></slot>
|
||||
<!-- <img class="toggle-collapse" src="../assets/expand-less.svg?url" alt="expand less" /> -->
|
||||
<Icon v-if="isShow" class="toggle-collapse" icon="mdi:chevron-down" @click="openClose"></Icon>
|
||||
<Icon v-else icon="mdi:chevron-up" class="toggle-collapse" @click="openClose"></Icon>
|
||||
<!-- <img
|
||||
class="toggle-collapse"
|
||||
src="../assets/expand-more.svg?url"
|
||||
alt="expand more"
|
||||
v-else
|
||||
|
||||
/> -->
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<div v-if="isShow" class="collapsableDiv">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import FLoader from "@/components/fcomponents/FLoader.vue";
|
||||
|
||||
const emit = defineEmits(["collapse:opened", "collapse:closed"]);
|
||||
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const isShow = ref(false);
|
||||
const openClose = () => {
|
||||
isShow.value = !isShow.value;
|
||||
if(isShow.value){
|
||||
emit("collapse:opened")
|
||||
} else{
|
||||
emit("collapse:closed")
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.f-collapse
|
||||
border-radius: var(--border-radius)
|
||||
border: 1px solid var(--color-collapse-border)
|
||||
background-color: var(--color-collapse-bg)
|
||||
.f-collapse-wrapper
|
||||
padding: 8px 16px
|
||||
&.loading
|
||||
opacity: 0.5
|
||||
position: relative
|
||||
pointer-events: none
|
||||
.cube
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
.f-collapse-inner
|
||||
display: flex
|
||||
position: relative
|
||||
align-items: center
|
||||
.toggle-collapse
|
||||
font-size: 25px
|
||||
align-self: flex-start
|
||||
g
|
||||
path
|
||||
fill: var(--color-expand-icon)
|
||||
&:hover, &:active, &:focus
|
||||
cursor: pointer
|
||||
.collapsableDiv
|
||||
color: var(--color-collapse-font)
|
||||
|
||||
.collapse-enter-active
|
||||
animation: collapse reverse 500ms ease
|
||||
|
||||
.collapse-leave-active
|
||||
animation: collapse 500ms ease
|
||||
|
||||
@keyframes collapse
|
||||
from
|
||||
max-height: 200px
|
||||
overflow: hidden
|
||||
|
||||
to
|
||||
max-height: 0px
|
||||
overflow: hidden
|
||||
|
||||
|
||||
</style>
|
||||
135
web-app/src/components/fcomponents/FInput.vue
Normal file
135
web-app/src/components/fcomponents/FInput.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div class="f-input" :class="classObject">
|
||||
<div class="f-input-label subheader2">
|
||||
<label v-if="props.label" :for="name">{{ props.label }}</label>
|
||||
<icon>
|
||||
<template v-slot:text v-if="slots.info">
|
||||
<slot name="info"></slot>
|
||||
</template>
|
||||
</icon>
|
||||
</div>
|
||||
<div class="f-input__wrapper" ref="inputWrapper" @click="setFocus">
|
||||
<input
|
||||
:disabled="props.disabled"
|
||||
:readonly="props.readonly"
|
||||
:type="props.type"
|
||||
:name="name"
|
||||
:id="name"
|
||||
@input="updateModelValue"
|
||||
:value="props.modelValue"
|
||||
/>
|
||||
<div class="f-input--suffix" v-if="slots.suffix">
|
||||
<slot name="suffix" >test </slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f-input--details" v-if="slots.details">
|
||||
<slot name="details"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance, useSlots, ref } from "vue";
|
||||
import useClickOutside from "@/composables/useClickOutside"
|
||||
|
||||
import Icon from "@/components/icons/IconInfo.vue";
|
||||
interface Props {
|
||||
size?: string;
|
||||
info?: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
modelValue?: any;
|
||||
type?: string;
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const inputWrapper = ref();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const name = `f-input-${instance!.uid}`;
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: "normal",
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
type: "string",
|
||||
});
|
||||
|
||||
useClickOutside(inputWrapper, () => {
|
||||
removeFocus();
|
||||
})
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
function updateModelValue($event: any) {
|
||||
emit("update:modelValue", $event.target.value);
|
||||
}
|
||||
|
||||
const classObject = computed(() => ({
|
||||
"f-input--normal": props.size === "normal",
|
||||
"f-input--big": props.size === "big",
|
||||
"f-input--disabled": props.disabled,
|
||||
}));
|
||||
|
||||
function removeFocus(){
|
||||
const target = inputWrapper.value as HTMLElement
|
||||
target.classList.remove("f-input__wrapper--focused")
|
||||
}
|
||||
|
||||
function setFocus(event:MouseEvent){
|
||||
console.log("setFocus");
|
||||
if(props.disabled){
|
||||
return
|
||||
}
|
||||
const target = inputWrapper.value as HTMLElement
|
||||
target.classList.add("f-input__wrapper--focused")
|
||||
}
|
||||
</script>
|
||||
<style lang="sass">
|
||||
.f-input
|
||||
display: flex
|
||||
flex-direction: column
|
||||
.info-icon
|
||||
position: static
|
||||
@media (min-width: 768px)
|
||||
position: relative
|
||||
.f-input-label
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
.f-input__wrapper
|
||||
display: flex
|
||||
border: 1px solid black
|
||||
border-radius: 12px
|
||||
width: 100%
|
||||
background-color: #2D2D2D
|
||||
&.f-input__wrapper--focused
|
||||
outline: none
|
||||
border: 1px solid #7550AE
|
||||
box-shadow: 0 0 5px #7550AE
|
||||
input
|
||||
border: 0
|
||||
border-radius: 12px
|
||||
padding: 8px 12px
|
||||
width: 100%
|
||||
background-color: #2D2D2D
|
||||
color: #FFFFFF
|
||||
outline: none
|
||||
&.f-input--big
|
||||
font-size: 18px
|
||||
&.f-input--normal
|
||||
font-size: 16px
|
||||
&.f-input--disabled
|
||||
input
|
||||
background-color: #202020
|
||||
color: #F0F0F0
|
||||
.f-input--suffix
|
||||
display: flex
|
||||
align-items: center
|
||||
margin-right: 10px
|
||||
|
||||
</style>
|
||||
77
web-app/src/components/fcomponents/FLoader.vue
Normal file
77
web-app/src/components/fcomponents/FLoader.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="cube">
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
<div class="side"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.cube
|
||||
margin: auto
|
||||
font-size: 24px
|
||||
height: 1em
|
||||
width: 1em
|
||||
position: relative
|
||||
transform: rotatex(30deg) rotatey(45deg)
|
||||
transform-style: preserve-3d
|
||||
animation: cube-spin 1.5s infinite ease-in-out alternate
|
||||
|
||||
.side
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
transform-style: preserve-3d
|
||||
|
||||
&::before
|
||||
content: ""
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
background-color: currentcolor
|
||||
transform: translatez(0.5em)
|
||||
animation: cube-explode 1.5s infinite ease-in-out
|
||||
opacity: 0.5
|
||||
|
||||
&:nth-child(1)
|
||||
transform: rotatey(90deg)
|
||||
|
||||
&:nth-child(2)
|
||||
transform: rotatey(180deg)
|
||||
|
||||
&:nth-child(3)
|
||||
transform: rotatey(270deg)
|
||||
|
||||
&:nth-child(4)
|
||||
transform: rotatey(360deg)
|
||||
|
||||
&:nth-child(5)
|
||||
transform: rotatex(90deg)
|
||||
|
||||
&:nth-child(6)
|
||||
transform: rotatex(270deg)
|
||||
|
||||
@keyframes cube-spin
|
||||
0%
|
||||
transform: rotatex(30deg) rotatey(45deg)
|
||||
|
||||
100%
|
||||
transform: rotatex(30deg) rotatey(405deg)
|
||||
|
||||
@keyframes cube-explode
|
||||
0%
|
||||
transform: translatez(0.5em)
|
||||
|
||||
50%
|
||||
transform: translatez(0.75em)
|
||||
|
||||
100%
|
||||
transform: translatez(0.5em)
|
||||
</style>
|
||||
73
web-app/src/components/fcomponents/FOutput.vue
Normal file
73
web-app/src/components/fcomponents/FOutput.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div
|
||||
class="output-wrapper"
|
||||
:class="{
|
||||
'no-name': !props.name,
|
||||
'output--variant-1': props.variant === 1,
|
||||
'output--variant-2': props.variant === 2,
|
||||
'output--variant-3': props.variant === 3,
|
||||
}"
|
||||
>
|
||||
<div class="output-left">
|
||||
<div class="output-text">{{ props.name }}</div>
|
||||
<div class="output-price">
|
||||
<slot name="price">
|
||||
{{ props.price }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="output-right" v-if="slots.end">
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
price: [String, Number],
|
||||
variant: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.output-wrapper
|
||||
height: 100%
|
||||
display: flex
|
||||
flex-direction: row
|
||||
&.no-name
|
||||
gap: 0
|
||||
align-items: center
|
||||
justify-content: center
|
||||
&.output--variant-1
|
||||
padding: 16px
|
||||
border: 1px solid #2D2D2D
|
||||
border-radius: var(--border-radius, 12px)
|
||||
background-color: var(--color-input)
|
||||
&.output--variant-2
|
||||
border-bottom: 1px solid var(--color-grey)
|
||||
padding: 8px
|
||||
&.output--variant-3
|
||||
padding: 8px
|
||||
.output-left
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 8px
|
||||
text-align: left
|
||||
.output-price
|
||||
font-size: 18px
|
||||
letter-spacing: 3px
|
||||
.output-text
|
||||
font-size: 14px
|
||||
font-weight: bold
|
||||
.output-right
|
||||
margin-left: auto
|
||||
align-self: center
|
||||
</style>
|
||||
234
web-app/src/components/fcomponents/FSelect.vue
Normal file
234
web-app/src/components/fcomponents/FSelect.vue
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
<template>
|
||||
<div class="o-select" @click="clickSelect" ref="componentRef">
|
||||
<f-input hide-details :clearable="props.clearable" :label="props.label" :selectedable="false"
|
||||
:focus="showList" :modelValue="`${year} % yearly`" readonly>
|
||||
<template #info v-if="slots.info">
|
||||
<slot name="info"></slot>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<Icon class="toggle-collapse" v-if="showList" icon="mdi:chevron-down"></Icon>
|
||||
<Icon class="toggle-collapse" v-else icon="mdi:chevron-up"></Icon>
|
||||
</template>
|
||||
</f-input>
|
||||
<div class="select-list-wrapper" v-show="showList" ref="selectList" >
|
||||
<div class="select-list-inner" @click.stop>
|
||||
<div class="select-list-item" v-for="(item, index) in props.items" :key="item.year" :class="{'active': year === item.year, 'hovered': activeIndex === index}"
|
||||
@click.stop="clickItem(item)" @mouseenter="mouseEnter($event, index)" @mouseleave="mouseLeave($event, index)">
|
||||
<div class="circle">
|
||||
<div class="active" v-if="year === item.year"></div>
|
||||
<div class="hovered" v-else-if="activeIndex === index"></div>
|
||||
</div>
|
||||
<div class="yearly">
|
||||
<div class="value">{{ item.year }} %</div>
|
||||
<div class="label">yearly</div>
|
||||
</div>
|
||||
<div class="daily">
|
||||
<div class="value">{{ item.daily.toFixed(4) }} %</div>
|
||||
<div class="label">daily</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot :style="[!props.editor ? {display: 'none'} : {}]"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, getCurrentInstance, type ComputedRef, useSlots } from "vue"
|
||||
import FInput from "@/components/fcomponents/FInput.vue"
|
||||
import useClickOutside from "@/composables/useClickOutside"
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const slots = useSlots();
|
||||
|
||||
interface Item {
|
||||
year: number;
|
||||
daily: number;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: [Array<Item>],
|
||||
required: false,
|
||||
default: []
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
itemTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "label"
|
||||
},
|
||||
itemValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "value"
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editor: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const showList = ref(<boolean>false);
|
||||
const slotElements = ref(<any[]>[])
|
||||
const componentRef = ref(<any>null);
|
||||
const selectList = ref();
|
||||
const activeIndex= ref();
|
||||
|
||||
useClickOutside(componentRef, () => {
|
||||
|
||||
showList.value = false
|
||||
})
|
||||
|
||||
|
||||
const year= computed({
|
||||
// getter
|
||||
get() {
|
||||
return props.modelValue || props.items[0].year
|
||||
},
|
||||
// setter
|
||||
set(newValue: number) {
|
||||
|
||||
emit("update:modelValue", newValue)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
function mouseEnter(event:MouseEvent , index:number){
|
||||
const target = event. target as HTMLElement;
|
||||
activeIndex.value = index;
|
||||
target.classList.add("active")
|
||||
}
|
||||
|
||||
function mouseLeave(event:MouseEvent, index:number){
|
||||
const elements = selectList.value.querySelectorAll('.select-list-item');
|
||||
elements.forEach((element:HTMLElement) => {
|
||||
element.classList.remove("active")
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function clickSelect(event: any) {
|
||||
showList.value = !showList.value;
|
||||
}
|
||||
|
||||
|
||||
function clickItem(item: any) {
|
||||
console.log("item", item);
|
||||
year.value = item.year
|
||||
showList.value = false;
|
||||
console.log("showList.value", showList.value);
|
||||
// emit('input', item)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.o-select
|
||||
position: relative
|
||||
display: inline-block
|
||||
.toggle-collapse
|
||||
color: black
|
||||
font-size: 20px
|
||||
&:active, &:focus, &:hover
|
||||
cursor: pointer
|
||||
.select-list-wrapper
|
||||
position: absolute
|
||||
box-shadow: 0 1px 6px 0 #20212447
|
||||
background-color: var(--color-card-bg)
|
||||
z-index: 10
|
||||
padding: 16px
|
||||
border-radius: 16px
|
||||
border: 1px solid black
|
||||
right: 0
|
||||
margin-top: 4px
|
||||
.select-list-inner
|
||||
height: 330px
|
||||
overflow-y: scroll
|
||||
display: grid
|
||||
.select-list-item
|
||||
display: grid
|
||||
grid-template-columns: 15px 110px 126px
|
||||
place-content: end
|
||||
margin-right: 8px
|
||||
-webkit-touch-callout: none
|
||||
-webkit-user-select: none
|
||||
-khtml-user-select: none
|
||||
-moz-user-select: none
|
||||
-ms-user-select: none
|
||||
user-select: none
|
||||
padding: 4px 0
|
||||
&:active, &:focus, &:hover
|
||||
cursor: pointer
|
||||
.daily, .yearly
|
||||
display: flex
|
||||
gap: 4px
|
||||
justify-content: end
|
||||
.daily
|
||||
color: var(--color-grey)
|
||||
.circle
|
||||
height: 14px
|
||||
width: 14px
|
||||
.active
|
||||
height: 14px
|
||||
width: 14px
|
||||
background-color: var(--color-based-blue)
|
||||
border-radius: 14px
|
||||
.hovered
|
||||
height: 14px
|
||||
width: 14px
|
||||
background-color: var(--color-based-blue)
|
||||
opacity: .5
|
||||
border-radius: 14px
|
||||
|
||||
|
||||
|
||||
.select-list-inner
|
||||
&::-webkit-scrollbar-track
|
||||
border-radius: 10px
|
||||
|
||||
&::-webkit-scrollbar
|
||||
width: 5px
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
border-radius: 10px
|
||||
background-color: lightgrey
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
259
web-app/src/components/fcomponents/FSlider.vue
Normal file
259
web-app/src/components/fcomponents/FSlider.vue
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="slider-wrapper">
|
||||
<div class="disabled-slider" :style="{ 'flex-basis': minPercentage + '%' }">
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div class="range-slider">
|
||||
<input
|
||||
class="range-slider__range"
|
||||
type="range"
|
||||
ref="sliderInput"
|
||||
:style="{
|
||||
background: `linear-gradient(90deg, ${settings.fill} ${percentageDot}%, ${settings.background} ${percentageDot + 0.1}%)`,
|
||||
}"
|
||||
:value="props.modelValue"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
@input="updateModelValue"
|
||||
/>
|
||||
<div
|
||||
class="testbla"
|
||||
@mousemove="testMove"
|
||||
:style="{
|
||||
left: percentageDot + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
|
||||
const sliderInput = ref();
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}>();
|
||||
|
||||
const minPercentage = computed(() => {
|
||||
if (!props.min || !props.max) {
|
||||
return 0n;
|
||||
}
|
||||
return (props.min * 100) / props.max;
|
||||
});
|
||||
|
||||
function updateModelValue(event: any) {
|
||||
emit("update:modelValue", event.target.valueAsNumber);
|
||||
}
|
||||
|
||||
|
||||
const sliderValue = computed({
|
||||
// getter
|
||||
get() {
|
||||
return props.modelValue || props.min;
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
emit("update:modelValue", newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const percentageDot = computed(() => {
|
||||
let percentage = (100 * (sliderValue.value - props.min)) / (props.max - props.min);
|
||||
|
||||
if(percentage < 20){
|
||||
percentage = percentage + 1;
|
||||
} else if(percentage > 50){
|
||||
percentage = percentage - 2;
|
||||
} else if(percentage > 80){
|
||||
percentage = percentage - 3;
|
||||
}
|
||||
|
||||
return percentage
|
||||
});
|
||||
|
||||
const settings = {
|
||||
fill: "#7550AE",
|
||||
background: "#000000",
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
sliderInput.value.addEventListener("input", setSliderValue);
|
||||
});
|
||||
|
||||
function setSliderValue(event: any){
|
||||
sliderValue.value = event.target.value;
|
||||
}
|
||||
|
||||
function testMove(event: any) {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log("sliderInput.value", sliderInput.value);
|
||||
if(sliderInput.value){
|
||||
sliderInput.value.removeEventListener("input", setSliderValue);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
.testbla {
|
||||
position: absolute;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
background-color: var(--color-based-blue);
|
||||
user-select: none;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
right: 50%;
|
||||
border-radius: 25px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.disabled-slider {
|
||||
width: 60px;
|
||||
height: 7px;
|
||||
background-color: var(--color-border-main);
|
||||
/* margin-top: auto; */
|
||||
align-self: center;
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dot {
|
||||
border-radius: 50px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-color: var(--color-border-main);
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
/* bottom: 50%; */
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
// Base Colors
|
||||
$shade-10: #4682b4 !default;
|
||||
$shade-1: #d7dcdf !default;
|
||||
$shade-0: #fff !default;
|
||||
$teal: #4682b4 !default;
|
||||
|
||||
// Range Slider
|
||||
$range-width: 100% !default;
|
||||
|
||||
$range-handle-color: $shade-10 !default;
|
||||
$range-handle-color-hover: $teal !default;
|
||||
$range-handle-size: 15px !default;
|
||||
|
||||
$range-track-color: $shade-1 !default;
|
||||
$range-track-height: 7px !default;
|
||||
|
||||
$range-label-color: $shade-10 !default;
|
||||
$range-label-width: 60px !default;
|
||||
|
||||
.range-slider {
|
||||
width: $range-width;
|
||||
position: relative;
|
||||
accent-color: #7550AE;
|
||||
}
|
||||
|
||||
.range-slider__range {
|
||||
// width: calc(100% - (#{$range-label-width + 13px}));
|
||||
width: 100%;
|
||||
height: $range-track-height;
|
||||
border-radius: 5px;
|
||||
background: $range-track-color;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// Range Handle
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
opacity: 0;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active::-webkit-slider-thumb {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: $range-handle-size;
|
||||
height: $range-handle-size;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: $range-handle-color;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:active::-moz-range-thumb {
|
||||
background: $range-handle-color-hover;
|
||||
}
|
||||
|
||||
// Focus state
|
||||
&:focus {
|
||||
&::-webkit-slider-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Range Label
|
||||
.range-slider__value {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: $range-label-width;
|
||||
color: $shade-0;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
background: $range-label-color;
|
||||
padding: 5px 10px;
|
||||
margin-left: 8px;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: -7px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 7px solid transparent;
|
||||
border-right: 7px solid $range-label-color;
|
||||
border-bottom: 7px solid transparent;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox Overrides
|
||||
::-moz-range-track {
|
||||
background: $range-track-color;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input::-moz-focus-inner,
|
||||
input::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
14
web-app/src/components/fcomponents/FTag.vue
Normal file
14
web-app/src/components/fcomponents/FTag.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div class="f-tag">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.f-tag
|
||||
border: 1px solid var(--color-font)
|
||||
border-radius: var(--border-radius, 12px)
|
||||
padding: 2px 16px
|
||||
font-size: 12px
|
||||
display: inline
|
||||
</style>
|
||||
64
web-app/src/components/fcomponents/tabs/FTab.vue
Normal file
64
web-app/src/components/fcomponents/tabs/FTab.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<section v-show="value === props.name" ref="tab" role="tabpanel" tabindex="-1">
|
||||
<slot></slot>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeMount, getCurrentInstance, ref, computed, watch, inject } from "vue";
|
||||
const instance = ref<any>();
|
||||
|
||||
const isActive = ref<any>(null);
|
||||
|
||||
const value = inject("value");
|
||||
var updateTab: any = inject("updateTab");
|
||||
|
||||
defineExpose({
|
||||
isActive,
|
||||
});
|
||||
|
||||
onBeforeMount(() => {});
|
||||
onMounted(() => {
|
||||
instance.value = getCurrentInstance();
|
||||
|
||||
var addTab: any = inject("addTab");
|
||||
addTab({
|
||||
uid: instance.value!.uid,
|
||||
name: props.name,
|
||||
label: props.label,
|
||||
});
|
||||
|
||||
// resolved = {"test123": 123, "blub": "abc"}
|
||||
});
|
||||
|
||||
// const test = computed(() => {
|
||||
// console.log(instance.value);
|
||||
|
||||
// return instance.value?.parent
|
||||
// })
|
||||
|
||||
//Props
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: [String],
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
watch(props, (newValue, oldValue) => {
|
||||
console.log("newValue", newValue);
|
||||
console.log("instance", instance.value);
|
||||
|
||||
updateTab({
|
||||
uid: instance.value!.uid,
|
||||
name: props.name,
|
||||
label: props.label,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
126
web-app/src/components/fcomponents/tabs/FTabs.vue
Normal file
126
web-app/src/components/fcomponents/tabs/FTabs.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="f-tabs" ref="tabsRef">
|
||||
<div class="f-tabs__header" ref="headerRef">
|
||||
<div
|
||||
class="f-tab"
|
||||
ref="tabsRef1"
|
||||
:id="`f-tab-${tab.uid}`"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.uid"
|
||||
@click="setActive(tab)"
|
||||
>
|
||||
<h5>{{ tab.label }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="f-tabs__content">
|
||||
<slot ref="tabsTest"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, h, computed, useSlots, provide, getCurrentInstance } from "vue";
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const slots1 = useSlots();
|
||||
|
||||
const tabsRef = ref<HTMLElement>();
|
||||
const tabsRef1 = ref<HTMLElement>();
|
||||
const headerRef = ref<HTMLElement>();
|
||||
const tabsArray = ref<Array<HTMLElement>>();
|
||||
const tabsComponents = ref<Array<any>>();
|
||||
const content = ref<any>();
|
||||
const target = ref<any>();
|
||||
const slots = ref<any>();
|
||||
const value = ref<any>();
|
||||
const tabsTest = ref<any>();
|
||||
const tabs = ref<Array<any>>([]);
|
||||
provide("value", value);
|
||||
provide("addTab", (tab: any) => {
|
||||
tabs.value.push(tab);
|
||||
});
|
||||
|
||||
provide("updateTab", (tab: any) => {
|
||||
let changedTabIndex = tabs.value.findIndex((obj) => obj.uid === tab.uid);
|
||||
if (changedTabIndex > -1) {
|
||||
tabs.value[changedTabIndex] = tab;
|
||||
}
|
||||
});
|
||||
// provide('addTab', tabs.value)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if(props.modelValue){
|
||||
const tab = tabs.value.find((obj) => obj.name === props.modelValue)
|
||||
setActive(tab);
|
||||
|
||||
} else {
|
||||
setActive(tabs.value[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setActive(tab: any) {
|
||||
|
||||
nextTick(() => {
|
||||
const tabElement: any = headerRef.value?.querySelector(`#f-tab-${tab.uid}`);
|
||||
var array = Array.prototype.slice.call(tabsRef.value?.children[0].children);
|
||||
|
||||
array.forEach((element: HTMLElement) => {
|
||||
element.classList.remove("f-tab--active");
|
||||
});
|
||||
tabElement.classList.add("f-tab--active");
|
||||
value.value = tab.name;
|
||||
emit("update:modelValue", tab.name);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.f-tabs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
width: 100%
|
||||
.f-tabs__header
|
||||
display: flex
|
||||
.f-tab, label
|
||||
cursor: pointer
|
||||
.f-tab
|
||||
padding: 0.6em 1em
|
||||
transition: background-color .2s,color .2s
|
||||
align-items: center
|
||||
justify-content: center
|
||||
font-weight: 400
|
||||
text-transform: uppercase
|
||||
font-size: 24px
|
||||
font-weight: bold
|
||||
border-width: 0 0 2px 0
|
||||
border-style: solid
|
||||
border-color: transparent
|
||||
flex: 1 1 auto
|
||||
text-align: center
|
||||
height: 50px
|
||||
border-bottom: 2px solid var(--color-light-grey)
|
||||
h5
|
||||
color: var(--color-tab-font)
|
||||
margin: 0
|
||||
&.f-tab--active
|
||||
border-bottom: 3px solid var(--color-tab-border--active)
|
||||
h5
|
||||
color: var(--color-tab-border--active)
|
||||
.f-tabs__content
|
||||
padding: 16px 12px
|
||||
@media (min-width: 768px)
|
||||
padding: 16px 24px
|
||||
</style>
|
||||
72
web-app/src/components/fcomponents/tabs/Tabs.spec.ts
Normal file
72
web-app/src/components/fcomponents/tabs/Tabs.spec.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import FTabs from './FTabs.vue';
|
||||
import FTab from './FTab.vue';
|
||||
|
||||
describe('FTabs.vue', () => {
|
||||
it('renders tabs correctly', () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Tab 1');
|
||||
expect(wrapper.text()).toContain('Tab 2');
|
||||
});
|
||||
|
||||
it('activates the first tab by default via v-model', async () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
props: {
|
||||
modelValue: 'tab1', // v-model initial value
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const sections = wrapper.findAll('section');
|
||||
|
||||
expect(sections[0].attributes('style')).not.toContain('display: none');
|
||||
expect(sections[1].attributes('style')).toContain('display: none');
|
||||
|
||||
});
|
||||
|
||||
it('switches to the correct tab on click', async () => {
|
||||
const wrapper = mount(FTabs, {
|
||||
slots: {
|
||||
default: `
|
||||
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
|
||||
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
|
||||
`,
|
||||
},
|
||||
global: {
|
||||
components: { FTab },
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
const tabs = wrapper.findAll('.f-tab');
|
||||
await tabs[1].trigger('click');
|
||||
|
||||
const sections = wrapper.findAll('section');
|
||||
expect(sections[0].attributes('style')).toContain('display: none');
|
||||
expect(sections[1].attributes('style')).not.toContain('display: none');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue