feat(magic): add DeviceHeroCard and DeviceListItem components
This commit is contained in:
parent
118269a8c9
commit
0258c818f3
141
apps/rebreak-magic/app/components/DeviceHeroCard.vue
Normal file
141
apps/rebreak-magic/app/components/DeviceHeroCard.vue
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-900 shadow-lg ring-1 ring-gray-100 dark:ring-gray-800 p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-5">
|
||||||
|
<div
|
||||||
|
class="shrink-0 w-16 h-16 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-8 h-8 text-[var(--rebreak-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-extrabold text-gray-900 dark:text-white truncate">
|
||||||
|
{{ deviceName }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{{ platformLabel }}
|
||||||
|
<span v-if="device.osVersion">· {{ device.osVersion }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 flex flex-col items-end gap-1">
|
||||||
|
<UBadge
|
||||||
|
:color="statusColor"
|
||||||
|
:variant="statusVariant"
|
||||||
|
size="sm"
|
||||||
|
class="font-bold"
|
||||||
|
>
|
||||||
|
{{ statusLabel }}
|
||||||
|
</UBadge>
|
||||||
|
<CooldownCountdown
|
||||||
|
v-if="device.status === 'cooldown' && device.cooldownUntil"
|
||||||
|
:until="device.cooldownUntil"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showIosStars && iosStars"
|
||||||
|
class="mt-4 flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<IosStarRating
|
||||||
|
:enrollment="iosStars.enrollment"
|
||||||
|
:sideload="iosStars.sideload"
|
||||||
|
:app="iosStars.app"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Enrollment · Sideload · App
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center gap-3">
|
||||||
|
<UButton
|
||||||
|
v-if="isCurrent && device.status === 'unprotected'"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
icon="i-heroicons-shield-check"
|
||||||
|
@click="emit('toggle-protection', device)"
|
||||||
|
>
|
||||||
|
Protect this device
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
trailing-icon="i-heroicons-chevron-right"
|
||||||
|
@click="emit('open', device)"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ComputedDevice, DeviceStatus } from "~/composables/useDeviceStatus";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
device: ComputedDevice;
|
||||||
|
isCurrent: boolean;
|
||||||
|
iosStars?: {
|
||||||
|
enrollment: boolean;
|
||||||
|
sideload: boolean;
|
||||||
|
app: boolean;
|
||||||
|
} | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "open", device: ComputedDevice): void;
|
||||||
|
(e: "toggle-protection", device: ComputedDevice): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const deviceName = computed(() => {
|
||||||
|
if (props.device.name) return props.device.name;
|
||||||
|
return props.device.platform === "ios" ? "iPhone" : "This Mac";
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceIcon = computed(() => {
|
||||||
|
if (props.device.platform === "ios" || props.device.platform === "android") {
|
||||||
|
return "i-heroicons-device-phone-mobile";
|
||||||
|
}
|
||||||
|
return "i-heroicons-computer-desktop";
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformLabel = computed(() => {
|
||||||
|
const labels: Record<ComputedDevice["platform"], string> = {
|
||||||
|
mac: "macOS",
|
||||||
|
windows: "Windows",
|
||||||
|
ios: "iOS",
|
||||||
|
android: "Android",
|
||||||
|
unknown: "Unknown",
|
||||||
|
};
|
||||||
|
return labels[props.device.platform];
|
||||||
|
});
|
||||||
|
|
||||||
|
const showIosStars = computed(() => props.isCurrent && props.device.platform === "ios");
|
||||||
|
|
||||||
|
const statusConfig: Record<
|
||||||
|
DeviceStatus,
|
||||||
|
{ label: string; color: "success" | "warning" | "error" | "neutral"; variant: "subtle" | "outline" }
|
||||||
|
> = {
|
||||||
|
active: { label: "Active", color: "success", variant: "subtle" },
|
||||||
|
cooldown: { label: "Cooldown", color: "warning", variant: "subtle" },
|
||||||
|
revoked: { label: "Revoked", color: "error", variant: "subtle" },
|
||||||
|
pending: { label: "Pending", color: "neutral", variant: "subtle" },
|
||||||
|
unprotected: { label: "Unprotected", color: "neutral", variant: "outline" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = computed(() => statusConfig[props.device.status].label);
|
||||||
|
const statusColor = computed(() => statusConfig[props.device.status].color);
|
||||||
|
const statusVariant = computed(() => statusConfig[props.device.status].variant);
|
||||||
|
</script>
|
||||||
94
apps/rebreak-magic/app/components/DeviceListItem.vue
Normal file
94
apps/rebreak-magic/app/components/DeviceListItem.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-4 p-4 rounded-xl bg-white dark:bg-gray-900 ring-1 ring-gray-100 dark:ring-gray-800 hover:ring-[var(--rebreak-primary)]/20 hover:shadow-md transition-all text-left"
|
||||||
|
@click="emit('open', device)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="shrink-0 w-10 h-10 rounded-xl bg-blue-50 dark:bg-gray-800 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="deviceIcon"
|
||||||
|
class="w-5 h-5 text-[var(--rebreak-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="font-bold text-gray-900 dark:text-white truncate">
|
||||||
|
{{ deviceName }}
|
||||||
|
</span>
|
||||||
|
<UBadge
|
||||||
|
:color="statusColor"
|
||||||
|
:variant="statusVariant"
|
||||||
|
size="xs"
|
||||||
|
class="font-semibold"
|
||||||
|
>
|
||||||
|
{{ statusLabel }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{{ platformLabel }}
|
||||||
|
<span v-if="device.osVersion">· {{ device.osVersion }}</span>
|
||||||
|
<span v-if="device.status === 'cooldown' && device.cooldownUntil" class="ml-2">
|
||||||
|
<CooldownCountdown :until="device.cooldownUntil" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-chevron-right"
|
||||||
|
class="w-5 h-5 text-gray-400 shrink-0"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ComputedDevice, DeviceStatus } from "~/composables/useDeviceStatus";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
device: ComputedDevice;
|
||||||
|
isCurrent: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "open", device: ComputedDevice): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const deviceName = computed(() => {
|
||||||
|
if (props.device.name) return props.device.name;
|
||||||
|
return props.device.platform === "ios" ? "iPhone" : "This Mac";
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceIcon = computed(() => {
|
||||||
|
if (props.device.platform === "ios" || props.device.platform === "android") {
|
||||||
|
return "i-heroicons-device-phone-mobile";
|
||||||
|
}
|
||||||
|
return "i-heroicons-computer-desktop";
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformLabel = computed(() => {
|
||||||
|
const labels: Record<ComputedDevice["platform"], string> = {
|
||||||
|
mac: "macOS",
|
||||||
|
windows: "Windows",
|
||||||
|
ios: "iOS",
|
||||||
|
android: "Android",
|
||||||
|
unknown: "Unknown",
|
||||||
|
};
|
||||||
|
return labels[props.device.platform];
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusConfig: Record<
|
||||||
|
DeviceStatus,
|
||||||
|
{ label: string; color: "success" | "warning" | "error" | "neutral"; variant: "subtle" | "outline" }
|
||||||
|
> = {
|
||||||
|
active: { label: "Active", color: "success", variant: "subtle" },
|
||||||
|
cooldown: { label: "Cooldown", color: "warning", variant: "subtle" },
|
||||||
|
revoked: { label: "Revoked", color: "error", variant: "subtle" },
|
||||||
|
pending: { label: "Pending", color: "neutral", variant: "subtle" },
|
||||||
|
unprotected: { label: "Unprotected", color: "neutral", variant: "outline" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = computed(() => statusConfig[props.device.status].label);
|
||||||
|
const statusColor = computed(() => statusConfig[props.device.status].color);
|
||||||
|
const statusVariant = computed(() => statusConfig[props.device.status].variant);
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user