feat(magic): add DeviceDetailSheet component
This commit is contained in:
parent
0258c818f3
commit
b5e89b5973
324
apps/rebreak-magic/app/components/DeviceDetailSheet.vue
Normal file
324
apps/rebreak-magic/app/components/DeviceDetailSheet.vue
Normal file
@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<USlideover
|
||||
v-if="device"
|
||||
v-model:open="open"
|
||||
side="right"
|
||||
:ui="{ content: 'w-full max-w-md bg-white dark:bg-gray-900' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start gap-4 w-full">
|
||||
<div
|
||||
class="shrink-0 w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-700 flex items-center justify-center"
|
||||
>
|
||||
<UIcon
|
||||
:name="deviceIcon"
|
||||
class="w-7 h-7 text-[var(--rebreak-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-lg font-bold text-gray-900 dark:text-white truncate">
|
||||
{{ deviceName }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ platformLabel }}
|
||||
<span v-if="device.osVersion">· {{ device.osVersion }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
:aria-label="'Schließen'"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<!-- Status -->
|
||||
<section class="rounded-xl bg-gray-50 dark:bg-gray-800/50 p-4">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">
|
||||
Status
|
||||
</h3>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<UBadge
|
||||
:color="statusColor"
|
||||
:variant="statusVariant"
|
||||
size="md"
|
||||
class="font-bold"
|
||||
>
|
||||
{{ statusLabel }}
|
||||
</UBadge>
|
||||
<div
|
||||
v-if="device.status === 'cooldown' && device.cooldownUntil"
|
||||
class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<CooldownCountdown :until="device.cooldownUntil" />
|
||||
<span>Bis {{ cooldownUntilText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- iOS protection status -->
|
||||
<section
|
||||
v-if="showIosStars && iosStars"
|
||||
class="rounded-xl bg-gray-50 dark:bg-gray-800/50 p-4"
|
||||
>
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">
|
||||
iOS Schutz-Status
|
||||
</h3>
|
||||
<IosStarRating
|
||||
:enrollment="iosStars.enrollment"
|
||||
:sideload="iosStars.sideload"
|
||||
:app="iosStars.app"
|
||||
/>
|
||||
<ul class="mt-3 space-y-1.5 text-sm text-gray-700 dark:text-gray-200">
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Supervised</span>
|
||||
<span :class="iosStars.isSupervised ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
||||
{{ iosStars.isSupervised ? "Ja" : "Nein" }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Enrollment</span>
|
||||
<span :class="iosStars.enrollment ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
||||
{{ iosStars.enrollment ? "Ja" : "Nein" }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Sideload/Lock</span>
|
||||
<span :class="iosStars.sideload ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
||||
{{ iosStars.sideload ? "Ja" : "Nein" }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>ReBreak App</span>
|
||||
<span :class="iosStars.app ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
||||
{{ iosStars.app ? "Ja" : "Nein" }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Desktop current device toggle -->
|
||||
<section
|
||||
v-if="showDesktopToggle"
|
||||
class="rounded-xl bg-gray-50 dark:bg-gray-800/50 p-4"
|
||||
>
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">
|
||||
Dieser Computer
|
||||
</h3>
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="md"
|
||||
:icon="desktopToggleIcon"
|
||||
block
|
||||
@click="emit('toggle-protection', device)"
|
||||
>
|
||||
{{ desktopToggleLabel }}
|
||||
</UButton>
|
||||
</section>
|
||||
|
||||
<!-- Cooldown controls -->
|
||||
<section
|
||||
v-if="device.isCurrent"
|
||||
class="rounded-xl bg-gray-50 dark:bg-gray-800/50 p-4"
|
||||
>
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-3">
|
||||
Schlafmodus
|
||||
</h3>
|
||||
|
||||
<div v-if="device.status === 'cooldown' && device.cooldownUntil">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 mb-3">
|
||||
<UIcon name="i-heroicons-moon-20-solid" class="w-5 h-5 text-amber-500" />
|
||||
<span>Schlafmodus aktiv</span>
|
||||
<CooldownCountdown :until="device.cooldownUntil" />
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
size="md"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
block
|
||||
@click="emit('cancel-cooldown', device)"
|
||||
>
|
||||
Abbrechen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<USelect
|
||||
v-model="selectedMinutes"
|
||||
:items="cooldownOptions"
|
||||
placeholder="Dauer wählen"
|
||||
size="md"
|
||||
class="mb-3"
|
||||
/>
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="solid"
|
||||
size="md"
|
||||
icon="i-heroicons-moon-20-solid"
|
||||
block
|
||||
:disabled="!selectedMinutes"
|
||||
@click="onStartCooldown"
|
||||
>
|
||||
Schlafmodus starten
|
||||
</UButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Remove device -->
|
||||
<section class="pt-2">
|
||||
<UButton
|
||||
color="error"
|
||||
variant="soft"
|
||||
size="md"
|
||||
icon="i-heroicons-trash-20-solid"
|
||||
block
|
||||
@click="emit('remove', device)"
|
||||
>
|
||||
Gerät entfernen
|
||||
</UButton>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="md"
|
||||
block
|
||||
@click="onClose"
|
||||
>
|
||||
Schließen
|
||||
</UButton>
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { ComputedDevice, DeviceStatus } from "~/composables/useDeviceStatus";
|
||||
|
||||
const props = defineProps<{
|
||||
device: ComputedDevice | null;
|
||||
iosStars?: {
|
||||
enrollment: boolean;
|
||||
sideload: boolean;
|
||||
app: boolean;
|
||||
isSupervised: boolean;
|
||||
} | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "toggle-protection", device: ComputedDevice): void;
|
||||
(e: "start-cooldown", device: ComputedDevice, minutes: number): void;
|
||||
(e: "cancel-cooldown", device: ComputedDevice): void;
|
||||
(e: "remove", device: ComputedDevice): void;
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
const selectedMinutes = ref<number | undefined>(undefined);
|
||||
|
||||
const cooldownOptions = [
|
||||
{ label: "15 Minuten", value: 15 },
|
||||
{ label: "30 Minuten", value: 30 },
|
||||
{ label: "1 Stunde", value: 60 },
|
||||
{ label: "3 Stunden", value: 180 },
|
||||
{ label: "8 Stunden", value: 480 },
|
||||
{ label: "24 Stunden", value: 1440 },
|
||||
];
|
||||
|
||||
const deviceName = computed(() => {
|
||||
if (!props.device) return "";
|
||||
if (props.device.name) return props.device.name;
|
||||
return props.device.platform === "ios" ? "iPhone" : "Dieser Computer";
|
||||
});
|
||||
|
||||
const deviceIcon = computed(() => {
|
||||
if (!props.device) return "i-heroicons-question-mark-circle";
|
||||
if (props.device.platform === "ios" || props.device.platform === "android") {
|
||||
return "i-heroicons-device-phone-mobile";
|
||||
}
|
||||
return "i-heroicons-computer-desktop";
|
||||
});
|
||||
|
||||
const platformLabel = computed(() => {
|
||||
if (!props.device) return "";
|
||||
const labels: Record<ComputedDevice["platform"], string> = {
|
||||
mac: "macOS",
|
||||
windows: "Windows",
|
||||
ios: "iOS",
|
||||
android: "Android",
|
||||
unknown: "Unbekannt",
|
||||
};
|
||||
return labels[props.device.platform];
|
||||
});
|
||||
|
||||
const showIosStars = computed(() => props.device?.isCurrent && props.device?.platform === "ios");
|
||||
const showDesktopToggle = computed(() => props.device?.isCurrent && (props.device?.platform === "mac" || props.device?.platform === "windows"));
|
||||
|
||||
const statusConfig: Record<
|
||||
DeviceStatus,
|
||||
{ label: string; color: "success" | "warning" | "error" | "neutral"; variant: "subtle" | "outline" }
|
||||
> = {
|
||||
active: { label: "Aktiv", color: "success", variant: "subtle" },
|
||||
cooldown: { label: "Schlafmodus", color: "warning", variant: "subtle" },
|
||||
revoked: { label: "Widerrufen", color: "error", variant: "subtle" },
|
||||
pending: { label: "Ausstehend", color: "neutral", variant: "subtle" },
|
||||
unprotected: { label: "Ungeschützt", color: "neutral", variant: "outline" },
|
||||
};
|
||||
|
||||
const statusLabel = computed(() => (props.device ? statusConfig[props.device.status].label : ""));
|
||||
const statusColor = computed(() => (props.device ? statusConfig[props.device.status].color : "neutral"));
|
||||
const statusVariant = computed(() => (props.device ? statusConfig[props.device.status].variant : "subtle"));
|
||||
|
||||
const cooldownUntilText = computed(() => {
|
||||
if (!props.device?.cooldownUntil) return "";
|
||||
return new Date(props.device.cooldownUntil).toLocaleString("de-DE", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
});
|
||||
});
|
||||
|
||||
const desktopToggleLabel = computed(() => {
|
||||
if (!props.device) return "Schutz umschalten";
|
||||
return props.device.status === "unprotected" ? "Schutz aktivieren" : "Schutz deaktivieren";
|
||||
});
|
||||
|
||||
const desktopToggleIcon = computed(() => {
|
||||
if (!props.device) return "i-heroicons-shield-exclamation";
|
||||
return props.device.status === "unprotected" ? "i-heroicons-shield-check" : "i-heroicons-shield-exclamation";
|
||||
});
|
||||
|
||||
function onClose() {
|
||||
open.value = false;
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function onStartCooldown() {
|
||||
if (!props.device || !selectedMinutes.value) return;
|
||||
emit("start-cooldown", props.device, selectedMinutes.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.device,
|
||||
() => {
|
||||
selectedMinutes.value = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
watch(open, (value, oldValue) => {
|
||||
if (oldValue === true && value === false) {
|
||||
emit("close");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user