feat(upc): avatar & brand components

This commit is contained in:
Zack Spear
2023-06-01 00:11:26 -07:00
parent 314160a9ea
commit 44ccb7abf0
14 changed files with 368 additions and 23 deletions

View File

@@ -37,17 +37,21 @@ if (state === 'TRIAL') expireTime = Date.now() + 60 * 60 * 1000; // in 1 hour
if (state === 'EEXPIRED') expireTime = uptime; // 1 hour ago
const serverState = {
// avatar: '',
avatar: 'https://source.unsplash.com/300x300/?portrait',
name: 'DevServer9000',
description: 'Fully automated media server and lab rig',
description: 'Fully automated media server',
guid: '9292-1111-BITE-444444444444',
deviceCount: 8,
expireTime,
lanIp: '192.168.0.1',
locale: 'en',
registered: false,
pluginInstalled: true,
registered: true,
site: 'http://localhost:4321',
state,
uptime,
username: 'zspearmint'
};
export default serverState;

View File

@@ -8,7 +8,7 @@ body {
--color-gamma: #999999;
--color-customgradient-start: rgba(242, 242, 242, .0);
--color-customgradient-end: rgba(242, 242, 242, .85);
/* --shadow-beta: 0 25px 50px -12px ${hexToRgba(beta, 0.15)};
--ring-offset-shadow: 0 0 ${beta};
--ring-shadow: 0 0 ${beta}; */
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, .15);
--ring-offset-shadow: 0 0 --var(--color-beta);
--ring-shadow: 0 0 --var(--color-beta);
}

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useServerStore } from '~/store/server';
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
const serverStore = useServerStore();
const { avatar, pluginInstalled, registered, username } = storeToRefs(serverStore);
// :class="{
// 'ml-8px': usernameButtonText,
// 'bg-transparent': registered && !avatarFail,
// 'bg-gradient-to-r from-red to-orange': !registered || avatarFail,
// }"
// :title="usernameButtonTitle"
</script>
<template>
<figure class="group relative z-0 flex items-center justify-center w-36px h-36px rounded-full bg-gradient-to-r from-red to-orange">
<img
v-if="avatar && pluginInstalled && registered"
:src="avatar"
:alt="username"
class="absolute z-10 inset-0 w-36px h-36px rounded-full overflow-hidden">
<template v-else>
<BrandMark gradient-start="#fff" gradient-stop="#fff" class="opacity-100 group-hover:opacity-0 absolute z-10 w-36px px-4px" />
<BrandLoading gradient-start="#fff" gradient-stop="#fff" class="opacity-0 group-hover:opacity-100 absolute z-10 w-36px px-4px" :height="36" />
</template>
</figure>
</template>

View File

@@ -0,0 +1,138 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
height?: number,
title?: string,
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
height: 64,
title: 'Loading',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 133.52 76.97"
class="unraid_sc_loader"
:class="`h-[${height}px]`"
role="img"
>
<title>{{ title }}</title>
<desc>Unraid logo animating with a wave like effect</desc>
<defs>
<linearGradient
id="unraidLoadingGradient"
x1="23.76"
y1="81.49"
x2="109.76"
y2="-4.51"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<path
d="m70,19.24zm57,0l6.54,0l0,38.49l-6.54,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_9"
/>
<path
d="m70,19.24zm47.65,11.9l-6.55,0l0,-23.79l6.55,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_8"
/>
<path
d="m70,19.24zm31.77,-4.54l-6.54,0l0,-14.7l6.54,0l0,14.7z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_7"
/>
<path
d="m70,19.24zm15.9,11.9l-6.54,0l0,-23.79l6.54,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_6"
/>
<path
d="m63.49,19.24l6.51,0l0,38.49l-6.51,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_5"
/>
<path
d="m70,19.24zm-22.38,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_4"
/>
<path
d="m70,19.24zm-38.26,43.03l6.55,0l0,14.73l-6.55,0l0,-14.73z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_3"
/>
<path
d="m70,19.24zm-54.13,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_2"
/>
<path
d="m70,19.24zm-63.46,38.49l-6.54,0l0,-38.49l6.54,0l0,38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_sc_loader_1"
/>
</svg>
</template>
<style lang="postcss" scoped>
.unraid_sc_loader_2,
.unraid_sc_loader_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_sc_loader_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_sc_loader_6,
.unraid_sc_loader_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_sc_loader_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

38
components/Brand/Logo.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 222.36 39.04"
>
<defs>
<linearGradient
id="unraidLogo"
x1="47.53"
y1="79.1"
x2="170.71"
y2="-44.08"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<title>Unraid Logo</title>
<path
d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z"
fill="url(#unraidLogo)"
/>
</svg>
</template>

