first commit

This commit is contained in:
2026-01-13 22:55:46 +01:00
parent 3a3b0b046d
commit 2faf2dd8dc
31 changed files with 8490 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
<script setup>
import { ref, onUpdated, nextTick } from 'vue';
import { useChatStore } from '../stores/chat';
import { storeToRefs } from 'pinia';
import { Send, Hash, Smile } from 'lucide-vue-next';
const chatStore = useChatStore();
const { currentMessages, currentChannel, walletAddress } = storeToRefs(chatStore);
const newMessage = ref('');
const messagesContainer = ref(null);
const showEmojiPicker = ref(null); // messageId
const EMOJIS = ['👍', '❤️', '🔥', '😂', '😮', '😢'];
const toggleReaction = (messageId, emoji) => {
chatStore.toggleReaction(messageId, emoji);
showEmojiPicker.value = null;
};
const getReactionCount = (reactions, emoji) => {
return reactions?.filter(r => r.emoji === emoji).length || 0;
};
const hasUserReacted = (reactions, emoji) => {
return reactions?.some(r => r.emoji === emoji && r.wallet_address === walletAddress.value);
};
const getUniqueEmojis = (reactions) => {
if (!reactions) return [];
return [...new Set(reactions.map(r => r.emoji))];
};
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
};
onUpdated(() => {
scrollToBottom();
});
const send = () => {
if (!newMessage.value.trim()) return;
chatStore.sendMessage(newMessage.value);
newMessage.value = '';
nextTick(scrollToBottom);
};
const formatTime = (isoString) => {
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
</script>
<template>
<div class="flex-1 flex flex-col h-full bg-black/40 backdrop-blur-sm relative z-10">
<!-- Header -->
<div class="h-12 border-b border-white/5 flex items-center px-4 shadow-sm bg-crypto-panel/50">
<div class="text-lg font-bold text-white"># {{ currentChannel }}</div>
</div>
<!-- Messages -->
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth">
<!-- Beginning of conversation marker -->
<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">
<Hash size="32" />
</div>
<h2 class="text-2xl font-bold text-white mb-1">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>
</div>
<div v-for="(msg, index) in currentMessages" :key="msg.id"
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
>
<!-- Avatar (only if first message in group) -->
<div class="w-10 flex-shrink-0">
<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="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-crypto-panel'"
>
{{ msg.username.substring(0, 2).toUpperCase() }}
</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">
{{ formatTime(msg.timestamp) }}
</div>
</div>
<!-- Content -->
<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">
<span :class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']">
{{ msg.username }}
</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>
</div>
<div class="text-gray-100 text-sm leading-relaxed break-words">
{{ msg.content }}
</div>
<!-- Reactions Display -->
<div v-if="msg.reactions && msg.reactions.length > 0" class="flex flex-wrap gap-1 mt-1.5">
<button
v-for="emoji in getUniqueEmojis(msg.reactions)"
:key="emoji"
@click="toggleReaction(msg.id, emoji)"
:class="['flex items-center gap-1.5 px-2 py-0.5 rounded-lg text-xs border transition-all animate-pop-in',
hasUserReacted(msg.reactions, emoji)
? 'bg-violet-500/20 border-violet-500/50 text-violet-300'
: 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10']"
>
<span>{{ emoji }}</span>
<span class="font-bold">{{ getReactionCount(msg.reactions, emoji) }}</span>
</button>
</div>
</div>
<!-- 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">
<button
@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"
title="Add Reaction"
>
<Smile size="16" />
</button>
<!-- 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">
<button
v-for="emoji in EMOJIS"
:key="emoji"
@click="toggleReaction(msg.id, emoji)"
class="hover:scale-125 transition-transform p-1 text-lg"
>
{{ emoji }}
</button>
</div>
</div>
</div>
</div>
<!-- Input -->
<div class="p-4 bg-crypto-panel/80 border-t border-white/5">
<div class="relative">
<input
v-model="newMessage"
@keyup.enter="send"
type="text"
: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"
/>
<button
@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"
>
<Send size="20" />
</button>
</div>
</div>
</div>
</template>
<style scoped>
.animate-pop-in {
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes pop-in {
0% { transform: scale(0.5); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.animate-fade-in-up {
animation: fade-in-up 0.2s ease-out;
}
@keyframes fade-in-up {
0% { transform: translateY(10px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
</style>