This commit is contained in:
Shnimlz
2025-12-01 18:59:48 +00:00
parent 9c20ca0930
commit 7661b2b8b1
117 changed files with 17954 additions and 3591 deletions

View File

@@ -0,0 +1,373 @@
<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>