mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 09:43:49 +02:00
feat: add dashboard
This commit is contained in:
31
src/views/Dashboard/components/DashboardEditToolbar.vue
Normal file
31
src/views/Dashboard/components/DashboardEditToolbar.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2 rounded-md border bg-card px-3 py-2">
|
||||
<span class="text-sm font-medium text-muted-foreground">{{ t('dashboard.toolbar.editing') }}</span>
|
||||
<Input
|
||||
:model-value="name"
|
||||
:placeholder="t('dashboard.name_placeholder')"
|
||||
class="mx-2 h-7 max-w-[200px] text-sm"
|
||||
@update:model-value="emit('update:name', $event)" />
|
||||
<div class="flex gap-2">
|
||||
<Button variant="secondary" size="sm" @click="emit('cancel')">{{ t('dashboard.actions.cancel') }}</Button>
|
||||
<Button variant="destructive" size="sm" @click="emit('delete')">{{ t('dashboard.actions.delete') }}</Button>
|
||||
</div>
|
||||
<Button class="ml-auto" size="sm" @click="emit('save')">{{ t('dashboard.actions.save') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['save', 'cancel', 'delete', 'update:name']);
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
89
src/views/Dashboard/components/DashboardPanel.vue
Normal file
89
src/views/Dashboard/components/DashboardPanel.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="relative flex min-h-0 flex-1 overflow-hidden rounded-md border bg-card">
|
||||
<template v-if="isEditing">
|
||||
<div class="flex w-full min-h-0 flex-col gap-2 p-3">
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<i v-if="panelIcon" :class="panelIcon" class="text-base" />
|
||||
<span>{{ panelLabel || t('dashboard.panel.not_selected') }}</span>
|
||||
</div>
|
||||
<Button variant="outline" class="w-full" @click="openSelector">
|
||||
{{ panelKey ? t('dashboard.panel.replace') : t('dashboard.panel.select') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="panelKey && panelComponent">
|
||||
<div class="h-full w-full overflow-y-auto">
|
||||
<component :is="panelComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="flex w-full items-center justify-center text-sm text-muted-foreground">
|
||||
{{ t('dashboard.panel.not_configured') }}
|
||||
</div>
|
||||
|
||||
<PanelSelector
|
||||
:open="selectorOpen"
|
||||
:current-key="panelKey"
|
||||
@select="handleSelect"
|
||||
@close="selectorOpen = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { navDefinitions } from '@/shared/constants/ui';
|
||||
|
||||
import PanelSelector from './PanelSelector.vue';
|
||||
import { panelComponentMap } from './panelRegistry';
|
||||
|
||||
const props = defineProps({
|
||||
panelKey: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
const { t } = useI18n();
|
||||
const selectorOpen = ref(false);
|
||||
|
||||
const panelComponent = computed(() => {
|
||||
if (!props.panelKey) {
|
||||
return null;
|
||||
}
|
||||
return panelComponentMap[props.panelKey] || null;
|
||||
});
|
||||
|
||||
const panelOption = computed(() => {
|
||||
if (!props.panelKey) {
|
||||
return null;
|
||||
}
|
||||
return navDefinitions.find((def) => def.key === props.panelKey) || null;
|
||||
});
|
||||
|
||||
const panelLabel = computed(() => {
|
||||
if (!panelOption.value?.labelKey) {
|
||||
return props.panelKey || '';
|
||||
}
|
||||
return t(panelOption.value.labelKey);
|
||||
});
|
||||
|
||||
const panelIcon = computed(() => panelOption.value?.icon || '');
|
||||
|
||||
const openSelector = () => {
|
||||
selectorOpen.value = true;
|
||||
};
|
||||
|
||||
const handleSelect = (value) => {
|
||||
emit('select', value);
|
||||
selectorOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
69
src/views/Dashboard/components/DashboardRow.vue
Normal file
69
src/views/Dashboard/components/DashboardRow.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="relative h-full min-h-[180px]">
|
||||
<div v-if="isEditing" class="flex h-full gap-2">
|
||||
<DashboardPanel
|
||||
v-for="(panelKey, panelIndex) in row.panels"
|
||||
:key="panelIndex"
|
||||
:panel-key="panelKey"
|
||||
:is-editing="true"
|
||||
:class="row.panels.length === 1 ? 'w-full' : 'w-1/2'"
|
||||
@select="(key) => emit('update-panel', rowIndex, panelIndex, key)" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
class="absolute -right-1 top-2 z-20 bg-background/80"
|
||||
@click="emit('remove-row', rowIndex)">
|
||||
<X class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ResizablePanelGroup
|
||||
v-else-if="row.panels.length === 2"
|
||||
direction="horizontal"
|
||||
:auto-save-id="`dashboard-${dashboardId}-row-${rowIndex}`"
|
||||
class="h-full min-h-[180px]">
|
||||
<ResizablePanel :default-size="50" :min-size="20">
|
||||
<DashboardPanel :panel-key="row.panels[0]" class="h-full" />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel :default-size="50" :min-size="20">
|
||||
<DashboardPanel :panel-key="row.panels[1]" class="h-full" />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
<div v-else class="h-full">
|
||||
<DashboardPanel :panel-key="row.panels[0]" class="h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { X } from 'lucide-vue-next';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
|
||||
|
||||
import DashboardPanel from './DashboardPanel.vue';
|
||||
|
||||
defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
rowIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
dashboardId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update-panel', 'remove-row']);
|
||||
</script>
|
||||
52
src/views/Dashboard/components/PanelSelector.vue
Normal file
52
src/views/Dashboard/components/PanelSelector.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="(value) => !value && emit('close')">
|
||||
<DialogContent class="sm:max-w-110">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('dashboard.selector.title') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 max-h-[50vh] overflow-y-auto">
|
||||
<button
|
||||
v-for="option in panelOptions"
|
||||
:key="option.key"
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-md border p-2 text-left text-sm hover:bg-accent"
|
||||
:class="option.key === currentKey ? 'border-primary bg-primary/5 ring-1 ring-primary/40' : ''"
|
||||
@click="emit('select', option.key)">
|
||||
<i :class="option.icon" class="text-base" />
|
||||
<span>{{ t(option.labelKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" @click="emit('select', null)">{{ t('dashboard.selector.clear') }}</Button>
|
||||
<Button variant="secondary" @click="emit('close')">{{ t('dashboard.actions.cancel') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { navDefinitions } from '@/shared/constants/ui';
|
||||
|
||||
defineProps({
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentKey: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'close']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const panelOptions = computed(() => navDefinitions.filter((def) => def.routeName));
|
||||
</script>
|
||||
33
src/views/Dashboard/components/panelRegistry.js
Normal file
33
src/views/Dashboard/components/panelRegistry.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import Feed from '../../Feed/Feed.vue';
|
||||
import FavoritesAvatar from '../../Favorites/FavoritesAvatar.vue';
|
||||
import FavoritesFriend from '../../Favorites/FavoritesFriend.vue';
|
||||
import FavoritesWorld from '../../Favorites/FavoritesWorld.vue';
|
||||
import FriendList from '../../FriendList/FriendList.vue';
|
||||
import FriendLog from '../../FriendLog/FriendLog.vue';
|
||||
import FriendsLocations from '../../FriendsLocations/FriendsLocations.vue';
|
||||
import GameLog from '../../GameLog/GameLog.vue';
|
||||
import Moderation from '../../Moderation/Moderation.vue';
|
||||
import MyAvatars from '../../MyAvatars/MyAvatars.vue';
|
||||
import Notification from '../../Notifications/Notification.vue';
|
||||
import PlayerList from '../../PlayerList/PlayerList.vue';
|
||||
import Search from '../../Search/Search.vue';
|
||||
import Tools from '../../Tools/Tools.vue';
|
||||
|
||||
export const panelComponentMap = {
|
||||
feed: Feed,
|
||||
'friends-locations': FriendsLocations,
|
||||
'game-log': GameLog,
|
||||
'player-list': PlayerList,
|
||||
search: Search,
|
||||
'favorite-friends': FavoritesFriend,
|
||||
'favorite-worlds': FavoritesWorld,
|
||||
'favorite-avatars': FavoritesAvatar,
|
||||
'friend-log': FriendLog,
|
||||
'friend-list': FriendList,
|
||||
moderation: Moderation,
|
||||
notification: Notification,
|
||||
'my-avatars': MyAvatars,
|
||||
'charts-instance': () => import('../../Charts/components/InstanceActivity.vue'),
|
||||
'charts-mutual': () => import('../../Charts/components/MutualFriends.vue'),
|
||||
tools: Tools
|
||||
};
|
||||
Reference in New Issue
Block a user