refactor: update UI styles to use new header background color

- Changed background color for various select elements and containers in Users.vue and Videos.vue to use 'bg-header'.
- Updated background color for status and role filters in the admin section.
- Adjusted background colors in Home.vue, QuickActions.vue, and other components to enhance UI consistency.
- Refactored Billing.vue and DomainsDns.vue to align with new design standards.
- Modified settings components to utilize new header color for better visual hierarchy.
- Improved accessibility and visual feedback in the SettingsRow and SettingsSectionCard components.
- Updated authentication middleware to include timestamp cookie for session management.
- Enhanced gRPC client to build internal metadata for service calls.
This commit is contained in:
2026-03-16 17:09:31 +07:00
parent b4bbacd9f1
commit 90d8409aa9
43 changed files with 174 additions and 241 deletions

View File

@@ -47,6 +47,7 @@ export function httpClientAdapter(opts: {
} else { } else {
res = await fetch(req); res = await fetch(req);
} }
if (!res.ok) { if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`); // throw new Error(`HTTP error: ${res.status}`);
throw new Error( throw new Error(
@@ -59,6 +60,9 @@ export function httpClientAdapter(opts: {
); );
// throw TinyRpcError.deserialize(res.status); // throw TinyRpcError.deserialize(res.status);
} }
// if (res.headers.get("set-cookie")) {
// console.log("Response has set-cookie header:", res.headers.get("set-cookie"));
// }
const result: Result<unknown, unknown> = JSON.parse( const result: Result<unknown, unknown> = JSON.parse(
await res.text() await res.text()
); );

View File

@@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import Upload from "@/routes/upload/Upload.vue";
import DashboardNav from "./DashboardNav.vue"; import DashboardNav from "./DashboardNav.vue";
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue"; import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
import Upload from "@/routes/upload/Upload.vue";
</script> </script>
<template> <template>
<DashboardNav /> <DashboardNav />
<main class="flex flex-1 flex-col transition-all duration-300 ease-in-out bg-page md:ps-18"> <main class="flex flex-1 flex-col transition-all duration-300 ease-in-out bg-white md:ps-18">
<div class=":uno: flex-1 overflow-auto p-4 bg-page rounded-lg md:(mr-2 mb-2) min-h-[calc(100vh-8rem)]"> <div class=":uno: flex-1 overflow-auto p-4 bg-white rounded-lg md:(mr-2 mb-2) min-h-[calc(100vh-8rem)]">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<Transition enter-active-class="transition-all duration-300 ease-in-out" <Transition enter-active-class="transition-all duration-300 ease-in-out"
enter-from-class="opacity-0 transform translate-y-4" enter-from-class="opacity-0 transform translate-y-4"

View File

@@ -7,7 +7,7 @@ import Video from "@/components/icons/Video.vue";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { computed, createStaticVNode, ref, VNode } from "vue"; import { computed, createStaticVNode, ref } from "vue";
import NotificationDrawer from "./NotificationDrawer.vue"; import NotificationDrawer from "./NotificationDrawer.vue";
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0"; const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
@@ -58,7 +58,7 @@ const links = computed<Record<string, any>>(() => {
<template> <template>
<header <header
class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-muted transition-all duration-300 ease-in-out w-18 items-center" class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-header transition-all duration-300 ease-in-out w-18 items-center border-r border-border text-foreground/60"
> >
<template v-for="i in links" :key="i.href"> <template v-for="i in links" :key="i.href">
<component <component

View File

@@ -1,66 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils';
import { computed } from 'vue'; import { computed } from 'vue';
type UiButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost'; type UiButtonSize = 'sm' | 'md' | 'lg';
type Size = 'sm' | 'md'; const props = withDefaults(
defineProps<{
const props = withDefaults(defineProps<{ variant?: UiButtonVariant;
variant?: Variant; size?: UiButtonSize;
size?: Size; block?: boolean;
loading?: boolean;
disabled?: boolean; disabled?: boolean;
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
}>(), { }>(),
variant: 'primary', {
variant: 'secondary',
size: 'md', size: 'md',
loading: false, block: false,
disabled: false, disabled: false,
type: 'button', type: 'button',
},
);
const classes = computed(() => {
const variants: Record<UiButtonVariant, string> = {
primary: 'border-transparent bg-primary text-white hover:bg-primaryHover focus-visible:ring-primary/25',
secondary: 'border-border bg-white text-text hover:bg-header focus-visible:ring-#0969da/20',
ghost: 'border-transparent bg-transparent text-text hover:bg-header focus-visible:ring-#0969da/20 shadow-none',
danger: 'border-transparent bg-danger text-white hover:opacity-92 focus-visible:ring-danger/20',
};
const sizes: Record<UiButtonSize, string> = {
sm: 'min-h-[28px] px-3 text-[12px] leading-[20px]',
md: 'min-h-[32px] px-3 text-[14px] leading-[20px]',
lg: 'min-h-[36px] px-4 text-[14px] leading-[20px]',
};
return [
'inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap shadow-primer outline-none transition-[transform,box-shadow,background-color,border-color,color] duration-150 ease-out active:translate-y-[0.5px] hover:shadow-[0_2px_0_rgba(27,31,36,0.06)] disabled:cursor-not-allowed disabled:opacity-60 focus-visible:ring-4',
variants[props.variant],
sizes[props.size],
props.block ? 'w-full' : '',
].join(' ');
}); });
const baseClass = 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-all press-animated select-none';
const sizeClass = computed(() => {
switch (props.size) {
case 'sm':
return 'px-3 py-1.5 text-sm';
case 'md':
default:
return 'px-4 py-2 text-sm';
}
});
const variantClass = computed(() => {
switch (props.variant) {
case 'secondary':
return 'bg-muted/50 text-foreground hover:bg-muted border border-border';
case 'danger':
return 'bg-danger text-white hover:bg-danger/90';
case 'ghost':
return 'bg-transparent text-foreground/70 hover:text-foreground hover:bg-muted/50';
case 'primary':
default:
return 'bg-primary text-white hover:bg-primary/90';
}
});
const disabledClass = computed(() => (props.disabled || props.loading) ? 'opacity-60 cursor-not-allowed' : '');
</script> </script>
<template> <template>
<button <button :type="type" :disabled="disabled" :class="classes">
:type="type"
:disabled="disabled || loading"
:class="cn(baseClass, sizeClass, variantClass, disabledClass)"
>
<span v-if="loading" class="inline-flex items-center" aria-hidden="true">
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
</svg>
</span>
<slot name="icon" />
<slot /> <slot />
</button> </button>
</template> </template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import XIcon from '@/components/icons/XIcon.vue'; import XIcon from '@/components/icons/XIcon.vue';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
// Ensure client-side only rendering to avoid hydration mismatch // Ensure client-side only rendering to avoid hydration mismatch
const isMounted = ref(false); const isMounted = ref(false);
@@ -75,7 +75,7 @@ onBeforeUnmount(() => {
<!-- Panel --> <!-- Panel -->
<div class="absolute inset-0 flex items-center justify-center p-4"> <div class="absolute inset-0 flex items-center justify-center p-4">
<div :class="cn('w-full bg-surface border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)"> <div :class="cn('w-full bg-header border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
<!-- Header slot --> <!-- Header slot -->
<div v-if="$slots.header" class="px-5 py-4 border-b border-border"> <div v-if="$slots.header" class="px-5 py-4 border-b border-border">
<slot name="header" :close="close" /> <slot name="header" :close="close" />

View File

@@ -61,7 +61,7 @@ const onKeyup = (e: KeyboardEvent) => {
if (e.key === 'Enter') emit('enter'); if (e.key === 'Enter') emit('enter');
}; };
const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-surface text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 disabled:opacity-60 disabled:cursor-not-allowed'; const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-header text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 disabled:opacity-60 disabled:cursor-not-allowed';
</script> </script>
<template> <template>

View File

@@ -40,7 +40,7 @@ const iconColors = {
<template> <template>
<div :class="[ <div :class="[
'transform translate-y-0 relative overflow-hidden rounded-2xl p-6 bg-surface', 'transform translate-y-0 relative overflow-hidden rounded-2xl p-6 bg-header',
// gradients[color], // gradients[color],
'border border-gray-300 transition-all duration-300', 'border border-gray-300 transition-all duration-300',
// 'group cursor-pointer' // 'group cursor-pointer'

View File

@@ -6,12 +6,12 @@
fill="#a6acb9" /> fill="#a6acb9" />
<path <path
d="M74 42c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32V74c0-18-14-32-32-32H74zM10 74c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H74c-35 0-64-29-64-64V74zm208 256v-80h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z" d="M74 42c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32V74c0-18-14-32-32-32H74zM10 74c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H74c-35 0-64-29-64-64V74zm208 256v-80h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="v-mid m-a" height="24" viewBox="-10 -226 468 468"> <svg v-else xmlns="http://www.w3.org/2000/svg" class="v-mid m-a" height="24" viewBox="-10 -226 468 468">
<path <path
d="M64-184c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32v-320c0-18-14-32-32-32H64zM0-152c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H64c-35 0-64-29-64-64v-320zm208 256V24h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z" d="M64-184c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32v-320c0-18-14-32-32-32H64zM0-152c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H64c-35 0-64-29-64-64v-320zm208 256V24h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,5 +1,5 @@
<template> <template>
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="#a6acb9"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="#1e3050"/></svg> <svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="#a6acb9"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="currentColor"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="518" viewBox="-10 -244 500 518"><path d="M461-229c12 5 19 16 19 29v416c0 13-7 24-19 29-11 5-25 3-34-5l-47-41c-43-38-98-60-156-63v96c0 18-14 32-32 32h-32c-18 0-32-14-32-32v-96C57 136 0 79 0 8s57-128 128-128h85c61 0 121-23 167-63l47-41c9-8 23-10 34-5zM224 72c70 3 138 29 192 74v-276c-54 45-122 71-192 74V72z" fill="currentColor"/></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="518" viewBox="-10 -244 500 518"><path d="M461-229c12 5 19 16 19 29v416c0 13-7 24-19 29-11 5-25 3-34-5l-47-41c-43-38-98-60-156-63v96c0 18-14 32-32 32h-32c-18 0-32-14-32-32v-96C57 136 0 79 0 8s57-128 128-128h85c61 0 121-23 167-63l47-41c9-8 23-10 34-5zM224 72c70 3 138 29 192 74v-276c-54 45-122 71-192 74V72z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 580 524"><path d="M10 234v112c0 46 38 84 84 84s84-38 84-84V234c0-46-38-84-84-84s-84 38-84 84zM206 94v252c0 46 38 84 84 84s84-38 84-84V94c0-46-38-84-84-84s-84 38-84 84zm196 56v196c0 46 38 84 84 84s84-38 84-84V150c0-46-38-84-84-84s-84 38-84 84z" fill="#a6acb9"/><path d="M10 500c0-8 6-14 14-14h532c8 0 14 6 14 14s-6 14-14 14H24c-8 0-14-6-14-14z" fill="#1e3050"/></svg> <svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 580 524"><path d="M10 234v112c0 46 38 84 84 84s84-38 84-84V234c0-46-38-84-84-84s-84 38-84 84zM206 94v252c0 46 38 84 84 84s84-38 84-84V94c0-46-38-84-84-84s-84 38-84 84zm196 56v196c0 46 38 84 84 84s84-38 84-84V150c0-46-38-84-84-84s-84 38-84 84z" fill="#a6acb9"/><path d="M10 500c0-8 6-14 14-14h532c8 0 14 6 14 14s-6 14-14 14H24c-8 0-14-6-14-14z" fill="currentColor"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 532 468"><path d="M272-184c9 0 16 7 16 16v352c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-352c0-9 7-16 16-16h32zm-32-32c-26 0-48 22-48 48v352c0 27 22 48 48 48h32c27 0 48-21 48-48v-352c0-26-21-48-48-48h-32zM80 8c9 0 16 7 16 16v160c0 9-7 16-16 16H48c-9 0-16-7-16-16V24c0-9 7-16 16-16h32zM48-24C22-24 0-2 0 24v160c0 27 22 48 48 48h32c27 0 48-21 48-48V24c0-26-21-48-48-48H48zm384-96h32c9 0 16 7 16 16v288c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-288c0-9 7-16 16-16zm-48 16v288c0 27 22 48 48 48h32c27 0 48-21 48-48v-288c0-26-21-48-48-48h-32c-26 0-48 22-48 48z" fill="#1e3050"/></svg> <svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 532 468"><path d="M272-184c9 0 16 7 16 16v352c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-352c0-9 7-16 16-16h32zm-32-32c-26 0-48 22-48 48v352c0 27 22 48 48 48h32c27 0 48-21 48-48v-352c0-26-21-48-48-48h-32zM80 8c9 0 16 7 16 16v160c0 9-7 16-16 16H48c-9 0-16-7-16-16V24c0-9 7-16 16-16h32zM48-24C22-24 0-2 0 24v160c0 27 22 48 48 48h32c27 0 48-21 48-48V24c0-26-21-48-48-48H48zm384-96h32c9 0 16 7 16 16v288c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-288c0-9 7-16 16-16zm-48 16v288c0 27 22 48 48 48h32c27 0 48-21 48-48v-288c0-26-21-48-48-48h-32c-26 0-48 22-48 48z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ filled?: boolean }>(); defineProps<{ filled?: boolean }>();

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="#a6acb9"/><path d="M10 106h512v64H10zm0 0z" fill="#1e3050"/></svg> <svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="#a6acb9"/><path d="M10 106h512v64H10zm0 0z" fill="currentColor"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -194 532 404"><path d="M448-136c9 0 16 7 16 16v32H48v-32c0-9 7-16 16-16h384zm16 112v160c0 9-7 16-16 16H64c-9 0-16-7-16-16V-24h416zM64-184c-35 0-64 29-64 64v256c0 35 29 64 64 64h384c35 0 64-29 64-64v-256c0-35-29-64-64-64H64zM80 96c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24h-48c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="#1e3050"/></svg> <svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -194 532 404"><path d="M448-136c9 0 16 7 16 16v32H48v-32c0-9 7-16 16-16h384zm16 112v160c0 9-7 16-16 16H64c-9 0-16-7-16-16V-24h416zM64-184c-35 0-64 29-64 64v256c0 35 29 64 64 64h384c35 0 64-29 64-64v-256c0-35-29-64-64-64H64zM80 96c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24h-48c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ filled?: boolean }>(); defineProps<{ filled?: boolean }>();

View File

@@ -6,7 +6,7 @@
fill="#a6acb9" /> fill="#a6acb9" />
<path <path
d="M208 26c3 0 7 0 10 1v103c0 31 25 56 56 56h103c1 3 1 7 1 11v261c0 27-21 48-48 48H74c-26 0-48-21-48-48V74c0-26 22-48 48-48h134zm156 137c2 2 4 4 6 7h-96c-22 0-40-18-40-40V34c3 2 5 4 7 6l123 123zM74 10c-35 0-64 29-64 64v384c0 35 29 64 64 64h256c35 0 64-29 64-64V197c0-17-7-34-19-46L253 29c-12-12-28-19-45-19H74zm144 272c9 0 16 7 16 16v96c0 9-7 16-16 16h-96c-9 0-16-7-16-16v-96c0-9 7-16 16-16h96zm-96-16c-18 0-32 14-32 32v96c0 18 14 32 32 32h96c18 0 32-14 32-32v-18l40 25c10 7 24-1 24-13v-84c0-12-14-20-24-13l-40 25v-18c0-18-14-32-32-32h-96zm176 38v84l-48-30v-24l48-30z" d="M208 26c3 0 7 0 10 1v103c0 31 25 56 56 56h103c1 3 1 7 1 11v261c0 27-21 48-48 48H74c-26 0-48-21-48-48V74c0-26 22-48 48-48h134zm156 137c2 2 4 4 6 7h-96c-22 0-40-18-40-40V34c3 2 5 4 7 6l123 123zM74 10c-35 0-64 29-64 64v384c0 35 29 64 64 64h256c35 0 64-29 64-64V197c0-17-7-34-19-46L253 29c-12-12-28-19-45-19H74zm144 272c9 0 16 7 16 16v96c0 9-7 16-16 16h-96c-9 0-16-7-16-16v-96c0-9 7-16 16-16h96zm-96-16c-18 0-32 14-32 32v96c0 18 14 32 32 32h96c18 0 32-14 32-32v-18l40 25c10 7 24-1 24-13v-84c0-12-14-20-24-13l-40 25v-18c0-18-14-32-32-32h-96zm176 38v84l-48-30v-24l48-30z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
<!-- Remote link icon --> <!-- Remote link icon -->
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 564"> <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 564">
@@ -15,7 +15,7 @@
fill="#a6acb9" /> fill="#a6acb9" />
<path <path
d="M570 274H458c-2 118-55 195-99 230 116-14 207-111 211-230zM269 498c10 3 21 5 32 6-9-7-18-16-27-26l6-18c18 22 36 37 50 45 40-22 109-99 112-231H335l4-16h103c-3-132-72-209-112-231-39 22-107 96-112 224l-16 5c3-117 56-193 99-228C185 42 94 139 90 258h104l-55 16H90c0 5 1 10 1 14l-16 5c0-9-1-18-1-27C74 125 189 10 330 10s256 115 256 256-115 256-256 256c-23 0-45-3-66-9l5-15zm301-240c-4-119-95-216-211-230 44 35 97 112 99 230h112zM150 414l2 5 46 92 60-205-204 60 91 46 5 2zM31 373l-21-11 23-7 231-68 18-5-5 18-68 232-7 22-60-120-94 94-6 5-11-11 5-6 95-94-100-49z" d="M570 274H458c-2 118-55 195-99 230 116-14 207-111 211-230zM269 498c10 3 21 5 32 6-9-7-18-16-27-26l6-18c18 22 36 37 50 45 40-22 109-99 112-231H335l4-16h103c-3-132-72-209-112-231-39 22-107 96-112 224l-16 5c3-117 56-193 99-228C185 42 94 139 90 258h104l-55 16H90c0 5 1 10 1 14l-16 5c0-9-1-18-1-27C74 125 189 10 330 10s256 115 256 256-115 256-256 256c-23 0-45-3-66-9l5-15zm301-240c-4-119-95-216-211-230 44 35 97 112 99 230h112zM150 414l2 5 46 92 60-205-204 60 91 46 5 2zM31 373l-21-11 23-7 231-68 18-5-5 18-68 232-7 22-60-120-94 94-6 5-11-11 5-6 95-94-100-49z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 503"><path d="M10 397v32c0 35 29 64 64 64h320c35 0 64-29 64-64v-32c0-35-29-64-64-64H266v32c0 18-14 32-32 32s-32-14-32-32v-32H74c-35 0-64 29-64 64zm392 16c0 13-11 24-24 24s-24-11-24-24 11-24 24-24 24 11 24 24z" fill="#a6acb9"/><path d="M234 397c18 0 32-14 32-32V122l41 42c13 12 33 12 46 0 12-13 12-33 0-46l-96-96c-13-12-33-12-46 0l-96 96c-12 13-12 33 0 46 13 12 33 12 46 0l41-42v243c0 18 14 32 32 32z" fill="#1e3050"/></svg> <svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 503"><path d="M10 397v32c0 35 29 64 64 64h320c35 0 64-29 64-64v-32c0-35-29-64-64-64H266v32c0 18-14 32-32 32s-32-14-32-32v-32H74c-35 0-64 29-64 64zm392 16c0 13-11 24-24 24s-24-11-24-24 11-24 24-24 24 11 24 24z" fill="#a6acb9"/><path d="M234 397c18 0 32-14 32-32V122l41 42c13 12 33 12 46 0 12-13 12-33 0-46l-96-96c-13-12-33-12-46 0l-96 96c-12 13-12 33 0 46 13 12 33 12 46 0l41-42v243c0 18 14 32 32 32z" fill="currentColor"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -260 468 502"><path d="M248 80c0 13-11 24-24 24s-24-11-24-24v-246l-63 63c-9 9-25 9-34 0s-9-25 0-34l104-104c9-9 25-9 34 0l104 104c9 9 9 25 0 34s-25 9-34 0l-63-63V80zm-96-8H64c-9 0-16 7-16 16v80c0 9 7 16 16 16h320c9 0 16-7 16-16V88c0-9-7-16-16-16h-88V24h88c35 0 64 29 64 64v80c0 35-29 64-64 64H64c-35 0-64-29-64-64V88c0-35 29-64 64-64h88v48zm168 56c0-13 11-24 24-24s24 11 24 24-11 24-24 24-24-11-24-24z" fill="#1e3050"/></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -260 468 502"><path d="M248 80c0 13-11 24-24 24s-24-11-24-24v-246l-63 63c-9 9-25 9-34 0s-9-25 0-34l104-104c9-9 25-9 34 0l104 104c9 9 9 25 0 34s-25 9-34 0l-63-63V80zm-96-8H64c-9 0-16 7-16 16v80c0 9 7 16 16 16h320c9 0 16-7 16-16V88c0-9-7-16-16-16h-88V24h88c35 0 64 29 64 64v80c0 35-29 64-64 64H64c-35 0-64-29-64-64V88c0-35 29-64 64-64h88v48zm168 56c0-13 11-24 24-24s24 11 24 24-11 24-24 24-24-11-24-24z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ filled?: boolean }>(); defineProps<{ filled?: boolean }>();

View File

@@ -9,7 +9,7 @@
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-11 -259 535 533"> <svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-11 -259 535 533">
<path <path
d="M272-242c-9-8-23-8-32 0L8-34C-2-25-3-10 6 0s24 11 34 2l8-7v205c0 35 29 64 64 64h288c35 0 64-29 64-64V-5l8 7c10 9 25 8 34-2s8-25-2-34L272-242zM416-48v248c0 9-7 16-16 16H112c-9 0-16-7-16-16V-48l160-144L416-48z" d="M272-242c-9-8-23-8-32 0L8-34C-2-25-3-10 6 0s24 11 34 2l8-7v205c0 35 29 64 64 64h288c35 0 64-29 64-64V-5l8 7c10 9 25 8 34-2s8-25-2-34L272-242zM416-48v248c0 9-7 16-16 16H112c-9 0-16-7-16-16V-48l160-144L416-48z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -6,12 +6,12 @@
fill="#a6acb9" /> fill="#a6acb9" />
<path <path
d="M394 42c18 0 32 14 32 32v64H42V74c0-18 14-32 32-32h320zM42 394V170h96v256H74c-18 0-32-14-32-32zm128 32V170h256v224c0 18-14 32-32 32H170zM74 10c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64V74c0-35-29-64-64-64H74z" d="M394 42c18 0 32 14 32 32v64H42V74c0-18 14-32 32-32h320zM42 394V170h96v256H74c-18 0-32-14-32-32zm128 32V170h256v224c0 18-14 32-32 32H170zM74 10c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64V74c0-35-29-64-64-64H74z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" v-else class="v-mid m-a" height="24" width="24" viewBox="-10 -226 468 468"> <svg xmlns="http://www.w3.org/2000/svg" v-else class="v-mid m-a" height="24" width="24" viewBox="-10 -226 468 468">
<path <path
d="M384-184c18 0 32 14 32 32v64H32v-64c0-18 14-32 32-32h320zM32 168V-56h96v256H64c-18 0-32-14-32-32zm128 32V-56h256v224c0 18-14 32-32 32H160zM64-216c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64v-320c0-35-29-64-64-64H64z" d="M384-184c18 0 32 14 32 32v64H32v-64c0-18 14-32 32-32h320zM32 168V-56h96v256H64c-18 0-32-14-32-32zm128 32V-56h256v224c0 18-14 32-32 32H160zM64-216c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64v-320c0-35-29-64-64-64H64z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,5 +1,5 @@
<template> <template>
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 404"><path d="M74 170v64c0 53 43 96 96 96h96v64h64v-64h96c53 0 96-43 96-96v-64c0-53-43-96-96-96h-96V10h-64v64h-96c-53 0-96 43-96 96zm96 0h256v64H170v-64z" fill="#a6acb9"/><path d="M170 10C82 10 10 82 10 170v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96V10h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM202 170h-32v64h256v-64H202z" fill="#1e3050"/></svg> <svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 404"><path d="M74 170v64c0 53 43 96 96 96h96v64h64v-64h96c53 0 96-43 96-96v-64c0-53-43-96-96-96h-96V10h-64v64h-96c-53 0-96 43-96 96zm96 0h256v64H170v-64z" fill="#a6acb9"/><path d="M170 10C82 10 10 82 10 170v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96V10h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM202 170h-32v64h256v-64H202z" fill="currentColor"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -194 596 404"><path d="M160-184C72-184 0-112 0-24v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96v-64h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM192-24h-32v64h256v-64H192z" fill="currentColor"/></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -194 596 404"><path d="M160-184C72-184 0-112 0-24v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96v-64h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM192-24h-32v64h256v-64H192z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -5,12 +5,12 @@
fill="#a6acb9" /> fill="#a6acb9" />
<path <path
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z" d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" v-else class="min-w-[28px]" viewBox="-10 -226 596 468"> <svg xmlns="http://www.w3.org/2000/svg" v-else class="min-w-[28px]" viewBox="-10 -226 596 468">
<path <path
d="M240-216c-88 0-160 72-160 160 0 5 0 10 1 15C33-18 0 31 0 88c0 80 65 144 144 144h304c71 0 128-57 128-128 0-50-28-93-70-114 4-12 6-25 6-38 0-66-54-120-120-120-11 0-23 2-33 5-30-33-72-53-119-53zM128-56c0-62 50-112 112-112 38 0 71 19 91 47 7 10 20 13 30 8 9-4 20-7 31-7 40 0 72 32 72 72 0 14-4 27-11 38-4 7-5 15-2 22s9 13 16 14c35 9 61 41 61 78 0 44-36 80-80 80H144c-53 0-96-43-96-96 0-43 28-79 67-91 11-4 18-16 16-29-2-7-3-16-3-24zm177 7c-9-9-25-9-34 0l-64 64c-9 9-9 25 0 34 10 9 25 9 34 0l23-23v86c0 13 11 24 24 24s24-11 24-24V26l23 23c9 9 25 9 34 0s9-25 0-34l-64-64z" d="M240-216c-88 0-160 72-160 160 0 5 0 10 1 15C33-18 0 31 0 88c0 80 65 144 144 144h304c71 0 128-57 128-128 0-50-28-93-70-114 4-12 6-25 6-38 0-66-54-120-120-120-11 0-23 2-33 5-30-33-72-53-119-53zM128-56c0-62 50-112 112-112 38 0 71 19 91 47 7 10 20 13 30 8 9-4 20-7 31-7 40 0 72 32 72 72 0 14-4 27-11 38-4 7-5 15-2 22s9 13 16 14c35 9 61 41 61 78 0 44-36 80-80 80H144c-53 0-96-43-96-96 0-43 28-79 67-91 11-4 18-16 16-29-2-7-3-16-3-24zm177 7c-9-9-25-9-34 0l-64 64c-9 9-9 25 0 34 10 9 25 9 34 0l23-23v86c0 13 11 24 24 24s24-11 24-24V26l23 23c9 9 25 9 34 0s9-25 0-34l-64-64z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -5,6 +5,6 @@ defineProps<{
</script> </script>
<template> <template>
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="#1e3050"/></svg> <svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="currentColor"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="#1e3050"/></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="currentColor"/></svg>
</template> </template>

View File

@@ -8,7 +8,7 @@
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -194 564 404"> <svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -194 564 404">
<path <path
d="M96-136c-9 0-16 7-16 16v256c0 9 7 16 16 16h256c9 0 16-7 16-16v-256c0-9-7-16-16-16H96zm-64 16c0-35 29-64 64-64h256c35 0 64 29 64 64v256c0 35-29 64-64 64H96c-35 0-64-29-64-64v-256zm506-11c4-3 9-5 14-5 13 0 24 11 24 24v240c0 13-11 24-24 24-5 0-10-2-14-5l-74-55V32l64 48V-64l-64 48v-60l74-55z" d="M96-136c-9 0-16 7-16 16v256c0 9 7 16 16 16h256c9 0 16-7 16-16v-256c0-9-7-16-16-16H96zm-64 16c0-35 29-64 64-64h256c35 0 64 29 64 64v256c0 35-29 64-64 64H96c-35 0-64-29-64-64v-256zm506-11c4-3 9-5 14-5 13 0 24 11 24 24v240c0 13-11 24-24 24-5 0-10-2-14-5l-74-55V32l64 48V-64l-64 48v-60l74-55z"
fill="#1e3050" /> fill="currentColor" />
</svg> </svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M42 170h241c-40 35-65 87-65 144 0 17 2 33 6 48H74c-18 0-32-14-32-32V170z" fill="#a6acb9"/><path d="M458 42H345l-96 96h84c-18 8-35 19-50 32H42v160c0 18 14 32 32 32h150c3 11 7 22 11 32H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3V74c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM43 138l96-96H74c-18 0-32 14-32 32v64h1zm46 0h114l96-96H185l-96 96zm321 288c62 0 112-50 112-112s-50-112-112-112-112 50-112 112 50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144 64-144 144-144zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V258c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="#1e3050"/></svg> <svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M42 170h241c-40 35-65 87-65 144 0 17 2 33 6 48H74c-18 0-32-14-32-32V170z" fill="#a6acb9"/><path d="M458 42H345l-96 96h84c-18 8-35 19-50 32H42v160c0 18 14 32 32 32h150c3 11 7 22 11 32H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3V74c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM43 138l96-96H74c-18 0-32 14-32 32v64h1zm46 0h114l96-96H185l-96 96zm321 288c62 0 112-50 112-112s-50-112-112-112-112 50-112 112 50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144 64-144 144-144zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V258c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="564" height="468" viewBox="22 -194 564 468"><path d="M480-152H367l-96 96h84c-18 8-35 19-50 32H64v160c0 18 14 32 32 32h150c3 11 7 22 11 32H96c-35 0-64-29-64-64v-256c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3v-64c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM65-56l96-96H96c-18 0-32 14-32 32v64h1zm46 0h114l96-96H207l-96 96zm321 288c62 0 112-50 112-112S494 8 432 8 320 58 320 120s50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144S352-24 432-24zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V64c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="#1e3050"/></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" width="564" height="468" viewBox="22 -194 564 468"><path d="M480-152H367l-96 96h84c-18 8-35 19-50 32H64v160c0 18 14 32 32 32h150c3 11 7 22 11 32H96c-35 0-64-29-64-64v-256c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3v-64c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM65-56l96-96H96c-18 0-32 14-32 32v64h1zm46 0h114l96-96H207l-96 96zm321 288c62 0 112-50 112-112S494 8 432 8 320 58 320 120s50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144S352-24 432-24zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V64c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ defineProps<{

View File

@@ -1,13 +1,12 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { setupMiddlewares } from './server/middlewares/setup'; import { setupMiddlewares } from './server/middlewares/setup';
import { registerAuthRoutes } from './server/routes/auth';
import { registerRpcRoutes } from './server/routes/rpc';
import { registerSSRRoutes } from './server/routes/ssr'; import { registerSSRRoutes } from './server/routes/ssr';
import { registerWellKnownRoutes } from './server/routes/wellKnown'; import { registerWellKnownRoutes } from './server/routes/wellKnown';
import { setupServices } from './server/services/grpcClient'; import { setupServices } from './server/services/grpcClient';
import { registerRpcRoutes } from './server/routes/rpc';
import { registerAuthRoutes } from './server/routes/auth';
const app = new Hono(); const app = new Hono();
console.log("bun: ", Bun);
// Global middlewares // Global middlewares
setupMiddlewares(app); setupMiddlewares(app);
setupServices(app); setupServices(app);

View File

@@ -364,7 +364,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" /> <textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">VAST URL</label> <label class="text-sm font-medium text-gray-700">VAST URL</label>
@@ -372,7 +372,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Ad format</label> <label class="text-sm font-medium text-gray-700">Ad format</label>
<select v-model="createForm.adFormat" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="createForm.adFormat" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option> <option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option>
</select> </select>
</div> </div>
@@ -412,7 +412,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" /> <textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">VAST URL</label> <label class="text-sm font-medium text-gray-700">VAST URL</label>
@@ -420,7 +420,7 @@ onMounted(loadTemplates);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Ad format</label> <label class="text-sm font-medium text-gray-700">Ad format</label>
<select v-model="editForm.adFormat" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="editForm.adFormat" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option> <option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option>
</select> </select>
</div> </div>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { client as rpcClient } from "@/api/rpcclient"; import { client as rpcClient } from "@/api/rpcclient";
import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt";
import AppButton from "@/components/app/AppButton.vue"; import AppButton from "@/components/app/AppButton.vue";
import AppDialog from "@/components/app/AppDialog.vue"; import AppDialog from "@/components/app/AppDialog.vue";
import AppInput from "@/components/app/AppInput.vue"; import AppInput from "@/components/app/AppInput.vue";
import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt";
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
@@ -415,7 +415,7 @@ onMounted(loadJobs);
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Command</label> <label class="text-sm font-medium text-gray-700">Command</label>
<textarea v-model="createForm.command" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="ffmpeg -i ..." /> <textarea v-model="createForm.command" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="ffmpeg -i ..." />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Image</label> <label class="text-sm font-medium text-gray-700">Image</label>
@@ -439,7 +439,7 @@ onMounted(loadJobs);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Environment</label> <label class="text-sm font-medium text-gray-700">Environment</label>
<textarea v-model="createForm.envText" rows="5" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="KEY=value per line" /> <textarea v-model="createForm.envText" rows="5" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="KEY=value per line" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -246,7 +246,7 @@ onMounted(loadPayments);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label> <label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label>
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="statusFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option> <option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option>
</select> </select>
</div> </div>
@@ -336,7 +336,7 @@ onMounted(loadPayments);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Payment method</label> <label class="text-sm font-medium text-gray-700">Payment method</label>
<select v-model="createForm.paymentMethod" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="createForm.paymentMethod" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="method in paymentMethodOptions" :key="method" :value="method">{{ method }}</option> <option v-for="method in paymentMethodOptions" :key="method" :value="method">{{ method }}</option>
</select> </select>
</div> </div>
@@ -359,7 +359,7 @@ onMounted(loadPayments);
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div> <div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label> <label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="statusForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="statusForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option> <option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select> </select>
</div> </div>

View File

@@ -306,11 +306,11 @@ onMounted(loadPlans);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" /> <textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Features</label> <label class="text-sm font-medium text-gray-700">Features</label>
<textarea v-model="createForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="One feature per line" /> <textarea v-model="createForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="One feature per line" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Price</label> <label class="text-sm font-medium text-gray-700">Price</label>
@@ -318,7 +318,7 @@ onMounted(loadPlans);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Cycle</label> <label class="text-sm font-medium text-gray-700">Cycle</label>
<select v-model="createForm.cycle" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="createForm.cycle" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option> <option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option>
</select> </select>
</div> </div>
@@ -354,11 +354,11 @@ onMounted(loadPlans);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" /> <textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Features</label> <label class="text-sm font-medium text-gray-700">Features</label>
<textarea v-model="editForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" /> <textarea v-model="editForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Price</label> <label class="text-sm font-medium text-gray-700">Price</label>
@@ -366,7 +366,7 @@ onMounted(loadPlans);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Cycle</label> <label class="text-sm font-medium text-gray-700">Cycle</label>
<select v-model="editForm.cycle" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="editForm.cycle" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option> <option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option>
</select> </select>
</div> </div>

View File

@@ -329,7 +329,7 @@ onMounted(loadUsers);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Role filter</label> <label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Role filter</label>
<select v-model="roleFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="roleFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleFilterOptions" :key="role || 'all'" :value="role">{{ role || 'ALL' }}</option> <option v-for="role in roleFilterOptions" :key="role || 'all'" :value="role">{{ role || 'ALL' }}</option>
</select> </select>
</div> </div>
@@ -423,7 +423,7 @@ onMounted(loadUsers);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label> <label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="createForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="createForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option> <option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select> </select>
</div> </div>
@@ -459,7 +459,7 @@ onMounted(loadUsers);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label> <label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="editForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="editForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option> <option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select> </select>
</div> </div>
@@ -486,7 +486,7 @@ onMounted(loadUsers);
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div> <div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label> <label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="roleForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="roleForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option> <option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select> </select>
</div> </div>

View File

@@ -345,7 +345,7 @@ onMounted(loadVideos);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label> <label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label>
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="statusFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option> <option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option>
</select> </select>
</div> </div>
@@ -428,7 +428,7 @@ onMounted(loadVideos);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label> <label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="createForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="createForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option> <option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select> </select>
</div> </div>
@@ -442,7 +442,7 @@ onMounted(loadVideos);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" /> <textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Format</label> <label class="text-sm font-medium text-gray-700">Format</label>
@@ -480,7 +480,7 @@ onMounted(loadVideos);
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label> <label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="editForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30"> <select v-model="editForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option> <option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select> </select>
</div> </div>
@@ -494,7 +494,7 @@ onMounted(loadVideos);
</div> </div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label> <label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" /> <textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Format</label> <label class="text-sm font-medium text-gray-700">Format</label>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
const { t } = useTranslation(); const { t } = useTranslation();
@@ -237,7 +237,7 @@ const isScalePack = (tag: string) => tag === scaleTag.value;
/> />
<path <path
d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z" d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z"
fill="#1e3050" fill="currentColor"
/> />
</svg> </svg>
</div> </div>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import Chart from '@/components/icons/Chart.vue'; import Chart from '@/components/icons/Chart.vue';
import Credit from '@/components/icons/Credit.vue'; import Credit from '@/components/icons/Credit.vue';
import Upload from '@/components/icons/Upload.vue'; import Upload from '@/components/icons/Upload.vue';
import Video from '@/components/icons/Video.vue'; import Video from '@/components/icons/Video.vue';
import { useUIState } from '@/stores/uiState'; import { useUIState } from '@/stores/uiState';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Referral from './Referral.vue'; import Referral from './Referral.vue';
@@ -71,12 +71,12 @@ const quickActions = computed(() => [
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[ <button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[
'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-surface', 'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-header',
'border border-gray-300 hover:border-primary hover:shadow-lg', 'border border-gray-300 hover:border-primary hover:shadow-lg',
'group press-animated', 'group press-animated',
]"> ]">
<div <div
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted group-hover:bg-primary/10"> class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted-dark group-hover:bg-primary/10">
<component filled :is="action.icon" class="w-6 h-6" /> <component filled :is="action.icon" class="w-6 h-6" />
</div> </div>
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3> <h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-surface"> <div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-header">
<div class="flex flex-col space-y-1.5 p-6"> <div class="flex flex-col space-y-1.5 p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ t('overview.referral.title') }}</h3> <h3 class="text-lg font-semibold leading-none tracking-tight">{{ t('overview.referral.title') }}</h3>
</div> </div>
@@ -27,8 +27,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { computed, ref } from 'vue';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
import { computed, ref } from 'vue';
const auth = useAuthStore(); const auth = useAuthStore();
const isCopied = ref(false); const isCopied = ref(false);

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import StatsCard from '@/components/dashboard/StatsCard.vue'; import StatsCard from '@/components/dashboard/StatsCard.vue';
import { formatBytes } from '@/lib/utils'; import { formatBytes } from '@/lib/utils';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
interface Props { interface Props {
loading: boolean; loading: boolean;
@@ -21,7 +21,7 @@ const localeTag = computed(() => i18next.resolvedLanguage === 'vi' ? 'vi-VN' : '
<template> <template>
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> <div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div v-for="i in 3" :key="i" class="bg-surface rounded-xl border border-gray-200 p-6"> <div v-for="i in 3" :key="i" class="bg-header rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div class="space-y-2"> <div class="space-y-2">
<div class="w-20 h-4 bg-gray-200 rounded animate-pulse mb-2" /> <div class="w-20 h-4 bg-gray-200 rounded animate-pulse mb-2" />

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { client as rpcClient } from '@/api/rpcclient'; import { client as rpcClient } from '@/api/rpcclient';
import type { PaymentHistoryItem as PaymentHistoryApiItem, Plan as ModelPlan } from '@/server/gen/proto/app/v1/common';
import AppButton from '@/components/app/AppButton.vue'; import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue'; import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue'; import AppInput from '@/components/app/AppInput.vue';
@@ -12,6 +11,7 @@ import BillingPlansSection from '@/routes/settings/components/billing/BillingPla
import BillingTopupDialog from '@/routes/settings/components/billing/BillingTopupDialog.vue'; import BillingTopupDialog from '@/routes/settings/components/billing/BillingTopupDialog.vue';
import BillingUsageSection from '@/routes/settings/components/billing/BillingUsageSection.vue'; import BillingUsageSection from '@/routes/settings/components/billing/BillingUsageSection.vue';
import BillingWalletRow from '@/routes/settings/components/billing/BillingWalletRow.vue'; import BillingWalletRow from '@/routes/settings/components/billing/BillingWalletRow.vue';
import type { Plan as ModelPlan, PaymentHistoryItem as PaymentHistoryApiItem } from '@/server/gen/proto/app/v1/common';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useQuery } from '@pinia/colada'; import { useQuery } from '@pinia/colada';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
@@ -634,7 +634,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border px-4 py-3 text-left transition-all', 'rounded-lg border px-4 py-3 text-left transition-all',
selectedTermMonths === months selectedTermMonths === months
? 'border-primary bg-primary/5 text-primary' ? 'border-primary bg-primary/5 text-primary'
: 'border-border bg-surface text-foreground hover:border-primary/30 hover:bg-muted/30', : 'border-border bg-header text-foreground hover:border-primary/30 hover:bg-muted/30',
]" ]"
@click="selectedTermMonths = months" @click="selectedTermMonths = months"
> >
@@ -645,11 +645,11 @@ const selectPreset = (amount: number) => {
</div> </div>
<div class="grid gap-3 md:grid-cols-3"> <div class="grid gap-3 md:grid-cols-3">
<div class="rounded-lg border border-border bg-surface p-4"> <div class="rounded-lg border border-border bg-header p-4">
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.totalLabel') }}</p> <p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.totalLabel') }}</p>
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(selectedTotalAmount) }}</p> <p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(selectedTotalAmount) }}</p>
</div> </div>
<div class="rounded-lg border border-border bg-surface p-4"> <div class="rounded-lg border border-border bg-header p-4">
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.walletBalanceLabel') }}</p> <p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.walletBalanceLabel') }}</p>
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(walletBalance) }}</p> <p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(walletBalance) }}</p>
</div> </div>
@@ -679,7 +679,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border p-4 text-left transition-all', 'rounded-lg border p-4 text-left transition-all',
selectedPaymentMethod === 'wallet' selectedPaymentMethod === 'wallet'
? 'border-primary bg-primary/5' ? 'border-primary bg-primary/5'
: 'border-border bg-surface hover:border-primary/30 hover:bg-muted/30', : 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
]" ]"
@click="selectUpgradePaymentMethod('wallet')" @click="selectUpgradePaymentMethod('wallet')"
> >
@@ -695,7 +695,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border p-4 text-left transition-all', 'rounded-lg border p-4 text-left transition-all',
selectedPaymentMethod === 'topup' selectedPaymentMethod === 'topup'
? 'border-primary bg-primary/5' ? 'border-primary bg-primary/5'
: 'border-border bg-surface hover:border-primary/30 hover:bg-muted/30', : 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
]" ]"
@click="selectUpgradePaymentMethod('topup')" @click="selectUpgradePaymentMethod('topup')"
> >

View File

@@ -13,8 +13,8 @@ import SettingsNotice from '@/routes/settings/components/SettingsNotice.vue';
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue'; import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue'; import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
import { useQuery } from '@pinia/colada'; import { useQuery } from '@pinia/colada';
import { computed, ref, watch } from 'vue';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
import { computed, ref, watch } from 'vue';
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
@@ -304,7 +304,7 @@ const copyIframeCode = async () => {
<p class="text-xs text-foreground/60 mb-2"> <p class="text-xs text-foreground/60 mb-2">
{{ t('settings.domainsDns.embedCodeHint') }} {{ t('settings.domainsDns.embedCodeHint') }}
</p> </p>
<pre class="bg-surface border border-border rounded-md p-3 text-xs text-foreground/70 overflow-x-auto"><code>{{ iframeCode }}</code></pre> <pre class="bg-header border border-border rounded-md p-3 text-xs text-foreground/70 overflow-x-auto"><code>{{ iframeCode }}</code></pre>
</div> </div>
<AppDialog <AppDialog

View File

@@ -2,7 +2,6 @@
import AppButton from '@/components/app/AppButton.vue'; import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue'; import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue'; import AppInput from '@/components/app/AppInput.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue'; import CheckIcon from '@/components/icons/CheckIcon.vue';
import LockIcon from '@/components/icons/LockIcon.vue'; import LockIcon from '@/components/icons/LockIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue'; import TelegramIcon from '@/components/icons/TelegramIcon.vue';
@@ -122,41 +121,6 @@ const saveLanguage = async () => {
} }
}; };
const handleToggle2FA = async () => {
if (!twoFactorEnabled.value) {
try {
await new Promise(resolve => setTimeout(resolve, 500));
twoFactorDialogVisible.value = true;
} catch (e) {
toast.add({
severity: 'error',
summary: t('settings.securityConnected.toast.twoFactorEnableFailedSummary'),
detail: t('settings.securityConnected.toast.twoFactorEnableFailedDetail'),
life: 5000
});
twoFactorEnabled.value = false;
}
} else {
try {
await new Promise(resolve => setTimeout(resolve, 500));
toast.add({
severity: 'success',
summary: t('settings.securityConnected.toast.twoFactorDisabledSummary'),
detail: t('settings.securityConnected.toast.twoFactorDisabledDetail'),
life: 3000
});
} catch (e) {
toast.add({
severity: 'error',
summary: t('settings.securityConnected.toast.twoFactorDisableFailedSummary'),
detail: t('settings.securityConnected.toast.twoFactorDisableFailedDetail'),
life: 5000
});
twoFactorEnabled.value = true;
}
}
};
const confirmTwoFactor = async () => { const confirmTwoFactor = async () => {
try { try {
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
@@ -228,10 +192,9 @@ const disconnectTelegram = async () => {
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.accountStatus.label')" :title="t('settings.securityConnected.accountStatus.label')"
:description="t('settings.securityConnected.accountStatus.detail')" :description="t('settings.securityConnected.accountStatus.detail')"
iconBoxClass="bg-success/10"
> >
<template #icon> <template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/> <polyline points="22 4 12 14.01 9 11.01"/>
</svg> </svg>
@@ -245,11 +208,10 @@ const disconnectTelegram = async () => {
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.language.label')" :title="t('settings.securityConnected.language.label')"
:description="t('settings.securityConnected.language.detail')" :description="t('settings.securityConnected.language.detail')"
iconBoxClass="bg-info/10"
actionsClass="flex items-center gap-2" actionsClass="flex items-center gap-2"
> >
<template #icon> <template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M2 12h20" /> <path d="M2 12h20" />
<path d="M12 2a15 15 0 0 1 0 20" /> <path d="M12 2a15 15 0 0 1 0 20" />
@@ -261,7 +223,7 @@ const disconnectTelegram = async () => {
<select <select
v-model="selectedLanguage" v-model="selectedLanguage"
:disabled="languageSaving" :disabled="languageSaving"
class="rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground disabled:opacity-60" class="rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground disabled:opacity-60"
> >
<option <option
v-for="option in languageOptions" v-for="option in languageOptions"
@@ -273,6 +235,7 @@ const disconnectTelegram = async () => {
</select> </select>
<AppButton <AppButton
size="sm" size="sm"
variant="secondary"
:loading="languageSaving" :loading="languageSaving"
:disabled="languageSaving" :disabled="languageSaving"
@click="saveLanguage" @click="saveLanguage"
@@ -282,33 +245,18 @@ const disconnectTelegram = async () => {
</template> </template>
</SettingsRow> </SettingsRow>
<SettingsRow
:title="t('settings.securityConnected.twoFactor.label')"
:description="twoFactorEnabled ? t('settings.securityConnected.twoFactor.enabled') : t('settings.securityConnected.twoFactor.disabled')"
iconBoxClass="bg-primary/10"
>
<template #icon>
<LockIcon class="w-5 h-5 text-primary" />
</template>
<template #actions>
<AppSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
</template>
</SettingsRow>
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.changePassword.label')" :title="t('settings.securityConnected.changePassword.label')"
:description="t('settings.securityConnected.changePassword.detail')" :description="t('settings.securityConnected.changePassword.detail')"
iconBoxClass="bg-primary/10"
> >
<template #icon> <template #icon>
<svg aria-hidden="true" class="fill-primary" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true"> <svg aria-hidden="true" class="fill-primary w-6 h-6" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true">
<path d="M22 9.75v5.5A1.75 1.75 0 0 1 20.25 17H3.75A1.75 1.75 0 0 1 2 15.25v-5.5C2 8.784 2.784 8 3.75 8h16.5c.966 0 1.75.784 1.75 1.75Zm-8.75 2.75a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Zm-6.5 1.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm10.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"></path> <path d="M22 9.75v5.5A1.75 1.75 0 0 1 20.25 17H3.75A1.75 1.75 0 0 1 2 15.25v-5.5C2 8.784 2.784 8 3.75 8h16.5c.966 0 1.75.784 1.75 1.75Zm-8.75 2.75a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Zm-6.5 1.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm10.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"></path>
</svg> </svg>
</template> </template>
<template #actions> <template #actions>
<AppButton size="sm" @click="openChangePassword"> <AppButton variant="secondary" size="sm" @click="openChangePassword">
{{ t('settings.securityConnected.changePassword.button') }} {{ t('settings.securityConnected.changePassword.button') }}
</AppButton> </AppButton>
</template> </template>
@@ -317,10 +265,9 @@ const disconnectTelegram = async () => {
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.email.label')" :title="t('settings.securityConnected.email.label')"
:description="emailConnected ? t('settings.securityConnected.email.connected') : t('settings.securityConnected.email.disconnected')" :description="emailConnected ? t('settings.securityConnected.email.connected') : t('settings.securityConnected.email.disconnected')"
iconBoxClass="bg-info/10"
> >
<template #icon> <template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="text-info w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/> <rect width="20" height="16" x="2" y="4" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg> </svg>
@@ -336,10 +283,9 @@ const disconnectTelegram = async () => {
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.telegram.label')" :title="t('settings.securityConnected.telegram.label')"
:description="telegramConnected ? (telegramUsername || t('settings.securityConnected.telegram.connectedFallback')) : t('settings.securityConnected.telegram.detailDisconnected')" :description="telegramConnected ? (telegramUsername || t('settings.securityConnected.telegram.connectedFallback')) : t('settings.securityConnected.telegram.detailDisconnected')"
iconBoxClass="bg-[#0088cc]/10"
> >
<template #icon> <template #icon>
<TelegramIcon class="w-5 h-5 text-[#0088cc]" /> <TelegramIcon class="w-6 h-6 text-[#0088cc]" />
</template> </template>
<template #actions> <template #actions>
@@ -354,6 +300,7 @@ const disconnectTelegram = async () => {
<AppButton <AppButton
v-else v-else
size="sm" size="sm"
variant="secondary"
@click="connectTelegram" @click="connectTelegram"
> >
{{ t('settings.securityConnected.telegram.connect') }} {{ t('settings.securityConnected.telegram.connect') }}
@@ -364,11 +311,10 @@ const disconnectTelegram = async () => {
<SettingsRow <SettingsRow
:title="t('settings.securityConnected.logout.label')" :title="t('settings.securityConnected.logout.label')"
:description="t('settings.securityConnected.logout.detail')" :description="t('settings.securityConnected.logout.detail')"
iconBoxClass="bg-danger/10"
hoverClass="hover:bg-danger/5" hoverClass="hover:bg-danger/5"
> >
<template #icon> <template #icon>
<XCircleIcon class="w-5 h-5 text-danger" /> <XCircleIcon class="w-6 h-6 text-danger" />
</template> </template>
<template #actions> <template #actions>

View File

@@ -34,7 +34,7 @@
? 'bg-primary/10 text-primary font-semibold' ? 'bg-primary/10 text-primary font-semibold'
: item.danger : item.danger
? 'text-danger hover:bg-danger/10' ? 'text-danger hover:bg-danger/10'
: 'text-foreground/70 hover:bg-muted hover:text-foreground' : 'text-foreground/70 hover:bg-header hover:text-foreground'
]" ]"
> >
<component :is="item.icon" class="w-5 h-5 shrink-0" :filled="currentTab === item.value" /> <component :is="item.icon" class="w-5 h-5 shrink-0" :filled="currentTab === item.value" />
@@ -62,21 +62,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import { useRoute } from 'vue-router';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import AppConfirmHost from '@/components/app/AppConfirmHost.vue'; import AppConfirmHost from '@/components/app/AppConfirmHost.vue';
import AppToastHost from '@/components/app/AppToastHost.vue'; import AppToastHost from '@/components/app/AppToastHost.vue';
import ClientOnly from '@/components/ClientOnly'; import ClientOnly from '@/components/ClientOnly';
import UserIcon from '@/components/icons/UserIcon.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue';
import GlobeIcon from '@/components/icons/Globe.vue';
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
import { useAuthStore } from '@/stores/auth';
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
import Bell from '@/components/icons/Bell.vue';
import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue'; import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue';
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
import Bell from '@/components/icons/Bell.vue';
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
import GlobeIcon from '@/components/icons/Globe.vue';
import UserIcon from '@/components/icons/UserIcon.vue';
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue'; import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
import { useAuthStore } from '@/stores/auth';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const auth = useAuthStore(); const auth = useAuthStore();
@@ -94,7 +94,7 @@ const tabPaths: Record<string, string> = {
}; };
// Menu items grouped by category (GitHub-style) // Menu items grouped by category (GitHub-style)
const menuSections = computed(() => [ const menuSections = computed<{ title: string; items: { value: string; label: string; icon: any, danger?: boolean }[] }[]>(() => [
{ {
title: t('settings.menu.securityGroup'), title: t('settings.menu.securityGroup'),
items: [ items: [

View File

@@ -15,8 +15,8 @@ const props = withDefaults(defineProps<{
rowClass?: string; rowClass?: string;
}>(), { }>(), {
description: '', description: '',
iconBoxClass: '', iconBoxClass: 'bg-muted text-foreground/70',
hoverClass: 'hover:bg-muted/30', hoverClass: 'hover:bg-header',
titleClass: 'text-sm font-medium text-foreground', titleClass: 'text-sm font-medium text-foreground',
descriptionClass: 'text-xs text-foreground/60 mt-0.5', descriptionClass: 'text-xs text-foreground/60 mt-0.5',
actionsClass: '', actionsClass: '',
@@ -43,7 +43,7 @@ const actionsWrapperClass = computed(() => cn('shrink-0', props.actionsClass));
<div v-bind="attrs" :class="rootClass"> <div v-bind="attrs" :class="rootClass">
<div class="flex min-w-0 items-center gap-4"> <div class="flex min-w-0 items-center gap-4">
<div :class="iconClass"> <div :class="iconClass">
<slot name="icon" /> <slot name="icon" class="h-6 w-6" />
</div> </div>
<div class="min-w-0"> <div class="min-w-0">

View File

@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<{
const attrs = useAttrs(); const attrs = useAttrs();
const rootClass = computed(() => cn( const rootClass = computed(() => cn(
'bg-surface border border-border rounded-lg', 'bg-white border border-border rounded-lg',
)); ));
</script> </script>
@@ -32,7 +32,7 @@ const rootClass = computed(() => cn(
<div <div
v-if="title || description || $slots['header-actions']" v-if="title || description || $slots['header-actions']"
:class="cn( :class="cn(
'px-6 py-4 border-b border-border', 'px-6 py-4 border-b border-border bg-header rounded-tl-lg rounded-tr-lg',
$slots['header-actions'] ? 'flex items-center justify-between gap-4' : '', $slots['header-actions'] ? 'flex items-center justify-between gap-4' : '',
headerClass, headerClass,
)" )"

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import UploadQueueItem from './UploadQueueItem.vue';
import type { QueueItem } from '@/composables/useUploadQueue'; import type { QueueItem } from '@/composables/useUploadQueue';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
@@ -24,7 +23,7 @@ const { t } = useTranslation();
<aside <aside
class=":uno: w-full flex flex-col h-[calc(100svh-64px)] sticky top-16 before:(content-[''] absolute pointer-events-none inset-[-1px] rounded-[calc(var(--radius-2xl)+1px)] bg-[linear-gradient(-45deg,var(--capra-ramp-5)_0,var(--capra-ramp-4)_8%,var(--capra-ramp-3)_17%,var(--capra-ramp-2)_25%,var(--capra-ramp-1)_33%,#292929_34%,#292929_40%,#e1dfdf_45%,#e1dfdf_100%)] bg-[length:400%_200%] bg-[position:0_0] transition-[background-position] duration-[1000ms] ease-in-out delay-[500ms] z-0)" class=":uno: w-full flex flex-col h-[calc(100svh-64px)] sticky top-16 before:(content-[''] absolute pointer-events-none inset-[-1px] rounded-[calc(var(--radius-2xl)+1px)] bg-[linear-gradient(-45deg,var(--capra-ramp-5)_0,var(--capra-ramp-4)_8%,var(--capra-ramp-3)_17%,var(--capra-ramp-2)_25%,var(--capra-ramp-1)_33%,#292929_34%,#292929_40%,#e1dfdf_45%,#e1dfdf_100%)] bg-[length:400%_200%] bg-[position:0_0] transition-[background-position] duration-[1000ms] ease-in-out delay-[500ms] z-0)"
:class="{ 'before:bg-[position:100%_100%]': pendingCount && pendingCount > 0 }"> :class="{ 'before:bg-[position:100%_100%]': pendingCount && pendingCount > 0 }">
<div class="bg-surface z-1 relative flex flex-col h-full rounded-2xl overflow-hidden"> <div class="bg-header z-1 relative flex flex-col h-full rounded-2xl overflow-hidden">
<div class="flex-1 overflow-y-auto scrollbar-thin p-6 space-y-5 relative" id="queue-list"> <div class="flex-1 overflow-y-auto scrollbar-thin p-6 space-y-5 relative" id="queue-list">
<div v-if="!items?.length" id="empty-queue" <div v-if="!items?.length" id="empty-queue"

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
import LinkIcon from '@/components/icons/LinkIcon.vue'; import LinkIcon from '@/components/icons/LinkIcon.vue';
import PencilIcon from '@/components/icons/PencilIcon.vue'; import PencilIcon from '@/components/icons/PencilIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue'; import TrashIcon from '@/components/icons/TrashIcon.vue';
import VideoIcon from '@/components/icons/VideoIcon.vue'; import VideoIcon from '@/components/icons/VideoIcon.vue';
import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils'; import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils';
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
import { useTranslation } from 'i18next-vue'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
@@ -75,7 +75,7 @@ const isSelected = (video: ModelVideo) =>
</div> </div>
<table v-else class="w-full min-w-[50rem]"> <table v-else class="w-full min-w-[50rem]">
<thead> <thead>
<tr class="border-b border-gray-200 bg-gray-50"> <tr class="border-b border-gray-200 bg-header">
<th class="w-12 px-4 py-3"> <th class="w-12 px-4 py-3">
<input type="checkbox" :checked="isAllSelected" @change="toggleAll" <input type="checkbox" :checked="isAllSelected" @change="toggleAll"
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" /> class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" />

View File

@@ -1,25 +1,25 @@
import { MiddlewareHandler } from "hono"; import { MiddlewareHandler } from "hono";
import { getCookie } from "hono/cookie"; import { getCookie, setCookie } from "hono/cookie";
import { HTTPException } from "hono/http-exception"; import { HTTPException } from "hono/http-exception";
import { getAccountServiceClient } from "../services/grpcClient"; import { buildInternalMetadata, getAccountServiceClient } from "../services/grpcClient";
import { generateAndSetTokens } from "../utils"; import { generateAndSetTokens } from "../utils";
export const authenticate: MiddlewareHandler = async (ctx, next) => { export const authenticate: MiddlewareHandler = async (ctx, next) => {
let payload let payload
let cause let cause
const jwtProvider = ctx.get("jwtProvider"); const jwtProvider = ctx.get("jwtProvider");
const token = getCookie(ctx, "access_token"); const token = getCookie(ctx, "access_token");
if (!token) {
throw new HTTPException(401, { if (token) {
message: 'Unauthorized',
})
}
try { try {
payload = await jwtProvider.parseToken(token); payload = await jwtProvider.parseToken(token);
} catch (e) { } catch (e) {
cause = e cause = e
} }
}
if (!payload) { if (!payload) {
const refreshToken = getCookie(ctx, "refresh_token"); const refreshToken = getCookie(ctx, "refresh_token");
if (!refreshToken) { if (!refreshToken) {
throw new HTTPException(401, { throw new HTTPException(401, {
message: 'Unauthorized', message: 'Unauthorized',
@@ -35,10 +35,10 @@ export const authenticate: MiddlewareHandler = async (ctx, next) => {
if (!userId) { if (!userId) {
throw new HTTPException(401) throw new HTTPException(401)
} }
const userData = await getAccountServiceClient().getMe({}); const metadata = buildInternalMetadata();
const user = userData.user; const user = await getAccountServiceClient().getUserById(userId, metadata);
redis.del("refresh_uuid:" + refreshUuid);
const tokenPair = await generateAndSetTokens(ctx, userData.user!); const tokenPair = await generateAndSetTokens(ctx, user);
if (!user?.id || !user?.role || user.id !== userId) { if (!user?.id || !user?.role || user.id !== userId) {
throw new HTTPException(401) throw new HTTPException(401)
} }
@@ -49,7 +49,6 @@ export const authenticate: MiddlewareHandler = async (ctx, next) => {
token_id: tokenPair.accessUUID, token_id: tokenPair.accessUUID,
} }
} }
if (!payload.user_id || !payload.role) { if (!payload.user_id || !payload.role) {
throw new HTTPException(401, { throw new HTTPException(401, {
message: 'Unauthorized', message: 'Unauthorized',
@@ -58,5 +57,9 @@ export const authenticate: MiddlewareHandler = async (ctx, next) => {
ctx.set('jwtPayload', payload) ctx.set('jwtPayload', payload)
ctx.set('userId', payload.user_id) ctx.set('userId', payload.user_id)
ctx.set("role", payload.role) ctx.set("role", payload.role)
setCookie(ctx, "timestamp", Date.now().toString(), {
httpOnly: false,
secure: false,
});
await next(); await next();
}; };

View File

@@ -1,10 +1,10 @@
import { RedisClient } from "bun";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { contextStorage } from "hono/context-storage"; import { contextStorage } from "hono/context-storage";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { languageDetector } from "hono/language"; import { languageDetector } from "hono/language";
import isMobile from "is-mobile"; import isMobile from "is-mobile";
import { JwtProvider } from "../utils/token"; import { JwtProvider } from "../utils/token";
import { RedisClient } from "bun";
type AppFetch = ( type AppFetch = (
input: string | Request | URL, input: string | Request | URL,
requestInit?: RequestInit requestInit?: RequestInit
@@ -42,8 +42,6 @@ const getRedisUrl = () => {
}; };
const getRedisClient = async (): Promise<RedisClient> => { const getRedisClient = async (): Promise<RedisClient> => {
console.log("bun", typeof Bun)
if (!redisClientPromise) { if (!redisClientPromise) {
const client = new RedisClient(getRedisUrl()) const client = new RedisClient(getRedisUrl())
await client.connect(); await client.connect();

View File

@@ -79,7 +79,7 @@ const buildForwardMetadataFromHeaders = (headers: Headers): Metadata => {
return metadata; return metadata;
}; };
const buildInternalMetadata = () => { export const buildInternalMetadata = () => {
const context = tryGetContext(); const context = tryGetContext();
const metadata = context ? buildForwardMetadataFromHeaders(context.req.raw.headers) : new Metadata(); const metadata = context ? buildForwardMetadataFromHeaders(context.req.raw.headers) : new Metadata();
const marker = process.env.STREAM_INTERNAL_AUTH_MARKER; const marker = process.env.STREAM_INTERNAL_AUTH_MARKER;

View File

@@ -179,9 +179,10 @@ export default defineConfig({
light: "#f8f9fa", light: "#f8f9fa",
}, },
border: { border: {
DEFAULT: "#e6e7e2", DEFAULT: "#d0d7de",
light: "#f8f9fa", light: "#f8f9fa",
}, },
header: '#f6f8fa',
// bg: { // bg: {
// page: "#faf8f8", // nền toàn trang // page: "#faf8f8", // nền toàn trang
// surface: "#ffffff", // card, modal, table // surface: "#ffffff", // card, modal, table