feat: implement premium UI, mobile responsiveness, and message status LEDs
This commit is contained in:
@@ -5,10 +5,11 @@ import MessageList from './MessageList.vue';
|
|||||||
import UserList from './UserList.vue';
|
import UserList from './UserList.vue';
|
||||||
import MusicPlayer from './MusicPlayer.vue';
|
import MusicPlayer from './MusicPlayer.vue';
|
||||||
import TokenCreator from './TokenCreator.vue';
|
import TokenCreator from './TokenCreator.vue';
|
||||||
import { Hash, Volume2, VolumeX, Settings, X, Coins } from 'lucide-vue-next';
|
import { Hash, Volume2, VolumeX, Settings, X, Coins, Menu } from 'lucide-vue-next';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const showTokenCreator = ref(false);
|
const showTokenCreator = ref(false);
|
||||||
|
const showMobileMenu = ref(false);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
||||||
@@ -34,10 +35,17 @@ const saveSettings = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen w-full overflow-hidden relative">
|
<div class="flex h-screen w-full overflow-hidden relative bg-discord-dark">
|
||||||
|
<!-- Mobile Menu Overlay -->
|
||||||
|
<div
|
||||||
|
v-if="showMobileMenu"
|
||||||
|
@click="showMobileMenu = false"
|
||||||
|
class="fixed inset-0 bg-black/60 z-40 md:hidden backdrop-blur-sm transition-opacity"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div v-if="showSettings" class="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
<div v-if="showSettings" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||||
<div class="bg-crypto-panel border border-white/10 rounded-2xl w-full max-w-md shadow-2xl animate-fade-in-up">
|
<div class="bg-discord-sidebar border border-white/10 rounded-2xl w-full max-w-md shadow-2xl animate-pop-in">
|
||||||
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
||||||
<h2 class="text-xl font-bold text-white">Profile Settings</h2>
|
<h2 class="text-xl font-bold text-white">Profile Settings</h2>
|
||||||
<button @click="showSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
<button @click="showSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
||||||
@@ -50,13 +58,13 @@ const saveSettings = () => {
|
|||||||
<input
|
<input
|
||||||
v-model="newUsername"
|
v-model="newUsername"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full bg-crypto-dark border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
class="w-full bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
||||||
placeholder="Enter new username"
|
placeholder="Enter new username"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-crypto-muted uppercase tracking-wider mb-2">Wallet Address</label>
|
<label class="block text-xs font-bold text-crypto-muted uppercase tracking-wider mb-2">Wallet Address</label>
|
||||||
<div class="w-full bg-crypto-dark/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
<div class="w-full bg-discord-black/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
||||||
{{ walletAddress }}
|
{{ walletAddress }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +87,12 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Channels Sidebar -->
|
<!-- Channels Sidebar -->
|
||||||
<div class="w-64 bg-[#2b2d31] flex flex-col border-r border-black/20">
|
<div
|
||||||
|
:class="[
|
||||||
|
'fixed inset-y-0 left-0 w-64 bg-discord-sidebar flex flex-col border-r border-black/20 z-50 transition-transform duration-300 md:relative md:translate-x-0',
|
||||||
|
showMobileMenu ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="h-12 px-4 flex items-center justify-between border-b border-black/20 shadow-sm">
|
<div class="h-12 px-4 flex items-center justify-between border-b border-black/20 shadow-sm">
|
||||||
<h1 class="font-bold text-white truncate">Plexus Server</h1>
|
<h1 class="font-bold text-white truncate">Plexus Server</h1>
|
||||||
<button @click="toggleMute" class="text-gray-400 hover:text-gray-200 transition-colors">
|
<button @click="toggleMute" class="text-gray-400 hover:text-gray-200 transition-colors">
|
||||||
@@ -91,7 +104,7 @@ const saveSettings = () => {
|
|||||||
<div class="flex-1 overflow-y-auto py-3 space-y-0.5 px-2">
|
<div class="flex-1 overflow-y-auto py-3 space-y-0.5 px-2">
|
||||||
<!-- Token Creator Link -->
|
<!-- Token Creator Link -->
|
||||||
<button
|
<button
|
||||||
@click="showTokenCreator = true"
|
@click="showTokenCreator = true; showMobileMenu = false"
|
||||||
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group mb-4',
|
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group mb-4',
|
||||||
showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||||
>
|
>
|
||||||
@@ -102,7 +115,7 @@ const saveSettings = () => {
|
|||||||
<div class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Text Channels</div>
|
<div class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Text Channels</div>
|
||||||
<div v-for="channel in channels" :key="channel.id">
|
<div v-for="channel in channels" :key="channel.id">
|
||||||
<button
|
<button
|
||||||
@click="chatStore.setChannel(channel.id); showTokenCreator = false"
|
@click="chatStore.setChannel(channel.id); showTokenCreator = false; showMobileMenu = false"
|
||||||
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group',
|
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group',
|
||||||
currentChannel === channel.id && !showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
currentChannel === channel.id && !showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||||
>
|
>
|
||||||
@@ -113,7 +126,7 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Music Player & Profile -->
|
<!-- Music Player & Profile -->
|
||||||
<div class="bg-[#232428] p-2 space-y-2">
|
<div class="bg-discord-black p-2 space-y-2">
|
||||||
<MusicPlayer />
|
<MusicPlayer />
|
||||||
|
|
||||||
<div class="flex items-center gap-2 p-1.5 rounded-md hover:bg-[#35373c] transition-all group cursor-pointer" @click="showSettings = true">
|
<div class="flex items-center gap-2 p-1.5 rounded-md hover:bg-[#35373c] transition-all group cursor-pointer" @click="showSettings = true">
|
||||||
@@ -121,7 +134,7 @@ const saveSettings = () => {
|
|||||||
<div class="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center text-white text-xs font-bold">
|
<div class="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center text-white text-xs font-bold">
|
||||||
{{ username?.substring(0, 2).toUpperCase() }}
|
{{ username?.substring(0, 2).toUpperCase() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-[#232428] rounded-full"></div>
|
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-discord-black rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-xs font-bold text-white truncate">{{ username }}</div>
|
<div class="text-xs font-bold text-white truncate">{{ username }}</div>
|
||||||
@@ -133,9 +146,15 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 flex flex-col bg-[#313338] relative overflow-hidden">
|
<div class="flex-1 flex flex-col bg-discord-dark relative overflow-hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="h-12 px-4 flex items-center border-b border-black/20 shadow-sm bg-[#313338]/95 backdrop-blur-sm z-10">
|
<div class="h-12 px-4 flex items-center border-b border-black/20 shadow-sm bg-discord-dark/95 backdrop-blur-sm z-10">
|
||||||
|
<button
|
||||||
|
@click="showMobileMenu = true"
|
||||||
|
class="md:hidden mr-3 text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<Menu size="24" />
|
||||||
|
</button>
|
||||||
<Hash size="20" class="text-gray-400 mr-2" />
|
<Hash size="20" class="text-gray-400 mr-2" />
|
||||||
<span class="font-bold text-white mr-4">{{ showTokenCreator ? 'Token Creator' : currentChannel }}</span>
|
<span class="font-bold text-white mr-4">{{ showTokenCreator ? 'Token Creator' : currentChannel }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +166,7 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Member List (Discord Style) -->
|
<!-- Member List (Discord Style) -->
|
||||||
<div class="w-60 bg-[#2b2d31] border-l border-black/20 hidden lg:flex flex-col">
|
<div class="w-60 bg-discord-sidebar border-l border-black/20 hidden xl:flex flex-col">
|
||||||
<UserList />
|
<UserList />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,33 +53,36 @@ const formatTime = (isoString) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 flex flex-col h-full bg-black/40 backdrop-blur-sm relative z-10">
|
<div class="flex-1 flex flex-col h-full bg-discord-dark relative z-10">
|
||||||
<!-- Header -->
|
<!-- Header (Desktop only, mobile header is in ChatLayout) -->
|
||||||
<div class="h-12 border-b border-white/5 flex items-center px-4 shadow-sm bg-crypto-panel/50">
|
<div class="hidden md:flex h-12 border-b border-black/20 items-center px-4 shadow-sm bg-discord-dark/95 backdrop-blur-sm">
|
||||||
<div class="text-lg font-bold text-white"># {{ currentChannel }}</div>
|
<div class="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Hash size="18" class="text-gray-400" />
|
||||||
|
{{ currentChannel }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth">
|
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth custom-scrollbar">
|
||||||
<!-- Beginning of conversation marker -->
|
<!-- Beginning of conversation marker -->
|
||||||
<div class="py-12 px-4 border-b border-white/5 mb-8">
|
<div class="py-12 px-4 border-b border-white/5 mb-8">
|
||||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center text-white mb-4 shadow-xl shadow-violet-600/20">
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center text-white mb-4 shadow-xl shadow-violet-600/20">
|
||||||
<Hash size="32" />
|
<Hash size="32" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-white mb-1">Welcome to #{{ currentChannel }}!</h2>
|
<h2 class="text-3xl font-bold text-white mb-2">Welcome to #{{ currentChannel }}!</h2>
|
||||||
<p class="text-gray-400 text-sm max-w-md">This is the very beginning of the <span class="text-violet-400 font-semibold">#{{ currentChannel }}</span> channel. Use this space to connect, share, and grow with the community.</p>
|
<p class="text-gray-400 text-base max-w-md leading-relaxed">This is the very beginning of the <span class="text-violet-400 font-semibold">#{{ currentChannel }}</span> channel. Use this space to connect, share, and grow with the community.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(msg, index) in currentMessages" :key="msg.id"
|
<div v-for="(msg, index) in currentMessages" :key="msg.id || msg.tempId"
|
||||||
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
|
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
|
||||||
>
|
>
|
||||||
<!-- Avatar (only if first message in group) -->
|
<!-- Avatar (only if first message in group) -->
|
||||||
<div class="w-10 flex-shrink-0">
|
<div class="w-10 flex-shrink-0">
|
||||||
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
||||||
class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm shadow-lg border border-white/10 mt-1"
|
class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm shadow-lg border border-white/10 mt-1"
|
||||||
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-crypto-panel'"
|
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-discord-sidebar'"
|
||||||
>
|
>
|
||||||
{{ msg.username.substring(0, 2).toUpperCase() }}
|
{{ msg.username?.substring(0, 2).toUpperCase() }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-10 text-[10px] text-crypto-muted opacity-0 group-hover:opacity-100 text-right pr-2 pt-1.5 transition-opacity">
|
<div v-else class="w-10 text-[10px] text-crypto-muted opacity-0 group-hover:opacity-100 text-right pr-2 pt-1.5 transition-opacity">
|
||||||
{{ formatTime(msg.timestamp) }}
|
{{ formatTime(msg.timestamp) }}
|
||||||
@@ -88,17 +91,26 @@ const formatTime = (isoString) => {
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress" class="flex items-baseline gap-2 mb-0.5">
|
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress" class="flex items-center gap-2 mb-0.5">
|
||||||
<span :class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']">
|
<span :class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']">
|
||||||
{{ msg.username }}
|
{{ msg.username }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="msg.txId" class="text-[9px] text-crypto-muted font-mono bg-white/5 px-1.5 py-0.5 rounded border border-white/5">
|
|
||||||
{{ msg.txId.slice(0, 8) }}
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
||||||
|
|
||||||
|
<!-- Status LED -->
|
||||||
|
<div
|
||||||
|
v-if="msg.status"
|
||||||
|
class="led ml-1"
|
||||||
|
:class="{
|
||||||
|
'led-orange': msg.status === 'pending',
|
||||||
|
'led-green': msg.status === 'validated',
|
||||||
|
'led-red': msg.status === 'failed'
|
||||||
|
}"
|
||||||
|
:title="msg.status"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-100 text-sm leading-relaxed break-words">
|
<div :class="['text-sm leading-relaxed break-words', msg.status === 'failed' ? 'text-status-failed line-through opacity-60' : 'text-gray-100']">
|
||||||
{{ msg.content }}
|
{{ msg.content }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,7 +132,7 @@ const formatTime = (isoString) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover Actions -->
|
<!-- Hover Actions -->
|
||||||
<div class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-crypto-panel border border-white/10 rounded-lg p-1 shadow-xl">
|
<div v-if="msg.status !== 'failed'" class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-discord-sidebar border border-white/10 rounded-lg p-1 shadow-xl">
|
||||||
<button
|
<button
|
||||||
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
|
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
|
||||||
class="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-all"
|
class="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-all"
|
||||||
@@ -130,7 +142,7 @@ const formatTime = (isoString) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Emoji Picker Popover -->
|
<!-- Emoji Picker Popover -->
|
||||||
<div v-if="showEmojiPicker === msg.id" class="absolute right-0 bottom-full mb-2 bg-crypto-panel border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-fade-in-up">
|
<div v-if="showEmojiPicker === msg.id" class="absolute right-0 bottom-full mb-2 bg-discord-sidebar border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-pop-in">
|
||||||
<button
|
<button
|
||||||
v-for="emoji in EMOJIS"
|
v-for="emoji in EMOJIS"
|
||||||
:key="emoji"
|
:key="emoji"
|
||||||
@@ -145,22 +157,33 @@ const formatTime = (isoString) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input -->
|
<!-- Input -->
|
||||||
<div class="p-4 bg-crypto-panel/80 border-t border-white/5">
|
<div class="p-4 bg-discord-dark">
|
||||||
<div class="relative">
|
<div class="relative bg-discord-sidebar/50 rounded-xl border border-white/5 p-1 transition-all focus-within:border-violet-500/30 focus-within:bg-discord-sidebar/80">
|
||||||
<input
|
<input
|
||||||
v-model="newMessage"
|
v-model="newMessage"
|
||||||
@keyup.enter="send"
|
@keyup.enter="send"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="`Message #${currentChannel}`"
|
:placeholder="`Message #${currentChannel}`"
|
||||||
class="w-full bg-crypto-dark/50 text-white placeholder-gray-500 rounded-lg py-3 pl-4 pr-12 focus:outline-none focus:ring-2 focus:ring-crypto-accent/50 border border-white/5 transition-all"
|
class="w-full bg-transparent text-white placeholder-gray-500 py-3 pl-4 pr-12 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="send"
|
@click="send"
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-white hover:bg-white/10 rounded-md transition-colors"
|
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-violet-400 transition-colors"
|
||||||
>
|
>
|
||||||
<Send size="20" />
|
<Send size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2 flex items-center gap-4 px-1">
|
||||||
|
<div class="flex items-center gap-1.5 text-[10px] text-gray-500 uppercase tracking-widest font-bold">
|
||||||
|
<div class="led led-orange w-1.5 h-1.5"></div> Pending
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5 text-[10px] text-gray-500 uppercase tracking-widest font-bold">
|
||||||
|
<div class="led led-green w-1.5 h-1.5"></div> Validated
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5 text-[10px] text-gray-500 uppercase tracking-widest font-bold">
|
||||||
|
<div class="led led-red w-1.5 h-1.5"></div> Failed
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -45,8 +45,19 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
if (!messages.value[message.channelId]) {
|
if (!messages.value[message.channelId]) {
|
||||||
messages.value[message.channelId] = [];
|
messages.value[message.channelId] = [];
|
||||||
}
|
}
|
||||||
// Use spread to trigger reactivity if needed, though .push should work in Vue 3
|
|
||||||
messages.value[message.channelId] = [...messages.value[message.channelId], message];
|
// Check if this message matches a local pending message (by content and wallet)
|
||||||
|
// In a real app, we'd use the txId to match
|
||||||
|
const pendingIdx = messages.value[message.channelId].findIndex(
|
||||||
|
m => m.status === 'pending' && m.content === message.content && m.walletAddress === message.walletAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingIdx !== -1) {
|
||||||
|
// Update the pending message with server data and mark as validated
|
||||||
|
messages.value[message.channelId][pendingIdx] = { ...message, status: 'validated' };
|
||||||
|
} else {
|
||||||
|
messages.value[message.channelId] = [...messages.value[message.channelId], { ...message, status: 'validated' }];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('userList', (userList) => {
|
socket.value.on('userList', (userList) => {
|
||||||
@@ -63,48 +74,74 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
||||||
|
username.value = newName;
|
||||||
|
const savedAuth = Cookies.get('plexus_auth');
|
||||||
|
if (savedAuth) {
|
||||||
|
const authData = JSON.parse(savedAuth);
|
||||||
|
authData.name = newName;
|
||||||
|
Cookies.set('plexus_auth', JSON.stringify(authData), { expires: 7 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('error', (err) => {
|
||||||
|
console.error('Socket error:', err);
|
||||||
|
// Handle failed messages if we can identify them
|
||||||
|
// For now, just alert
|
||||||
|
alert(err.message || 'An error occurred');
|
||||||
|
});
|
||||||
|
|
||||||
fetchChannels();
|
fetchChannels();
|
||||||
fetchMessages(currentChannel.value);
|
fetchMessages(currentChannel.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReaction(messageId, emoji) {
|
|
||||||
if (!socket.value) return;
|
|
||||||
|
|
||||||
// Simulate a blockchain transaction for reaction
|
|
||||||
console.log('Simulating 1 $PLEXUS transaction for reaction...');
|
|
||||||
|
|
||||||
socket.value.emit('toggleReaction', {
|
|
||||||
messageId,
|
|
||||||
walletAddress: walletAddress.value,
|
|
||||||
emoji
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const savedAuth = Cookies.get('plexus_auth');
|
|
||||||
if (savedAuth) {
|
|
||||||
try {
|
|
||||||
const { wallet, name, sig } = JSON.parse(savedAuth);
|
|
||||||
connect(wallet, name, sig);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse saved auth', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendMessage(content) {
|
function sendMessage(content) {
|
||||||
if (!socket.value || !content.trim()) return;
|
if (!socket.value || !content.trim()) return;
|
||||||
|
|
||||||
// Simulate a blockchain transaction
|
const tempId = 'temp-' + Date.now();
|
||||||
|
const pendingMsg = {
|
||||||
|
tempId,
|
||||||
|
channelId: currentChannel.value,
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
username: username.value,
|
||||||
|
content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'pending',
|
||||||
|
reactions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to local state immediately
|
||||||
|
if (!messages.value[currentChannel.value]) {
|
||||||
|
messages.value[currentChannel.value] = [];
|
||||||
|
}
|
||||||
|
messages.value[currentChannel.value].push(pendingMsg);
|
||||||
|
|
||||||
|
// Simulate a blockchain transaction delay
|
||||||
console.log('Simulating 1 $PLEXUS transaction for message...');
|
console.log('Simulating 1 $PLEXUS transaction for message...');
|
||||||
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
||||||
|
|
||||||
socket.value.emit('sendMessage', {
|
setTimeout(() => {
|
||||||
channelId: currentChannel.value,
|
// Randomly fail 5% of the time for demonstration
|
||||||
walletAddress: walletAddress.value,
|
const failed = Math.random() < 0.05;
|
||||||
content,
|
|
||||||
txId: mockTxId
|
if (failed) {
|
||||||
});
|
const msg = messages.value[currentChannel.value].find(m => m.tempId === tempId);
|
||||||
|
if (msg) msg.status = 'failed';
|
||||||
|
console.error('Transaction failed!');
|
||||||
|
|
||||||
|
// Remove failed message after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
messages.value[currentChannel.value] = messages.value[currentChannel.value].filter(m => m.tempId !== tempId);
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
socket.value.emit('sendMessage', {
|
||||||
|
channelId: currentChannel.value,
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
content,
|
||||||
|
txId: mockTxId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReaction(messageId, emoji) {
|
function toggleReaction(messageId, emoji) {
|
||||||
@@ -134,23 +171,6 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for username updates
|
|
||||||
onMounted(() => {
|
|
||||||
if (socket.value) {
|
|
||||||
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
|
||||||
username.value = newName;
|
|
||||||
// Update cookie
|
|
||||||
const authData = JSON.parse(Cookies.get('plexus_auth') || '{}');
|
|
||||||
authData.name = newName;
|
|
||||||
Cookies.set('plexus_auth', JSON.stringify(authData), { expires: 7 });
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('error', (err) => {
|
|
||||||
alert(err.message || 'An error occurred');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setChannel(channelId) {
|
function setChannel(channelId) {
|
||||||
currentChannel.value = channelId;
|
currentChannel.value = channelId;
|
||||||
if (!messages.value[channelId]) {
|
if (!messages.value[channelId]) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-crypto-dark text-crypto-text overflow-hidden;
|
@apply bg-discord-dark text-crypto-text overflow-hidden antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
@@ -12,21 +12,48 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-crypto-dark;
|
@apply bg-discord-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-crypto-panel rounded;
|
@apply bg-discord-sidebar rounded-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
@apply bg-crypto-muted;
|
@apply bg-white/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass {
|
.glass {
|
||||||
@apply backdrop-blur-md bg-white/5 border border-white/10;
|
@apply backdrop-blur-md bg-white/[0.03] border border-white/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
@apply backdrop-blur-xl bg-black/40 border-r border-white/5;
|
@apply backdrop-blur-xl bg-discord-sidebar/80 border-r border-white/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led {
|
||||||
|
@apply w-2 h-2 rounded-full shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-orange {
|
||||||
|
@apply bg-status-pending shadow-status-pending/50 animate-led-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-green {
|
||||||
|
@apply bg-status-validated shadow-status-validated/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-red {
|
||||||
|
@apply bg-status-failed shadow-status-failed/50 animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness helpers */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-hidden {
|
||||||
|
@apply -translate-x-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-visible {
|
||||||
|
@apply translate-x-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,18 +12,34 @@ export default {
|
|||||||
'crypto-accent': '#8b5cf6', // Violet
|
'crypto-accent': '#8b5cf6', // Violet
|
||||||
'crypto-text': '#e2e8f0',
|
'crypto-text': '#e2e8f0',
|
||||||
'crypto-muted': '#94a3b8',
|
'crypto-muted': '#94a3b8',
|
||||||
|
'discord-dark': '#313338',
|
||||||
|
'discord-sidebar': '#2b2d31',
|
||||||
|
'discord-black': '#1e1f22',
|
||||||
|
'status-pending': '#f59e0b', // Orange
|
||||||
|
'status-validated': '#10b981', // Green
|
||||||
|
'status-failed': '#ef4444', // Red
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ['Inter', 'sans-serif', 'system-ui'],
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
||||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'pop-in': 'popIn 0.2s cubic-bezier(0.26, 0.53, 0.74, 1.48)',
|
||||||
|
'led-pulse': 'ledPulse 2s infinite',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
fadeInUp: {
|
fadeInUp: {
|
||||||
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
popIn: {
|
||||||
|
'0%': { opacity: '0', transform: 'scale(0.8)' },
|
||||||
|
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
},
|
||||||
|
ledPulse: {
|
||||||
|
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
'50%': { opacity: '0.6', transform: 'scale(0.95)' },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import duckdb
|
import duckdb
|
||||||
import os
|
import os
|
||||||
|
|
||||||
DB_PATH = "data/tasks.duckdb"
|
DB_PATH = "tasks/tasks.duckdb"
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
if not os.path.exists("data"):
|
|
||||||
os.makedirs("data")
|
|
||||||
|
|
||||||
con = duckdb.connect(DB_PATH)
|
con = duckdb.connect(DB_PATH)
|
||||||
con.execute("""
|
con.execute("""
|
||||||
|
|||||||
Reference in New Issue
Block a user