193 lines
8.7 KiB
Vue
193 lines
8.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
const props = defineProps<{ token: string }>()
|
|
const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'
|
|
|
|
const competitors = ref<Array<any>>([])
|
|
const isLoading = ref(true)
|
|
const isSubmitting = ref(false)
|
|
const errorMsg = ref('')
|
|
|
|
const showForm = ref(false)
|
|
const form = ref({
|
|
competitor_name: '',
|
|
platform: 'twitter',
|
|
target_handle: '',
|
|
keywords: ''
|
|
})
|
|
|
|
const fetchCompetitors = async () => {
|
|
isLoading.value = true
|
|
try {
|
|
const res = await fetch(`${API_BASE}/monitors/competitors`, {
|
|
headers: { 'Authorization': `Bearer ${props.token}` }
|
|
})
|
|
const data = await res.json()
|
|
if (res.ok && data.code === 200) {
|
|
competitors.value = data.data || []
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const createCompetitor = async () => {
|
|
isSubmitting.value = true
|
|
errorMsg.value = ''
|
|
try {
|
|
const res = await fetch(`${API_BASE}/monitors/competitors`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${props.token}`
|
|
},
|
|
body: JSON.stringify(form.value)
|
|
})
|
|
const data = await res.json()
|
|
if (!res.ok || data.code !== 200) {
|
|
throw new Error(data.message || 'Failed to add competitor')
|
|
}
|
|
|
|
// Reset form & reload
|
|
form.value = { competitor_name: '', platform: 'twitter', target_handle: '', keywords: '' }
|
|
showForm.value = false
|
|
await fetchCompetitors()
|
|
|
|
} catch (err: any) {
|
|
errorMsg.value = err.message
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
const deleteCompetitor = async (id: number) => {
|
|
if (!confirm('Are you sure you want to stop tracking this competitor?')) return
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/monitors/competitors/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${props.token}` }
|
|
})
|
|
if (res.ok) {
|
|
competitors.value = competitors.value.filter(c => c.id !== id)
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (props.token) fetchCompetitors()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
|
|
<div v-if="isLoading" class="p-10 flex justify-center">
|
|
<div class="w-6 h-6 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div class="flex justify-between items-center mb-6">
|
|
<p class="text-sm text-zinc-400">Track competitors tweets & mentions to find opportunistic conversations.</p>
|
|
<button
|
|
v-if="!showForm"
|
|
@click="showForm = true"
|
|
class="px-4 py-2 bg-brand-primary/20 text-brand-primary border border-brand-primary/30 rounded-lg text-xs font-semibold hover:bg-brand-primary hover:text-white transition-colors flex items-center gap-1"
|
|
>
|
|
+ Add Competitor
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Create Form -->
|
|
<form v-if="showForm" @submit.prevent="createCompetitor" class="bg-[#171717] border border-white/10 rounded-xl p-6 space-y-4 mb-8">
|
|
<div class="flex justify-between items-center mb-2 border-b border-white/10 pb-2">
|
|
<h3 class="text-sm font-semibold text-white">Add Target Radar</h3>
|
|
<button type="button" @click="showForm = false" class="text-zinc-500 hover:text-white">✕</button>
|
|
</div>
|
|
|
|
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded-lg">
|
|
{{ errorMsg }}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-medium text-zinc-300">Competitor Product / Name</label>
|
|
<input v-model="form.competitor_name" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. Acme Corp" />
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-medium text-zinc-300">Platform</label>
|
|
<select v-model="form.platform" class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50 appearance-none">
|
|
<option value="twitter">Twitter / X</option>
|
|
<option value="reddit">Reddit (Coming Soon)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-medium text-zinc-300">Target Handle</label>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-zinc-500 pointer-events-none text-sm">@</span>
|
|
<input v-model="form.target_handle" class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg pl-8 pr-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="acmecorp" />
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs font-medium text-zinc-300">Track Keywords (Comma separated)</label>
|
|
<input v-model="form.keywords" required class="w-full bg-[#0A0A0A] border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-brand-primary/50" placeholder="e.g. acme sucks, alternative to acme" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-4">
|
|
<button type="submit" :disabled="isSubmitting" class="px-5 py-2.5 bg-brand-primary hover:bg-brand-primary/90 text-white rounded-lg text-sm font-medium transition-all shadow flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed">
|
|
<span v-if="isSubmitting" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></span>
|
|
{{ isSubmitting ? 'Saving...' : 'Start Tracking' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- List -->
|
|
<div v-if="competitors.length === 0 && !showForm" class="text-center p-12 border border-dashed border-white/10 rounded-xl text-zinc-500">
|
|
You are not tracking any competitors.
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div v-for="c in competitors" :key="c.id" class="bg-[#0A0A0A] border border-white/10 rounded-xl p-5 relative group hover:border-white/20 transition-colors">
|
|
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-brand-primary">
|
|
<svg v-if="c.platform === 'twitter'" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-white">{{ c.competitor_name }}</h4>
|
|
<p v-if="c.target_handle" class="text-xs text-zinc-500">@{{ c.target_handle }}</p>
|
|
</div>
|
|
</div>
|
|
<span class="px-2.5 py-1 bg-green-500/10 text-green-400 text-[10px] font-bold rounded-full border border-green-500/20 uppercase tracking-wide">Active</span>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] text-zinc-500 uppercase tracking-wider font-semibold">Keywords Targeted</p>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
<span v-for="kw in c.keywords.split(',')" :key="kw" class="px-2 py-0.5 bg-zinc-800 text-zinc-300 text-[11px] rounded flex border border-zinc-700">
|
|
{{ kw.trim() }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button @click="deleteCompetitor(c.id)" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-red-500 hover:text-red-400 p-2 rounded-lg hover:bg-red-500/10">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|