38
components/Brand/Mark.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 133.52 76.97"
>
<title>unraid-mark</title>
<defs>
<linearGradient
id="unraid-mark"
x1="23.76"
y1="81.49"
x2="109.76"
y2="-4.51"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<path
fill="url(#unraid-mark)"
d="M63.49,19.24H70V57.73H63.49ZM6.54,57.73H0V19.24H6.54Zm25.2,4.54h6.55V77H31.74ZM15.87,45.84h6.54V69.62H15.87Zm31.75,0h6.54V69.62H47.62ZM127,19.24h6.54V57.73H127ZM101.77,14.7H95.23V0h6.54Zm15.88,16.44H111.1V7.35h6.55Zm-31.75,0H79.36V7.35H85.9Z"
/>
</svg>
</template>

View File

@@ -74,9 +74,9 @@ onBeforeMount(() => {
<template>
<div id="UserProfile" class="text-alpha relative z-20 flex flex-col h-full pl-80px rounded">
<div class="text-gamma text-12px text-right font-semibold leading-normal flex flex-row items-baseline justify-end gap-x-12px">
<upc-uptime-expire :time="uptimeOrExpiredTime" :state="state" />
<UpcUptimeExpire :time="uptimeOrExpiredTime" :state="state" />
<span>&bull;</span>
<upc-server-state />
<UpcServerState />
</div>
<div class="relative z-0 flex flex-row items-center justify-end gap-x-16px h-full">
@@ -98,8 +98,8 @@ onBeforeMount(() => {
<div class="block w-2px h-24px bg-grey-mid"></div>
<div ref="dropdown" class="relative flex items-center justify-end h-full">
<upc-dropdown-trigger @click="toggleDropdown" />
<upc-dropdown v-show="dropdownOpen" />
<UpcDropdownTrigger @click="toggleDropdown" :open="dropdownOpen" />
<UpcDropdown v-show="dropdownOpen" />
</div>
</div>
</div>

View File

@@ -85,11 +85,11 @@ const links = computed(():UserProfileLink[] => {
</script>
<template>
<upc-dropdown-wrapper class="Dropdown min-w-300px max-w-350px">
<UpcDropdownWrapper class="min-w-300px max-w-350px">
<header class="flex flex-row items-start justify-between mt-8px mx-8px">
<h3 class="text-18px leading-none inline-flex flex-row gap-x-8px items-center">
<span class="font-semibold">Connect</span>
<upc-beta />
<UpcBeta />
<span v-if="myServersEnv" :title="`API • ${myServersEnv}`"></span>
<span v-if="devEnv" :title="`UPC • ${devEnv}`"></span>
</h3>
@@ -97,28 +97,25 @@ const links = computed(():UserProfileLink[] => {
<ul class="list-reset flex flex-col gap-y-4px p-0">
<template v-if="stateDataKeyActions">
<li v-for="action in stateDataKeyActions" :key="action.name">
<upc-dropdown-item :item="action" />
<UpcDropdownItem :item="action" />
</li>
</template>
<li class="m-8px">
<upc-keyline />
<UpcKeyline />
</li>
<template v-if="links">
<li v-for="(link, index) in links" :key="`link_${index}`">
<upc-dropdown-item :item="link" />
<UpcDropdownItem :item="link" />
</li>
</template>
</ul>
</upc-dropdown-wrapper>
</UpcDropdownWrapper>
</template>
<style lang="postcss" scoped>
.Dropdown {
@apply text-beta;
top: 95%;
/* .Dropdown {
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
&::before {
@@ -133,5 +130,5 @@ const links = computed(():UserProfileLink[] => {
border-bottom: 11px solid var(--color-alpha);
border-left: 11px solid transparent;
}
}
} */
</style>

View File

@@ -1,9 +1,68 @@
<script setup lang="ts">
defineEmits(['click'])
import { storeToRefs } from 'pinia';
import { ChevronDownIcon, InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
import { useServerStore } from '~/store/server';
export interface Props {
open?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
defineEmits(['click']);
const {
pluginInstalled,
pluginOutdated,
registered,
state,
stateData,
username,
} = storeToRefs(useServerStore());
const registeredAndPluginInstalled = computed(() => {
return pluginInstalled.value && registered.value;
});
const text = computed((): string | undefined => {
if ((stateData.value.error) && state.value !== 'EEXPIRED') return 'Fix Error';
if (registeredAndPluginInstalled.value) return username.value;
return;
});
const title = computed((): string => {
if (state.value === 'ENOKEYFILE') return 'Get Started';
if (state.value === 'EEXPIRED') return 'Trial Expired, see options below';
if (stateData.value.error) return 'Learn More';
// if (cloud.value && cloud.value.error) return 'Unraid API Error';
// if (myServersError.value && registeredAndPluginInstalled.value return 'Unraid API Error';
// if (errorTooManyDisks.value) return 'Too many devices';
// if (isLaunchpadOpen.value) return 'Close and continue to webGUI';
return props.open ? 'Close Dropdown' : 'Open Dropdown';
});
</script>
<template>
<div class="relative flex items-center justify-end h-full">
<button @click="$emit('click')">Open</button>
<button
@click="$emit('click')"
class="group text-18px hover:text-alpha focus:text-alpha border border-transparent flex flex-row justify-end items-center h-full gap-x-8px outline-none focus:outline-none"
:title="title"
>
<!-- show info icon for non-error of myServersOutOfDate b/c it still allows the API to connect -->
<InformationCircleIcon v-if="pluginOutdated" class="text-red fill-current relative w-24px h-24px" />
<!-- v-else-if="showWarning" -->
<ExclamationTriangleIcon class="text-red fill-current relative w-24px h-24px" />
<span class="flex flex-row items-center gap-x-8px">
{{ text }}
<UpcTriangleDown v-if="registeredAndPluginInstalled" :open="open" />
</span>
<BrandAvatar />
<UpcTriangleDown v-if="!registeredAndPluginInstalled" :open="open" />
</button>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<nav class="absolute z-30 right-0 flex flex-col gap-y-8px p-8px bg-alpha border rounded-lg">
<nav class="text-beta absolute z-30 top-full right-0 flex flex-col gap-y-8px p-8px bg-alpha border rounded-lg shadow-[var(--ring-offset-shadow)_var(--ring-shadow)_var(--shadow-beta)]">
<slot/>
</nav>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
export interface Props {
open?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
</script>
<template>
<span
class="w-0 h-0 border-t-[6px] border-l-[6px] border-r-[6px] border-t-solid border-t-black border-l-solid border-l-transparent border-r-solid border-r-transparent transition-transform"
:class="{
'-rotate-180': open,
'rotate-0': !open,
}"
/>
</template>

View File

@@ -11,6 +11,7 @@ export default defineNuxtConfig({
'nuxt-custom-elements',
],
components: [
{ path: '~/components/Brand', prefix: 'Brand' },
{ path: '~/components/UserProfile', prefix: 'Upc' },
'~/components',
],

View File

@@ -2,6 +2,7 @@ import { defineStore, createPinia, setActivePinia } from "pinia";
import { ArrowRightOnRectangleIcon, GlobeAltIcon, KeyIcon } from '@heroicons/vue/24/solid';
import type {
Server,
ServerState,
ServerStateData,
} from '~/types/server';
/**
@@ -14,6 +15,7 @@ export const useServerStore = defineStore('server', () => {
/**
* State
*/
const avatar = ref<string>(); // @todo potentially move to a user store
const description = ref<string>();
const deviceCount = ref<number>();
const expireTime = ref<number>();
@@ -32,13 +34,19 @@ export const useServerStore = defineStore('server', () => {
const site = ref<string>();
const state = ref<string>(); // @todo implement ServerState ENUM
const uptime = ref<number>();
const username = ref<string>(); // @todo potentially move to a user store
const wanFQDN = ref<string>();
/**
* Getters
*/
const pluginOutdated = computed(():boolean => {
return false;
});
const server = computed<Server>(():Server => {
return {
avatar: avatar.value,
description: description.value,
deviceCount: deviceCount.value,
expireTime: expireTime.value,
@@ -57,6 +65,7 @@ export const useServerStore = defineStore('server', () => {
site: site.value,
state: state.value,
uptime: uptime.value,
username: username.value,
wanFQDN: wanFQDN.value,
}
});
@@ -191,6 +200,7 @@ export const useServerStore = defineStore('server', () => {
*/
const setServer = (data: Server) => {
console.debug('[setServer]', data);
avatar.value = data?.avatar;
description.value = data?.description;
deviceCount.value = data?.deviceCount;
expireTime.value = data?.expireTime;
@@ -209,11 +219,13 @@ export const useServerStore = defineStore('server', () => {
site.value = data?.site;
state.value = data?.state;
uptime.value = data?.uptime;
username.value = data?.username;
wanFQDN.value = data?.wanFQDN;
};
return {
// state
avatar,
description,
deviceCount,
expireTime,
@@ -228,7 +240,9 @@ export const useServerStore = defineStore('server', () => {
site,
state,
uptime,
username,
// getters
pluginOutdated,
server,
stateData,
// actions

View File

@@ -20,6 +20,7 @@ export enum ServerState {
ENOCONN = 'ENOCONN',
}
export interface Server {
avatar?: string;
description?: string;
deviceCount?: number;
expireTime?: number;
@@ -39,6 +40,7 @@ export interface Server {
// state?: ServerState;
state: string;
uptime?: number;
username?: string;
wanFQDN?: string;
}