Files
amayo/AmayoWeb/src/views/EmbedsView.vue
2025-12-01 18:59:48 +00:00

374 lines
8.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="embeds-view">
<!-- Header -->
<div class="view-header">
<div class="header-content">
<h2 class="view-title">Custom Messages</h2>
<p class="view-subtitle">Create rich visual messages for your server</p>
</div>
<button class="create-btn" @click="createNewEmbed">
<span class="icon">+</span>
Create Embed
</button>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>Loading your embeds...</p>
</div>
<!-- Empty State -->
<div v-else-if="embeds.length === 0" class="empty-state">
<div class="empty-icon">🎨</div>
<h3>No embeds yet</h3>
<p>Create your first custom embed to make your server stand out!</p>
<button class="create-btn-large" @click="createNewEmbed">
Start Creating
</button>
</div>
<!-- Embeds Grid -->
<div v-else class="embeds-grid">
<div
v-for="embed in embeds"
:key="embed.id"
class="embed-card"
@click="editEmbed(embed)"
>
<div class="embed-preview" :style="{ borderLeftColor: '#' + embed.color.toString(16).padStart(6, '0') }">
<div class="embed-header">
<h3 class="embed-title">{{ embed.name || 'Untitled Embed' }}</h3>
</div>
<div class="embed-body">
<div class="component-count">
<span class="icon">🧩</span>
{{ embed.components.length }} components
</div>
<div class="last-updated">
Updated {{ formatDate(embed.updatedAt) }}
</div>
</div>
</div>
<div class="embed-actions">
<button class="action-btn edit" @click.stop="editEmbed(embed)" title="Edit">
</button>
<button class="action-btn delete" @click.stop="deleteEmbed(embed)" title="Delete">
🗑
</button>
</div>
</div>
</div>
<!-- Builder Modal -->
<div v-if="showBuilder" class="builder-modal">
<EmbedBuilder
:initial-data="selectedEmbed"
:guild="guild"
@close="closeBuilder"
@save="handleSave"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { embedsService } from '../services/embeds'
import EmbedBuilder from '../components/EmbedBuilder.vue'
const props = defineProps({
guildId: {
type: String,
required: true
},
guild: {
type: Object,
default: null
}
})
const loading = ref(true)
const embeds = ref([])
const showBuilder = ref(false)
const selectedEmbed = ref(null)
onMounted(async () => {
console.log('EmbedsView mounted for guild:', props.guildId)
await loadEmbeds()
})
const loadEmbeds = async () => {
console.log('Loading embeds...')
loading.value = true
try {
const rawEmbeds = await embedsService.getEmbeds(props.guildId)
embeds.value = rawEmbeds.map(e => ({
id: e.id,
name: e.name,
updatedAt: e.updatedAt,
title: e.config?.title || '',
color: e.config?.color || 0x5865F2,
coverImage: e.config?.coverImage || '',
components: e.config?.components || []
}))
console.log('Embeds loaded:', embeds.value)
} catch (error) {
console.error('Failed to load embeds:', error)
} finally {
loading.value = false
}
}
const createNewEmbed = () => {
selectedEmbed.value = null
showBuilder.value = true
}
const editEmbed = (embed) => {
selectedEmbed.value = JSON.parse(JSON.stringify(embed)) // Deep copy
showBuilder.value = true
}
const deleteEmbed = async (embed) => {
if (!confirm(`Are you sure you want to delete "${embed.title}"?`)) return
try {
await embedsService.deleteEmbed(props.guildId, embed.id)
embeds.value = embeds.value.filter(e => e.id !== embed.id)
} catch (error) {
console.error('Failed to delete embed:', error)
alert('Failed to delete embed')
}
}
const closeBuilder = () => {
showBuilder.value = false
selectedEmbed.value = null
}
const handleSave = async (embedData) => {
try {
// Construct payload for backend (nested data)
const payload = {
name: embedData.name,
data: {
title: embedData.title,
color: embedData.color,
coverImage: embedData.coverImage,
components: embedData.components
}
}
if (selectedEmbed.value) {
// Update existing
await embedsService.updateEmbed(props.guildId, selectedEmbed.value.id, payload)
} else {
// Create new
await embedsService.createEmbed(props.guildId, payload)
}
await loadEmbeds()
closeBuilder()
} catch (error) {
console.error('Failed to save embed:', error)
alert('Failed to save embed')
}
}
const formatDate = (dateString) => {
if (!dateString) return 'Never'
return new Date(dateString).toLocaleDateString()
}
</script>
<style scoped>
.embeds-view {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.view-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.view-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 5px;
background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.view-subtitle {
color: var(--text-secondary);
}
.create-btn {
background: var(--accent-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.create-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
}
.embeds-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
overflow-y: auto;
padding-bottom: 20px;
}
.embed-card {
background: var(--unity-card);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
position: relative;
display: flex;
flex-direction: column;
}
.embed-card:hover {
transform: translateY(-4px);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.embed-preview {
padding: 20px;
border-left: 4px solid #5865f2;
background: rgba(0, 0, 0, 0.2);
flex: 1;
}
.embed-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 15px;
color: white;
}
.embed-body {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-secondary);
font-size: 0.9rem;
}
.component-count {
display: flex;
align-items: center;
gap: 6px;
}
.embed-actions {
display: flex;
justify-content: flex-end;
padding: 10px;
gap: 8px;
background: rgba(0, 0, 0, 0.3);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.action-btn {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 6px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.action-btn.delete:hover {
background: rgba(255, 59, 59, 0.1);
color: #ff3b3b;
}
.builder-modal {
position: fixed;
inset: 0;
background: var(--unity-bg);
z-index: 100;
display: flex;
flex-direction: column;
}
/* Loading & Empty States */
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
text-align: center;
color: var(--text-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
.create-btn-large {
margin-top: 20px;
background: var(--accent-primary);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.create-btn-large:hover {
transform: scale(1.05);
}
</style>