primevue-mirror/apps/showcase/components/layout/designer/dashboard/DesignDashboard.vue

323 lines
14 KiB
Vue
Raw Normal View History

2024-12-26 14:41:51 +00:00
<template>
2025-01-13 09:28:00 +00:00
<div style="border-radius: 50px" class="overflow-hidden mb-8">
<NuxtLink to="/designer" class="block">
2025-01-13 13:51:11 +00:00
<img alt="PrimeVue Designer" :src="coverImage" class="w-full" />
2025-01-13 09:28:00 +00:00
</NuxtLink>
</div>
2025-02-11 10:26:24 +00:00
<div class="text-lg font-semibold mb-2">Authenticate</div>
2025-02-18 18:14:33 +00:00
<div v-if="!$appState.designer.verified">
<span class="block leading-6 mb-4"
>Theme Designer is the ultimate tool to customize and design your own themes featuring a visual editor, figma to code, cloud storage, and migration assistant. <NuxtLink to="/designer" class="doc-link">Discover</NuxtLink> more about the
Theme Designer by visiting the detailed <NuxtLink to="/designer/guide" class="doc-link">documentation</NuxtLink></span
>
<span class="block leading-6 mb-4"
>A license can be purchased from <a href="https://primefaces.org/store/designer.xhtml" class="doc-link" rel="noopener noreferrer">PrimeStore</a>, if you do not have a license key, you are still able to experience the Designer in trial
mode. Note that in trial mode, downloads, figma to code, migration assistant and cloud storage are not available.</span
>
<span class="block leading-6 mb-4">Sign-in at <a href="https://primefaces.org/store/designer.xhtml" class="doc-link" rel="noopener noreferrer">PrimeStore</a> to retrieve your license key along with the pass key.</span>
</div>
2025-02-11 10:26:24 +00:00
<form v-if="!$appState.designer.verified" @submit.prevent class="flex gap-4">
<input v-model="licenseKey" type="password" autocomplete="off" class="px-3 py-2 rounded-md border border-surface-300 dark:border-surface-700 flex-1" placeholder="License Key" />
2025-02-17 11:28:56 +00:00
<input v-model="otp" autocomplete="off" class="px-3 py-2 rounded-md border border-surface-300 dark:border-surface-700" placeholder="Pass Key" />
2025-02-11 10:26:24 +00:00
<button type="button" @click="activate(false)" class="btn-design">Activate</button>
</form>
<form v-else @submit.prevent class="flex gap-4">
<input value="***********************************************************" type="password" autocomplete="off" class="px-3 py-2 rounded-md border border-surface-300 dark:border-surface-700 flex-1" placeholder="License Key" disabled />
<button type="button" @click="signOut" class="btn-design">Sign Out</button>
2024-12-26 14:41:51 +00:00
</form>
2025-01-02 08:26:32 +00:00
<div class="flex justify-between items-center mb-2 mt-6">
<span class="text-lg font-semibold">My Themes</span>
2025-01-03 16:06:56 +00:00
<div v-if="$appState.designer.themeLimit" class="flex items-center gap-2">
<span class="text-muted-color text-xs">{{ $appState.designer.themes.length }} / {{ $appState.designer.themeLimit }}</span>
<div class="h-2 border rounded-md w-32 overflow-hidden">
<div class="bg-zinc-950 dark:bg-white h-full" :style="{ width: themeUsageRatio + '%' }"></div>
</div>
</div>
2025-01-02 08:26:32 +00:00
</div>
2024-12-26 14:41:51 +00:00
<span class="block text-muted-color leading-6 mb-4">Continue editing your existing themes or build a new one.</span>
2025-01-03 16:06:56 +00:00
<div class="flex flex-wrap gap-4">
2025-01-02 08:26:32 +00:00
<button
type="button"
:class="[
2025-01-03 16:56:06 +00:00
'rounded-xl h-32 w-32 bg-transparent border border-surface-200 dark:border-surface-700 text-black dark:text-white',
{ 'opacity-50 cursor-auto': themeLimitReached, 'hover:border-surface-400 dark:hover:border-surface-500': !themeLimitReached }
2025-01-02 08:26:32 +00:00
]"
@click="openNewTheme"
>
<i class="pi pi-plus"></i>
2024-12-26 14:41:51 +00:00
</button>
<template v-if="loading">
2025-01-03 16:06:56 +00:00
<Skeleton class="!rounded-xl !h-32 !w-32">-</Skeleton>
<Skeleton class="!rounded-xl !h-32 !w-32">-</Skeleton>
2024-12-26 14:41:51 +00:00
</template>
<template v-else>
<div v-for="theme of $appState.designer.themes" :key="theme.t_key" class="flex flex-col gap-2 relative">
<button
type="button"
2025-01-03 16:56:06 +00:00
class="rounded-xl h-32 w-32 px-4 overflow-hidden text-ellipsis bg-transparent border border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-500 text-black dark:text-white"
2024-12-26 14:41:51 +00:00
@click="loadTheme(theme)"
>
2025-01-03 16:06:56 +00:00
<span class="text-2xl uppercase font-bold">{{ abbrThemeName(theme) }}</span>
2024-12-26 14:41:51 +00:00
</button>
2025-01-03 16:06:56 +00:00
<div class="flex flex-col items-center gap-1">
2024-12-26 14:41:51 +00:00
<div class="group flex items-center gap-2 relative">
2025-02-18 11:53:26 +00:00
<input v-model="theme.t_name" type="text" class="w-24 text-sm px-2 text-center pr-4" maxlength="100" @blur="renameTheme(theme)" />
<i class="hidden group-hover:block pi pi-pencil !text-xs absolute top-50 text-muted-color" style="right: 2px"></i>
2024-12-26 14:41:51 +00:00
</div>
2024-12-27 20:17:25 +00:00
<span class="text-muted-color text-xs">{{ formatTimestamp(theme.t_last_updated) }}</span>
2024-12-26 14:41:51 +00:00
</div>
2025-01-03 16:56:06 +00:00
<button type="button" @click="toggleMenuOptions($event, theme)" class="hover:bg-surface-100 dark:hover:bg-surface-800 text-surface-500 dark:text-surface-400 flex absolute top-1 right-1 w-8 h-8 rounded-lg items-center justify-center">
2025-01-03 16:06:56 +00:00
<i class="pi pi-ellipsis-h !text-xs" />
2024-12-26 14:41:51 +00:00
</button>
</div>
</template>
<Menu ref="themeMenu" :model="themeOptions" :popup="true" />
</div>
</template>
<script>
2025-02-17 11:28:56 +00:00
import { usePreset } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
2024-12-26 14:41:51 +00:00
export default {
setup() {
const runtimeConfig = useRuntimeConfig();
return {
2025-02-18 07:36:04 +00:00
designerApiUrl: runtimeConfig.public.designerApiUrl
2024-12-26 14:41:51 +00:00
};
},
2024-12-27 20:17:25 +00:00
inject: ['designerService'],
2024-12-26 14:41:51 +00:00
data() {
return {
2025-02-11 10:26:24 +00:00
licenseKey: null,
otp: null,
2024-12-26 14:41:51 +00:00
loading: false,
currentTheme: null,
confirmDialogVisible: false,
themeOptions: [
{
label: 'Delete',
icon: 'pi pi-times',
command: () => {
this.$confirm.require({
group: 'designer',
message: 'Are you sure you want to delete this theme?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
2024-12-30 20:13:05 +00:00
acceptProps: {
severity: 'contrast'
},
2024-12-26 14:41:51 +00:00
rejectProps: {
severity: 'secondary'
},
accept: () => {
this.deleteTheme(this.currentTheme);
}
});
}
},
{
label: 'Duplicate',
icon: 'pi pi-copy',
command: () => {
this.duplicateTheme(this.currentTheme);
}
},
2024-12-27 20:17:25 +00:00
{
label: 'Download',
icon: 'pi pi-download',
command: () => {
this.designerService.downloadTheme(this.currentTheme);
}
}
2024-12-26 14:41:51 +00:00
]
};
},
created() {
2025-02-11 10:26:24 +00:00
if (this.$appState.designer.verified) {
2024-12-26 14:41:51 +00:00
this.loadThemes();
}
},
methods: {
2025-02-17 11:28:56 +00:00
async activate() {
2025-02-18 07:36:04 +00:00
const { data, error } = await $fetch(this.designerApiUrl + '/license/signin/' + this.licenseKey, {
2025-02-11 10:26:24 +00:00
credentials: 'include',
query: {
passkey: this.otp
}
});
2024-12-26 14:41:51 +00:00
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
2024-12-27 20:17:25 +00:00
if (data.valid) {
2025-02-11 10:26:24 +00:00
this.$appState.designer.verified = true;
2025-02-17 23:12:20 +00:00
this.$appState.designer.csrfToken = data.csrfToken;
2025-01-02 08:26:32 +00:00
this.$appState.designer.themeLimit = data.themeLimit;
2025-01-01 11:27:54 +00:00
this.loadThemes();
2025-02-17 11:28:56 +00:00
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'License is activated.', life: 3000 });
2024-12-26 14:41:51 +00:00
} else {
2025-02-11 10:26:24 +00:00
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'Invalid key.', life: 3000 });
2024-12-26 14:41:51 +00:00
this.$appState.designer.themes = [];
}
}
},
2025-02-11 10:26:24 +00:00
async signOut() {
2025-02-18 07:36:04 +00:00
const { data } = await $fetch(this.designerApiUrl + '/license/signout/', {
2025-02-11 10:26:24 +00:00
credentials: 'include'
});
if (data.signout) {
this.$appState.designer = {
verified: false,
ticket: null,
themeLimit: null,
active: true,
activeView: 'dashboard',
activeTab: '0',
theme: {
key: null,
name: null,
preset: null,
config: null
},
acTokens: [],
themes: []
};
this.licenseKey = null;
this.otp = null;
this.currentTheme = null;
usePreset(Aura);
2025-02-17 11:28:56 +00:00
document.body.classList.remove('material');
this.$primevue.config.ripple = false;
2025-02-11 10:26:24 +00:00
}
},
2024-12-26 14:41:51 +00:00
openNewTheme() {
2025-01-02 08:26:32 +00:00
if (!this.themeLimitReached) {
this.$appState.designer.activeView = 'create_theme';
}
2024-12-26 14:41:51 +00:00
},
async loadThemes() {
2024-12-28 09:43:15 +00:00
this.loading = true;
2025-02-18 07:36:04 +00:00
const { data, error } = await $fetch(this.designerApiUrl + '/theme/list/', {
2025-02-11 10:26:24 +00:00
credentials: 'include',
2025-01-01 11:27:54 +00:00
headers: {
2025-02-17 23:12:20 +00:00
'X-CSRF-Token': this.$appState.designer.csrfToken
2025-01-01 11:27:54 +00:00
}
});
2024-12-26 14:41:51 +00:00
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.$appState.designer.themes = data;
}
2024-12-28 09:43:15 +00:00
this.loading = false;
2024-12-26 14:41:51 +00:00
},
async loadTheme(theme) {
2025-02-18 07:36:04 +00:00
const { data, error } = await $fetch(this.designerApiUrl + '/theme/load/' + theme.t_key, {
2025-02-11 10:26:24 +00:00
credentials: 'include',
2025-01-01 11:27:54 +00:00
headers: {
2025-02-17 23:12:20 +00:00
'X-CSRF-Token': this.$appState.designer.csrfToken
2025-01-01 11:27:54 +00:00
}
});
2024-12-26 14:41:51 +00:00
if (error) {
2024-12-27 20:17:25 +00:00
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
2024-12-26 14:41:51 +00:00
} else {
this.designerService.activateTheme(data);
2025-02-18 18:14:33 +00:00
this.$appState.designer.activeTab = '0';
2024-12-26 14:41:51 +00:00
this.$appState.designer.activeView = 'editor';
}
},
async renameTheme(theme) {
2025-02-18 07:36:04 +00:00
const { error } = await $fetch(this.designerApiUrl + '/theme/rename/' + theme.t_key, {
2024-12-26 14:41:51 +00:00
method: 'PATCH',
2025-02-11 10:26:24 +00:00
credentials: 'include',
2025-01-01 11:27:54 +00:00
headers: {
2025-02-17 23:12:20 +00:00
'X-CSRF-Token': this.$appState.designer.csrfToken
2025-01-01 11:27:54 +00:00
},
2024-12-26 14:41:51 +00:00
body: {
2025-01-01 11:27:54 +00:00
name: theme.t_name
2024-12-26 14:41:51 +00:00
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
}
},
async deleteTheme(theme) {
2025-02-18 07:36:04 +00:00
const { error } = await $fetch(this.designerApiUrl + '/theme/delete/' + theme.t_key, {
2024-12-26 14:41:51 +00:00
method: 'DELETE',
2025-02-11 10:26:24 +00:00
credentials: 'include',
2025-01-01 11:27:54 +00:00
headers: {
2025-02-17 23:12:20 +00:00
'X-CSRF-Token': this.$appState.designer.csrfToken
2024-12-26 14:41:51 +00:00
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.loadThemes();
}
},
async duplicateTheme(theme) {
2025-02-18 07:36:04 +00:00
const { error } = await $fetch(this.designerApiUrl + '/theme/duplicate/' + theme.t_key, {
2024-12-26 14:41:51 +00:00
method: 'POST',
2025-02-11 10:26:24 +00:00
credentials: 'include',
2025-01-01 11:27:54 +00:00
headers: {
2025-02-17 23:12:20 +00:00
'X-CSRF-Token': this.$appState.designer.csrfToken
2024-12-26 14:41:51 +00:00
}
});
if (error) {
this.$toast.add({ severity: 'error', summary: 'An Error Occurred', detail: error.message, life: 3000 });
} else {
this.loadThemes();
}
},
formatTimestamp(timestamp) {
const date = new Date(timestamp);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
};
return date.toLocaleString('en-US', options);
},
toggleMenuOptions(event, theme) {
this.currentTheme = theme;
this.$refs.themeMenu.toggle(event);
},
abbrThemeName(theme) {
2025-01-02 08:26:32 +00:00
return theme.t_name ? theme.t_name.substring(0, 2) : 'UT';
}
},
computed: {
themeLimitReached() {
2025-01-03 16:06:56 +00:00
return this.$appState.designer.themeLimit ? this.$appState.designer.themeLimit === this.$appState.designer.themes.length : false;
},
themeUsageRatio() {
return this.$appState.designer.themeLimit ? 100 * (this.$appState.designer.themes.length / this.$appState.designer.themeLimit) : 0;
2025-01-13 09:28:00 +00:00
},
coverImage() {
const image = this.$appState.darkTheme ? 'editor-intro-dark.png' : 'editor-intro.png';
return 'https://primefaces.org/cdn/designer/' + image;
2024-12-26 14:41:51 +00:00
}
}
};
</script>