merge titles +start global config page + advanced

* merge all title components in one title
* same with subtitle
* update components using title and subtitle
* enhance css entry point
* start global config page and advanced form
This commit is contained in:
Jordan Blasenhauer 2024-06-06 15:43:51 +02:00
parent 0ee88857a3
commit 9499259c74
26 changed files with 691 additions and 236 deletions

View file

@ -184,7 +184,7 @@ body {
}
.select-btn {
@apply relative dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-800 dark:disabled:border-gray-800/0 duration-300 ease-in-out dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 hover:border-gray-600 focus:border-gray-600 flex justify-between align-middle items-center text-left text-sm leading-5.6 w-full rounded-lg border border-solid border-gray-400 bg-white bg-clip-padding px-1.5 py-1 md:px-2.5 md:py-1.5 font-normal text-gray-700 transition-all placeholder:text-gray-500;
@apply relative dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-800 dark:disabled:border-gray-800/0 duration-300 ease-in-out dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 hover:border-gray-600 focus:border-gray-600 flex justify-between align-middle items-center text-left text-sm leading-5.6 w-full md:max-w-[350px] rounded-lg border border-solid border-gray-400 bg-white bg-clip-padding px-1.5 py-1 md:px-2.5 md:py-1.5 font-normal text-gray-700 transition-all placeholder:text-gray-500;
}
.select-btn-name {
@ -220,7 +220,7 @@ body {
}
.input-regular {
@apply hover:border-gray-600 outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-gray-200 focus:border-gray-300/0 dark:focus:border-gray-600/0 focus:ring-1 focus:valid:ring-green-500 focus:invalid:ring-red-500 text-sm leading-5.6 ease-in block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-2.5 md:py-1.5 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-800 dark:disabled:border-gray-800/0 dark:disabled:text-gray-300 disabled:text-gray-700;
@apply hover:border-gray-600 outline-none dark:border-slate-600 dark:bg-slate-700 dark:text-gray-200 focus:border-gray-300/0 dark:focus:border-gray-600/0 focus:ring-1 focus:valid:ring-green-500 focus:invalid:ring-red-500 text-sm leading-5.6 ease-in block w-full md:max-w-[350px] appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-2.5 md:py-1.5 font-normal text-gray-700 transition-all placeholder:text-gray-500 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-800 dark:disabled:border-gray-800/0 dark:disabled:text-gray-300 disabled:text-gray-700;
}
.invalid.input-regular,
@ -829,10 +829,9 @@ body {
/* CONTENT COMPONENT */
.content-title {
@apply text-lg font-bold mb-2 col-span-12;
.content-text {
@apply leading-normal text-base mb-0 lowercase;
}
/* CONTENT DETAIL LSIT COMPONENT */
.content-detail-list-container {
@ -851,6 +850,38 @@ body {
@apply min-w-[2rem] break-all transition duration-300 ease-in-out pl-3 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-100;
}
.content-stat {
@apply my-1 font-bold dark:text-white/90 uppercase;
}
/* TITLE */
.title-container {
@apply break-words max-w-[80%] font-bold mb-2 col-span-12 dark:text-white/90 transition duration-300 ease-in-out text-2xl;
}
.title-card {
@apply break-words max-w-[80%] mb-2 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl;
}
.title-stat {
@apply mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-400;
}
/* SUBTITLE */
.subtitle-container {
@apply leading-normal text-xl mb-0 lowercase;
}
.subtitle-card {
@apply leading-normal text-base mb-0 lowercase;
}
.subtitle-stat {
@apply font-bold leading-normal text-sm mb-0 lowercase;
}
/* STAT COMPONENT */
.stat-content-container.no-icon {
@ -861,18 +892,6 @@ body {
@apply mr-12;
}
.stat-title {
@apply mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-gray-400;
}
.stat-value {
@apply my-1 font-bold dark:text-white/90 uppercase;
}
.stat-subtitle {
@apply font-bold leading-normal text-sm mb-0 lowercase;
}
.stat-svg {
@apply leading-none text-lg relative w-6 h-6;
}
@ -887,14 +906,6 @@ body {
@apply relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border transform duration-300 ease-in-out;
}
.card-title {
@apply break-words max-w-[80%] font-bold mb-2 col-span-12 dark:text-white/90 transition duration-300 ease-in-out text-2xl;
}
.card-content-title {
@apply break-words max-w-[80%] mb-2 font-bold dark:text-white/90 transition duration-300 ease-in-out text-xl;
}
/* CARD INFO */
.card-info-text {
@ -1284,147 +1295,291 @@ body {
/* TEXT COLOR MODIFIER */
.success.stat-subtitle {
.success.subtitle-stat,
.success.subtitle-card,
.success.title-card,
.success.title-container,
.success.title-stat {
@apply text-green-500;
}
.error.stat-subtitle {
.error.subtitle-stat,
.error.subtitle-card,
.error.title-card,
.error.title-container,
.error.title-stat {
@apply text-red-500;
}
.warning.stat-subtitle {
.warning.subtitle-stat,
.warning.subtitle-card,
.warning.title-card,
.warning.title-container,
.warning.title-stat {
@apply text-yellow-500;
}
.info.stat-subtitle {
.info.subtitle-stat,
.info.subtitle-card,
.info.title-card,
.info.title-container,
.info.title-stat {
@apply text-sky-500;
}
.purple.stat-subtitle {
.purple.subtitle-stat,
.purple.subtitle-card,
.purple.title-card,
.purple.title-container,
.purple.title-stat {
@apply text-purple-500;
}
.green.stat-subtitle {
.green.subtitle-stat,
.green.subtitle-card,
.green.title-card,
.green.title-container,
.green.title-stat {
@apply text-green-500;
}
.red.stat-subtitle {
.red.subtitle-stat,
.red.subtitle-card,
.red.title-card,
.red.title-container,
.red.title-stat {
@apply text-red-500;
}
.orange.stat-subtitle {
.orange.subtitle-stat,
.orange.subtitle-card,
.orange.title-card,
.orange.title-container,
.orange.title-stat {
@apply text-orange-500;
}
.blue.stat-subtitle {
.blue.subtitle-stat,
.blue.subtitle-card,
.blue.title-card,
.blue.title-container,
.blue.title-stat {
@apply text-blue-500;
}
.yellow.stat-subtitle {
.yellow.subtitle-stat,
.yellow.subtitle-card,
.yellow.title-card,
.yellow.title-container,
.yellow.title-stat {
@apply text-yellow-500;
}
.gray.stat-subtitle {
.gray.subtitle-stat,
.gray.subtitle-card,
.gray.title-card,
.gray.title-container,
.gray.title-stat {
@apply text-gray-500;
}
.dark.stat-subtitle {
.dark.subtitle-stat,
.dark.subtitle-card,
.dark.title-card,
.dark.title-container,
.dark.title-stat {
@apply text-slate-500;
}
.amber.stat-subtitle {
.amber.subtitle-stat,
.amber.subtitle-card,
.amber.title-card,
.amber.title-container,
.amber.title-stat {
@apply text-amber-500;
}
.emerald.stat-subtitle {
.emerald.subtitle-stat,
.emerald.subtitle-card,
.emerald.title-card,
.emerald.title-container,
.emerald.title-stat {
@apply text-emerald-500;
}
.teal.stat-subtitle {
.teal.subtitle-stat,
.teal.subtitle-card,
.teal.title-card,
.teal.title-container,
.teal.title-stat {
@apply text-teal-500;
}
.indigo.stat-subtitle {
.indigo.subtitle-stat,
.indigo.subtitle-card,
.indigo.title-card,
.indigo.title-container,
.indigo.title-stat {
@apply text-indigo-500;
}
.cyan.stat-subtitle {
.cyan.subtitle-stat,
.cyan.subtitle-card,
.cyan.title-card,
.cyan.title-container,
.cyan.title-stat {
@apply text-cyan-500;
}
.sky.stat-subtitle {
.sky.subtitle-stat,
.sky.subtitle-card,
.sky.title-card,
.sky.title-container,
.sky.title-stat {
@apply text-sky-500;
}
.pink.stat-subtitle {
.pink.subtitle-stat,
.pink.subtitle-card,
.pink.title-card,
.pink.title-container,
.pink.title-stat {
@apply text-pink-500;
}
.lime.stat-subtitle {
.lime.subtitle-stat,
.lime.subtitle-card,
.lime.title-card,
.lime.title-container,
.lime.title-stat {
@apply text-lime-500;
}
.purple-darker.stat-subtitle {
.purple-darker.subtitle-stat,
.purple.subtitle-card,
.purple.title-card,
.purple.title-container,
.purple.title-stat {
@apply text-purple-600;
}
.green-darker.stat-subtitle {
.green-darker.subtitle-stat,
.green-dark.subtitle-card,
.green-dark.title-card,
.green-dark.title-container,
.green-dark.title-stat {
@apply text-green-700;
}
.red-darker.stat-subtitle {
.red-darker.subtitle-stat,
.red-darker.subtitle-card,
.red-darker.title-card,
.red-darker.title-container,
.red-darker.title-stat {
@apply text-red-700;
}
.orange-darker.stat-subtitle {
.orange-darker.subtitle-stat,
.orange-darker.subtitle-card,
.orange-darker.title-card,
.orange-darker.title-container,
.orange-darker.title-stat {
@apply text-orange-600;
}
.blue-darker.stat-subtitle {
.blue-darker.subtitle-stat,
.blue-darker.subtitle-card,
.blue-darker.title-card,
.blue-darker.title-container,
.blue-darker.title-stat {
@apply text-blue-600;
}
.yellow-darker.stat-subtitle {
.yellow-darker.subtitle-stat,
.yellow-darker.subtitle-card,
.yellow-darker.title-card,
.yellow-darker.title-container,
.yellow-darker.title-stat {
@apply text-yellow-600;
}
.gray-darker.stat-subtitle {
.gray-darker.subtitle-stat,
.gray-darker.subtitle-card,
.gray-darker.title-card,
.gray-darker.title-container,
.gray-darker.title-stat {
@apply text-gray-600;
}
.dark-darker.stat-subtitle {
.dark-darker.subtitle-stat,
.dark-darker.subtitle-card,
.dark-darker.title-card,
.dark-darker.title-container,
.dark-darker.title-stat {
@apply text-slate-600;
}
.amber-darker.stat-subtitle {
.amber-darker.subtitle-stat,
.amber-darker.subtitle-card,
.amber-darker.title-card,
.amber-darker.title-container,
.amber-darker.title-stat {
@apply text-amber-600;
}
.emerald-darker.stat-subtitle {
.emerald-darker.subtitle-stat,
.emerald-darker.subtitle-card,
.emerald-darker.title-card,
.emerald-darker.title-container,
.emerald-darker.title-stat {
@apply text-emerald-600;
}
.teal-darker.stat-subtitle {
.teal-darker.subtitle-stat,
.teal-darker.subtitle-card,
.teal-darker.title-card,
.teal-darker.title-container,
.teal-darker.title-stat {
@apply text-teal-600;
}
.indigo-darker.stat-subtitle {
.indigo-darker.subtitle-stat,
.indigo-darker.subtitle-card,
.indigo-darker.title-card,
.indigo-darker.title-container,
.indigo-darker.title-stat {
@apply text-indigo-600;
}
.cyan-darker.stat-subtitle {
.cyan-darker.subtitle-stat,
.cyan-darker.subtitle-card,
.cyan-darker.title-card,
.cyan-darker.title-container,
.cyan-darker.title-stat {
@apply text-cyan-600;
}
.sky-darker.stat-subtitle {
.sky-darker.subtitle-stat,
.sky-darker.subtitle-card,
.sky-darker.title-card,
.sky-darker.title-container,
.sky-darker.title-stat {
@apply text-sky-700;
}
.pink-darker.stat-subtitle {
.pink-darker.subtitle-stat,
.pink-darker.subtitle-card,
.pink-darker.title-card,
.pink-darker.title-container,
.pink-darker.title-stat {
@apply text-pink-600;
}
.lime-darker.stat-subtitle {
.lime-darker.subtitle-stat,
.lime-darker.subtitle-card,
.lime-darker.title-card,
.lime-darker.title-container,
.lime-darker.title-stat {
@apply text-lime-600;
}

File diff suppressed because one or more lines are too long

View file

@ -3,12 +3,9 @@ import { reactive, onBeforeMount } from "vue";
// Containers
import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
// Title
import TitleCard from "@components/Title/Card.vue";
import TitleCardContent from "@components/Title/CardContent.vue";
import TitleStat from "@components/Title/Stat.vue";
// Subtitle
import SubtitleStat from "@components/Subtitle/Stat.vue";
// Headings
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
// Content
import ContentStat from "@components/Content/Stat.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
@ -85,22 +82,14 @@ const props = defineProps({
<Grid>
<!-- widget element -->
<template v-for="(widget, index) in container.widgets" :key="index">
<TitleCard v-if="widget.type === 'TitleCard'" v-bind="widget.data" />
<TitleCardContent
v-if="widget.type === 'TitleCardContent'"
v-bind="widget.data"
/>
<Title v-if="widget.type === 'Title'" v-bind="widget.data" />
<Subtitle v-if="widget.type === 'Subtitle'" v-bind="widget.data" />
<Checkbox v-if="widget.type === 'Checkbox'" v-bind="widget.data" />
<Select v-if="widget.type === 'Select'" v-bind="widget.data" />
<Input v-if="widget.type === 'Input'" v-bind="widget.data" />
<Datepicker v-if="widget.type === 'Datepicker'" v-bind="widget.data" />
<Button v-if="widget.type === 'Button'" v-bind="widget.data" />
<Stat v-if="widget.type === 'Stat'" v-bind="widget.data" />
<TitleStat v-if="widget.type === 'TitleStat'" v-bind="widget.data" />
<SubtitleStat
v-if="widget.type === 'SubtitleStat'"
v-bind="widget.data"
/>
<ContentStat
v-if="widget.type === 'ContentStat'"
v-bind="widget.data"

View file

@ -27,7 +27,7 @@ const props = defineProps({
</script>
<template>
<h5 :class="['stat-value', props.statClass]">
<h5 :class="['content-stat', props.statClass]">
{{ $t(props.stat, props.stat) }}
</h5>
</template>

View file

@ -0,0 +1,31 @@
<script setup>
/**
@name Content/Text.vue
@description This component is used for regular paragraph.
@example
{
text: "This is a paragraph",
textClass: "text-3xl"
}
@param {string} text - The text value. Can be a translation key or by default raw text.
@param {string} [textClass=""] - Additional class if needef
*/
const props = defineProps({
text: {
type: [String, Number],
required: true,
},
textClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<h5 :class="['content-text', props.textClass]">
{{ $t(props.text, props.text) }}
</h5>
</template>

View file

@ -19,12 +19,14 @@ import ErrorField from "@components/Forms/Error/Field.vue";
name: "checkbox",
required: true,
hideLabel: false,
inpType: "checkbox",
headerClass: "text-red-500"
}
@param {string} id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {string} [inpType="checkbox"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {boolean} [disabled=false]
@param {boolean} [required=false]
@param {object} [columns={"pc": "12", "tablet": "12", "mobile": "12}] - Field has a grid system. This allow to get multiple field in the same row if needed.
@ -50,6 +52,11 @@ const props = defineProps({
type: String,
required: true,
},
inpType: {
type: String,
required: false,
default: "checkbox",
},
disabled: {
type: Boolean,
required: false,
@ -119,7 +126,7 @@ onMounted(() => {
<template>
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:containerClass="`w-full p-2 md:p-3 ${props.containerClass}`"
:columns="props.columns"
>
<Header

View file

@ -27,10 +27,12 @@ import "@assets/css/flatpickr.dark.css";
noPickBeforeStamp: 1735682600000,
noPickAfterStamp: 1735689600000,
inpClass: "text-center",
inpType : ""
}
@param {string} id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} [inpType="datepicker"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {string|number|date} [defaultDate=null] - Default date when instanciate
@param {string|number} [noPickBeforeStamp=""] - Impossible to pick a date before this date
@param {string|number} [noPickAfterStamp=""] - Impossible to pick a date after this date
@ -57,6 +59,11 @@ const props = defineProps({
type: String,
required: false,
},
inpType: {
type: String,
required: false,
default: "datepicker",
},
hideLabel: {
type: Boolean,
required: false,
@ -614,7 +621,8 @@ function setIndex(calendarEl, tabindex) {
<template>
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
v-if="props.inpType === 'datepicker'"
:containerClass="`w-full p-2 md:p-3 ${props.containerClass}`"
:columns="props.columns"
>
<Header

View file

@ -22,12 +22,14 @@ import ErrorField from "@components/Forms/Error/Field.vue";
required: true,
label: 'Test input',
pattern : "(test)",
inpType: "input",
}
@param {string} id
@param {string} type - text, email, password, number, tel, url
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text. @param {string} label
@param {string} value
@param {string} [inpType="input"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {object} [columns={"pc": "12", "tablet": "12", "mobile": "12}] - Field has a grid system. This allow to get multiple field in the same row if needed.
@param {boolean} [disabled=false]
@param {boolean} [required=false]
@ -61,6 +63,11 @@ const props = defineProps({
type: String,
required: true,
},
inpType: {
type: String,
required: false,
default: "input",
},
required: {
type: Boolean,
required: false,
@ -167,7 +174,7 @@ onMounted(() => {
<template>
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:containerClass="`w-full p-2 md:p-3 ${props.containerClass}`"
:columns="props.columns"
>
<Header

View file

@ -21,12 +21,14 @@ import ErrorField from "@components/Forms/Error/Field.vue";
required: true,
requiredValues : ['no'], // need required to be checked
label: 'Test select',
inpType: "select",
}
@param {string} id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@param {array} values
@param {string} [inpType="select"] - The type of the field, useful when we have multiple fields in the same container to display the right field
@param {boolean} [disabled=false]
@param {boolean} [required=false]
@param {array} [requiredValues=[]] - values that need to be selected to be valid, works only if required is true
@ -57,6 +59,11 @@ const props = defineProps({
type: Array,
required: true,
},
inpType: {
type: String,
required: false,
default: "select",
},
disabled: {
type: Boolean,
required: false,
@ -197,7 +204,7 @@ const emits = defineEmits(["inp"]);
<template>
<Container
:containerClass="`w-full m-1 p-1 ${props.containerClass}`"
:containerClass="`w-full p-2 md:p-3 ${props.containerClass}`"
:columns="props.columns"
>
<Header

View file

@ -0,0 +1,92 @@
<script setup>
import { reactive, defineProps, onMounted, ref } from "vue";
import Container from "@components/Widget/Container.vue";
import Checkbox from "@components/Forms/Field/Checkbox.vue";
import Input from "@components/Forms/Field/Input.vue";
import Select from "@components/Forms/Field/Select.vue";
import Datepicker from "@components/Forms/Field/Datepicker.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
/**
@name Forms/Type/Advanced.vue
@description This component is used to create a complete advanced form with plugin selection.
@example
const data = [
{
name: "plugin name",
type: "pro",
is_activate: true,
description: "plugin description",
page: "/page",
settings: [
{
columns: { pc: 6, tablet: 12, mobile: 12 },
id: "test-check",
value: "yes",
label: "Checkbox",
name: "checkbox",
required: true,
hideLabel: false,
headerClass: "text-red-500",
inpType: "checkbox",
},
{
id: "test-input",
value: "yes",
type: "text",
name: "test-input",
disabled: false,
required: true,
label: "Test input",
pattern: "(test)",
inpType: "input",
},
],
},
];
@param {array} settings - List of settings that must fit Field components format.
@param {boolean} [isActive=true] - Check if the form is active, it will display the form if true
*/
const props = defineProps({
// id && value && method
plugins: {
type: Array,
required: true,
default: [],
},
isActive: {
type: Boolean,
required: false,
default: true,
},
});
</script>
<template>
<Container
v-if="props.isActive"
:tag="'form'"
method="POST"
:containerClass="`col-span-12 w-full m-1 p-1`"
:columns="props.columns"
>
<Container class="col-span-12 w-full" v-for="plugin in plugins">
<Title type="card" :title="plugin.name" />
<Subtitle type="card" :subtitle="plugin.description" />
<Container class="grid grid-cols-12 w-full">
<template v-for="(setting, index) in plugin.settings" :key="index">
<Checkbox v-if="setting.inpType === 'checkbox'" v-bind="setting" />
<Select v-if="setting.inpType === 'select'" v-bind="setting" />
<Datepicker
v-if="setting.inpType === 'datepicker'"
v-bind="setting"
/>
<Input v-if="setting.inpType === 'input'" v-bind="setting" />
</template>
</Container>
</Container>
</Container>
</template>

View file

@ -1,43 +0,0 @@
<script setup>
/**
@name Stat/Subtitle.vue
@description This component is a subtitle used with stats.
This can be used alone in case we don't need a complete stat widget.
In case you have a title, subtitle, stat and icon to display, you can directly use Stat widget.
@example
{
subtitle: "Last 30 days",
subtitleColor: "info",
subtitleClass: "text-sm"
}
@param {string} subtitle - The subtitle of the stat. Can be a translation key or by default raw text.
@param {string} [subtitleColor="info"] - The color of the subtitle between error, success, warning, info
@param {string} [subtitleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
subtitle: {
type: String,
required: true,
},
subtitleColor: {
type: String,
required: false,
default: "info",
},
subtitleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<p
v-if="props.subtitle"
:class="['stat-subtitle', props.subtitleColor, props.subtitleClass]"
>
{{ $t(props.subtitle, props.subtitle) }}
</p>
</template>

View file

@ -1,32 +0,0 @@
<script setup>
/**
@name Title/Card.vue
@description This component is a used as card title.
This can be used outside of the card container.
@example
{
title: "Total Users",
titleClass: "text-lg"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,
required: true,
},
titleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<h1 v-if="props.title" :class="[props.titleClass, 'card-title']">
{{ $t(props.title, props.title) }}
</h1>
</template>

View file

@ -1,32 +0,0 @@
<script setup>
/**
@name Title/CardContent.vue
@description This component is similar to TitleCard but with a lower size.
This can be used outside of the card container.
@example
{
title: "Total Users",
titleClass: "text-lg"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,
required: true,
},
titleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<h2 v-if="props.title" :class="[props.titleClass, 'card-content-title']">
{{ $t(props.title, props.title) }}
</h2>
</template>

View file

@ -1,33 +0,0 @@
<script setup>
/**
@name Title/Stat.vue
@description This component is a title used with stats.
This can be used alone in case we don't need a complete stat widget.
In case you have a title, subtitle, stat and icon to display, you can directly use Stat widget.
@example
{
title: "Total Users",
titleClass: "text-lg"
}
@param {string} title - The title of the stat. Can be a translation key or by default raw text.
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,
required: true,
},
titleClass: {
type: String,
required: false,
default: "",
},
});
</script>
<template>
<p :class="['stat-title', props.titleClass]">
{{ $t(props.title, props.title) }}
</p>
</template>

View file

@ -14,6 +14,7 @@ import { computed } from "vue";
}
@param {string} [containerClass=""] - Additional class
@param {object|boolean} [columns=false] - Work with grid system { pc: 12, tablet: 12, mobile: 12}
@param {string} [tag="div"] - The tag for the container
*/
const props = defineProps({
@ -27,6 +28,11 @@ const props = defineProps({
required: false,
default: false,
},
tag: {
type: String,
required: false,
default: "div",
},
});
const gridClass = computed(() => {
@ -37,10 +43,11 @@ const gridClass = computed(() => {
</script>
<template>
<div
<component
:is="props.tag"
data-container
:class="[props.containerClass ? props.containerClass : '', gridClass]"
>
<slot></slot>
</div>
</component>
</template>

View file

@ -1,7 +1,7 @@
<script setup>
import { defineProps } from "vue";
import Container from "@components/Widget/Container.vue";
import TitleCardContent from "@components/Title/CardContent.vue";
import Title from "@components/Widget/Title.vue";
import IconStatus from "@components/Icon/Status.vue";
import ContentDetailList from "@components/Content/DetailList.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
@ -9,7 +9,7 @@ import { v4 as uuidv4 } from "uuid";
/**
@name Widget/Instance.vue
@description This component is an instance widget.
This component is using the Container, TitleCardContent, IconStatus, ContentDetailList and ButtonGroup components.
This component is using the Container, TitleCard, IconStatus, ContentDetailList and ButtonGroup components.
@example
{
id: "instance-1",
@ -72,7 +72,7 @@ const props = defineProps({
<template>
<Container :columns="{ pc: 12, tablet: 12, mobile: 12 }">
<IconStatus :id="props.title" :status="props.status" />
<TitleCardContent :title="props.title" />
<Title type="card" :title="props.title" />
<ContentDetailList :details="props.details" />
<ButtonGroup
:id="props.id"

View file

@ -1,8 +1,8 @@
<script setup>
import Container from "@components/Widget/Container.vue";
import TitleStat from "@components/Title/Stat.vue";
import Title from "@components/Widget/Title.vue";
import Subtitle from "@components/Widget/Subtitle.vue";
import ContentStat from "@components/Content/Stat.vue";
import SubtitleStat from "@components/Subtitle/Stat.vue";
import IconStat from "@components/Icon/Stat.vue";
/**
@ -74,9 +74,10 @@ const props = defineProps({
props.iconName ? 'is-icon' : 'no-icon',
]"
>
<TitleStat :title="props.title" />
<Title type="stat" :title="props.title" />
<ContentStat :stat="props.stat" />
<SubtitleStat
<Subtitle
type="stat"
v-if="props.subtitle"
:subtitle="props.subtitle"
:subtitleColor="props.subtitleColor"

View file

@ -0,0 +1,71 @@
<script setup>
import { computed } from "vue";
/**
@name Widget/Subtitle.vue
@description This component is a general subtitle wrapper.
@example
{
subtitle: "Total Users",
type: "card",
subtitleClass: "text-lg",
subtitleColor : "info",
tag: "h2"
}
@param {string} subtitle - Can be a translation key or by default raw text.
@param {string} type - The type of subtitle between "card", "container" or "stat"
@param {string} [tag=""] - The tag of the subtitle. Can be h1, h2, h3, h4, h5, h6 or p. If empty, will be determine by the type of subtitle.
@param {string} [subtitleColor=""] - The color of the subtitle between error, success, warning, info or tailwind color
@param {string} [subtitleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
subtitle: {
type: String,
required: true,
},
type: {
type: String,
required: false,
default: "card",
},
tag: {
type: String,
required: false,
default: "",
},
subtitleColor: {
type: String,
required: false,
default: "",
},
subtitleClass: {
type: String,
required: false,
default: "",
},
});
const tag = computed(() => {
if (props.tag) return props.tag;
if (props.type === "container") return "p";
if (props.type === "card") return "p";
if (props.type === "stat") return "p";
});
const baseClass = computed(() => {
if (props.type === "container") return "subtitle-container";
if (props.type === "card") return "subtitle-card";
if (props.type === "stat") return "subtitle-stat";
return "subtitle-card";
});
</script>
<template>
<component
:is="tag"
v-if="props.subtitle"
:class="[props.subtitleClass, props.subtitleColor, baseClass]"
>
{{ $t(props.subtitle, props.subtitle) }}
</component>
</template>

View file

@ -0,0 +1,71 @@
<script setup>
import { computed } from "vue";
/**
@name Widget/Title.vue
@description This component is a general title wrapper.
@example
{
title: "Total Users",
type: "card",
titleClass: "text-lg",
titleColor : "info",
tag: "h2"
}
@param {string} title - Can be a translation key or by default raw text.
@param {string} type - The type of title between "card", "container" or "stat"
@param {string} [tag=""] - The tag of the title. Can be h1, h2, h3, h4, h5, h6 or p. If empty, will be determine by the type of title.
@param {string} [titleColor=""] - The color of the title between error, success, warning, info or tailwind color
@param {string} [titleClass=""] - Additional class, useful when component is used directly on a grid system
*/
const props = defineProps({
title: {
type: String,
required: true,
},
type: {
type: String,
required: false,
default: "card",
},
tag: {
type: String,
required: false,
default: "",
},
titleColor: {
type: String,
required: false,
default: "",
},
titleClass: {
type: String,
required: false,
default: "",
},
});
const tag = computed(() => {
if (props.tag) return props.tag;
if (props.type === "container") return "h1";
if (props.type === "card") return "h2";
if (props.type === "stat") return "p";
});
const baseClass = computed(() => {
if (props.type === "container") return "title-container";
if (props.type === "card") return "title-card";
if (props.type === "stat") return "title-stat";
return "title-card";
});
</script>
<template>
<component
:is="tag"
v-if="props.title"
:class="[props.titleClass, props.titleColor, baseClass]"
>
{{ $t(props.title, props.title) }}
</component>
</template>

View file

@ -80,6 +80,9 @@
"dashboard_status_error": "status inactive or error.",
"dashboard_status_warning": "status warning or alert.",
"dashboard_status_info": "status loading or waiting or unknown.",
"inp_input_valid": "input valid",
"inp_input_error_required": "input is required",
"inp_input_error": "input is invalid",
"action_send": "send {name}",
"action_start": "start {name}",
"action_disable": "disable {name}",

View file

@ -0,0 +1,11 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { getI18n } from "@utils/lang.js";
import GlobalConfig from "./GlobalConfig.vue";
const pinia = createPinia();
createApp(GlobalConfig)
.use(pinia)
.use(getI18n(["dashboard", "action", "inp", "global_config"]))
.mount("#app");

View file

@ -0,0 +1,109 @@
<script setup>
import { reactive, onBeforeMount, onMounted } from "vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import Builder from "@components/Builder.vue";
import Advanced from "@components/Forms/Type/Advanced.vue";
import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/GlobalConfig.vue
@description This component is the gllobal config page.
This page displays an overview of multiple stats related to BunkerWeb.
*/
const globalConfig = reactive({
builder: "",
});
onBeforeMount(() => {
// Get builder data
const dataAtt = "data-server-builder";
const dataEl = document.querySelector(`[${dataAtt}]`);
const data =
dataEl && !dataEl.getAttribute(dataAtt).includes(dataAtt)
? JSON.parse(dataEl.getAttribute(dataAtt))
: {};
globalConfig.builder = data;
});
onMounted(() => {
useGlobal();
useForm();
});
// const data = [
// {
// type: "Instance",
// data: {
// details: [
// { key: <instances_hostname="hostname">, value: "www.example.com" },
// { key: <instances_method="method">, value: <dashboard_ui> or <dashboard_scheduler>...},
// { key: <instances_port="port">, value: "1084" },
// { key: <instances_status="status">, value: <instances_active="active"> or <instances_inactive="inactive"> },
// ],
// status: "success",
// title: "www.example.com",
// buttons: [
// {
// text: <action_*>,
// color: "edit",
// size: "normal",
// },
// ],
// },
// },
// ];
const data = [
{
name: "plugin name",
type: "pro",
is_activate: true,
description: "plugin description",
page: "/page",
settings: [
{
columns: { pc: 4, tablet: 6, mobile: 12 },
id: "test-check",
value: "yes",
label: "Checkbox",
name: "checkbox",
required: true,
hideLabel: false,
inpType: "checkbox",
},
{
columns: { pc: 4, tablet: 6, mobile: 12 },
id: "test-input",
value: "yes",
type: "text",
name: "test-input",
disabled: false,
required: true,
label: "Test input",
pattern: "(test)",
inpType: "input",
},
{
columns: { pc: 4, tablet: 6, mobile: 12 },
id: "test-select",
value: "yes",
values: ["yes", "no"],
name: "test-select",
disabled: false,
required: true,
requiredValues: ["no"], // need required to be checked
label: "Test select",
inpType: "select",
},
],
},
];
</script>
<template>
<DashboardLayout>
<Advanced :plugins="data" />
</DashboardLayout>
</template>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
<link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="/css/flag-icons.min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BunkerWeb | DASHBOARD</title>
</head>
<body>
<div class="hidden" data-server-global='{"username" : "admin"}'></div>
<div class="hidden"
data-server-flash='[{"type" : "success", "title" : "title", "message" : "Success feedback"}, {"type" : "error", "title" : "title", "message" : "Error feedback"}, {"type" : "warning", "title" : "title", "message" : "Warning feedback"}, {"type" : "info", "title" : "title", "message" : "Info feedback"}]'>
</div>
<div class="hidden"
data-server-builder='[{"type":"card","containerColumns":{"pc":6,"tablet":6,"mobile":12},"widgets":[{"type":"Instance","data":{"details":[{"key":"instances_hostname","value":"bunkerweb"},{"key":"instances_type","value":"manual"},{"key":"instances_status","value":"instances_active"}],"status":"success","title":"bunkerweb","buttons":[{"attrs":{"data-form-INSTANCE_ID":"bunkerweb","data-form-operation":"reload","data-submit-form":"true"},"text":"action_reload","color":"warning","size":"normal"},{"attrs":{"data-form-INSTANCE_ID":"bunkerweb","data-form-operation":"stop","data-submit-form":"true"},"text":"action_stop","color":"error","size":"normal"}]}}]}]'>
</div>
<div id="app"></div>
<script type="module" src="globalConfig.js"></script>
</body>
</html>

View file

@ -7,5 +7,5 @@ const pinia = createPinia();
createApp(Home)
.use(pinia)
.use(getI18n(["dashboard", "action", "home"]))
.use(getI18n(["dashboard", "action", "inp", "home"]))
.mount("#app");

View file

@ -6,9 +6,9 @@ import { useGlobal } from "@utils/global.js";
import { useForm } from "@utils/form.js";
/**
@name Page/Home.vue
@description This component is the home page.
This page displays an overview of multiple stats related to BunkerWeb.
@name Page/Instances.vue
@description This component is the instances page.
This page displays current instances and allows to manage them.
*/
const home = reactive({

View file

@ -31,6 +31,10 @@ export default defineConfig({
test: resolve(__dirname, "./src/pages/test/index.html"),
home: resolve(__dirname, "./src/pages/home/index.html"),
instances: resolve(__dirname, "./src/pages/instances/index.html"),
"global-config": resolve(
__dirname,
"./src/pages/global-config/index.html"
),
},
},
},