This commit is contained in:
@@ -3,12 +3,13 @@ import { useChatStore } from '../stores/chat';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import MessageList from './MessageList.vue';
|
import MessageList from './MessageList.vue';
|
||||||
import UserList from './UserList.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 { Hash, Volume2, VolumeX, Settings, X, Coins, Menu, User } 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 showProfile = ref(false);
|
||||||
|
const selectedProfileAddress = ref(null);
|
||||||
const showMobileMenu = ref(false);
|
const showMobileMenu = ref(false);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@@ -102,24 +103,24 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 -->
|
<!-- Profile Link -->
|
||||||
<button
|
<button
|
||||||
@click="showTokenCreator = true; showMobileMenu = false"
|
@click="selectedProfileAddress = walletAddress; showProfile = 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']"
|
showProfile && selectedProfileAddress === walletAddress ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||||
>
|
>
|
||||||
<Coins size="18" class="text-violet-400" />
|
<User size="18" class="text-violet-400" />
|
||||||
<span class="text-sm font-medium">Token Creator</span>
|
<span class="text-sm font-medium">My Profile</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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; showMobileMenu = false"
|
@click="chatStore.setChannel(channel.id); showProfile = 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 && !showProfile ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||||
>
|
>
|
||||||
<Hash size="18" :class="currentChannel === channel.id && !showTokenCreator ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'" />
|
<Hash size="18" :class="currentChannel === channel.id && !showProfile ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'" />
|
||||||
<span class="text-sm font-medium">{{ channel.name }}</span>
|
<span class="text-sm font-medium">{{ channel.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,18 +157,18 @@ const saveSettings = () => {
|
|||||||
<Menu size="24" />
|
<Menu size="24" />
|
||||||
</button>
|
</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">{{ showProfile ? (selectedProfileAddress === walletAddress ? 'My Profile' : 'User Profile') : currentChannel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<div class="flex-1 flex flex-col relative overflow-hidden">
|
<div class="flex-1 flex flex-col relative overflow-hidden">
|
||||||
<TokenCreator v-if="showTokenCreator" @back="showTokenCreator = false" />
|
<UserProfile v-if="showProfile" :address="selectedProfileAddress" />
|
||||||
<MessageList v-else />
|
<MessageList v-else @view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Member List (Discord Style) -->
|
<!-- Member List (Discord Style) -->
|
||||||
<div class="w-60 bg-discord-sidebar border-l border-black/20 hidden xl:flex flex-col">
|
<div class="w-60 bg-discord-sidebar border-l border-black/20 hidden xl:flex flex-col">
|
||||||
<UserList />
|
<UserList @view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const send = () => {
|
|||||||
const formatTime = (isoString) => {
|
const formatTime = (isoString) => {
|
||||||
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['view-profile']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -79,7 +81,8 @@ const formatTime = (isoString) => {
|
|||||||
<!-- 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"
|
@click="emit('view-profile', 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 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-discord-sidebar'"
|
: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() }}
|
||||||
@@ -92,7 +95,10 @@ 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-center 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
|
||||||
|
@click="emit('view-profile', msg.walletAddress)"
|
||||||
|
:class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']"
|
||||||
|
>
|
||||||
{{ msg.username }}
|
{{ msg.username }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { storeToRefs } from 'pinia';
|
|||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
||||||
|
|
||||||
|
const emit = defineEmits(['view-profile']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -13,7 +15,12 @@ const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
|||||||
<div v-if="onlineUsers.length > 0">
|
<div v-if="onlineUsers.length > 0">
|
||||||
<h3 class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Online — {{ onlineUsers.length }}</h3>
|
<h3 class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Online — {{ onlineUsers.length }}</h3>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<div v-for="user in onlineUsers" :key="user.wallet_address" class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all">
|
<div
|
||||||
|
v-for="user in onlineUsers"
|
||||||
|
:key="user.wallet_address"
|
||||||
|
@click="emit('view-profile', user.wallet_address)"
|
||||||
|
class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all"
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center text-xs font-bold text-white shadow-sm">
|
<div class="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center text-xs font-bold text-white shadow-sm">
|
||||||
{{ user.username.substring(0, 2).toUpperCase() }}
|
{{ user.username.substring(0, 2).toUpperCase() }}
|
||||||
@@ -33,7 +40,12 @@ const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
|||||||
<div v-if="offlineUsers.length > 0">
|
<div v-if="offlineUsers.length > 0">
|
||||||
<h3 class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Offline — {{ offlineUsers.length }}</h3>
|
<h3 class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Offline — {{ offlineUsers.length }}</h3>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<div v-for="user in offlineUsers" :key="user.wallet_address" class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all opacity-60 hover:opacity-100">
|
<div
|
||||||
|
v-for="user in offlineUsers"
|
||||||
|
:key="user.wallet_address"
|
||||||
|
@click="emit('view-profile', user.wallet_address)"
|
||||||
|
class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all opacity-60 hover:opacity-100"
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="w-8 h-8 rounded-full bg-[#3f4147] flex items-center justify-center text-xs font-bold text-gray-500">
|
<div class="w-8 h-8 rounded-full bg-[#3f4147] flex items-center justify-center text-xs font-bold text-gray-500">
|
||||||
{{ user.username.substring(0, 2).toUpperCase() }}
|
{{ user.username.substring(0, 2).toUpperCase() }}
|
||||||
|
|||||||
201
client/src/components/UserProfile.vue
Normal file
201
client/src/components/UserProfile.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { useChatStore } from '../stores/chat';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { User, MessageSquare, Calendar, MapPin, Link as LinkIcon, Edit3, Send, X } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const { profileUser, profilePosts, isProfileLoading, walletAddress, username } = storeToRefs(chatStore);
|
||||||
|
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const editBio = ref('');
|
||||||
|
const editBannerColor = ref('');
|
||||||
|
const newPostContent = ref('');
|
||||||
|
|
||||||
|
const loadProfile = () => {
|
||||||
|
chatStore.getProfile(props.address);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadProfile);
|
||||||
|
watch(() => props.address, loadProfile);
|
||||||
|
|
||||||
|
const startEditing = () => {
|
||||||
|
editBio.value = profileUser.value.bio || '';
|
||||||
|
editBannerColor.value = profileUser.value.banner_color || '#6366f1';
|
||||||
|
isEditing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProfile = () => {
|
||||||
|
chatStore.updateProfile(editBio.value, editBannerColor.value);
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPost = () => {
|
||||||
|
if (!newPostContent.value.trim()) return;
|
||||||
|
chatStore.createPost(newPostContent.value);
|
||||||
|
newPostContent.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (isoString) => {
|
||||||
|
const date = new Date(isoString);
|
||||||
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-1 flex flex-col h-full bg-discord-dark overflow-y-auto custom-scrollbar">
|
||||||
|
<div v-if="isProfileLoading" class="flex-1 flex items-center justify-center">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-violet-500"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="profileUser" class="flex flex-col">
|
||||||
|
<!-- Banner -->
|
||||||
|
<div
|
||||||
|
class="h-48 w-full relative transition-colors duration-500"
|
||||||
|
:style="{ backgroundColor: profileUser.banner_color }"
|
||||||
|
>
|
||||||
|
<div class="absolute -bottom-16 left-8">
|
||||||
|
<div class="w-32 h-32 rounded-full border-8 border-discord-dark bg-violet-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl">
|
||||||
|
{{ profileUser.username?.substring(0, 2).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Info -->
|
||||||
|
<div class="mt-20 px-8 pb-8 border-b border-white/5">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-white">{{ profileUser.username }}</h1>
|
||||||
|
<p class="text-gray-400 font-mono text-sm mt-1">{{ profileUser.wallet_address }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="profileUser.wallet_address === walletAddress"
|
||||||
|
@click="startEditing"
|
||||||
|
class="px-4 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-full text-white text-sm font-medium transition-all flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Edit3 size="16" /> Edit Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-gray-200 text-lg leading-relaxed max-w-2xl">
|
||||||
|
{{ profileUser.bio || 'No bio yet...' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-wrap gap-4 text-gray-400 text-sm">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<Calendar size="16" /> Joined {{ new Date(profileUser.last_seen).toLocaleDateString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wall / Posts -->
|
||||||
|
<div class="p-8 max-w-3xl">
|
||||||
|
<h2 class="text-xl font-bold text-white mb-6 flex items-center gap-2">
|
||||||
|
<MessageSquare size="20" class="text-violet-400" /> Wall
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- New Post Input (only for owner) -->
|
||||||
|
<div v-if="profileUser.wallet_address === walletAddress" class="mb-8 bg-discord-sidebar/30 rounded-2xl p-4 border border-white/5">
|
||||||
|
<textarea
|
||||||
|
v-model="newPostContent"
|
||||||
|
placeholder="What's on your mind?"
|
||||||
|
class="w-full bg-transparent text-white placeholder-gray-500 resize-none focus:outline-none min-h-[100px]"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex justify-end mt-2 pt-2 border-t border-white/5">
|
||||||
|
<button
|
||||||
|
@click="submitPost"
|
||||||
|
class="px-6 py-2 bg-violet-600 hover:bg-violet-500 text-white rounded-full font-bold transition-all flex items-center gap-2 shadow-lg shadow-violet-600/20"
|
||||||
|
>
|
||||||
|
Post <Send size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Posts List -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div v-if="profilePosts.length === 0" class="text-center py-12 text-gray-500 italic">
|
||||||
|
No posts yet.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="post in profilePosts"
|
||||||
|
:key="post.id"
|
||||||
|
class="bg-discord-sidebar/20 rounded-2xl p-6 border border-white/5 hover:border-white/10 transition-all group"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-start mb-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-violet-600/20 flex items-center justify-center text-violet-400 font-bold">
|
||||||
|
{{ profileUser.username?.substring(0, 2).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-bold text-white">{{ profileUser.username }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ formatTime(post.timestamp) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-200 leading-relaxed">
|
||||||
|
{{ post.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div v-if="isEditing" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||||
|
<div class="bg-discord-sidebar border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl animate-pop-in">
|
||||||
|
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
||||||
|
<h2 class="text-xl font-bold text-white">Edit Profile</h2>
|
||||||
|
<button @click="isEditing = false" class="text-gray-400 hover:text-white transition-colors">
|
||||||
|
<X size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Bio</label>
|
||||||
|
<textarea
|
||||||
|
v-model="editBio"
|
||||||
|
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 min-h-[120px]"
|
||||||
|
placeholder="Tell us about yourself..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Banner Color</label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<input
|
||||||
|
v-model="editBannerColor"
|
||||||
|
type="color"
|
||||||
|
class="w-12 h-12 rounded-lg bg-transparent border-none cursor-pointer"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="editBannerColor"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 border-t border-white/5 flex gap-3">
|
||||||
|
<button
|
||||||
|
@click="isEditing = false"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-xl border border-white/10 text-white font-medium hover:bg-white/5 transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="saveProfile"
|
||||||
|
class="flex-1 px-4 py-2.5 rounded-xl bg-violet-600 text-white font-medium hover:bg-violet-500 shadow-lg shadow-violet-600/20 transition-all"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -15,6 +15,11 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const users = ref([]);
|
const users = ref([]);
|
||||||
const channels = ref([]);
|
const channels = ref([]);
|
||||||
|
|
||||||
|
// Profile state
|
||||||
|
const profileUser = ref(null);
|
||||||
|
const profilePosts = ref([]);
|
||||||
|
const isProfileLoading = ref(false);
|
||||||
|
|
||||||
const onlineUsers = computed(() => users.value.filter(u => u.online));
|
const onlineUsers = computed(() => users.value.filter(u => u.online));
|
||||||
const offlineUsers = computed(() => users.value.filter(u => !u.online));
|
const offlineUsers = computed(() => users.value.filter(u => !u.online));
|
||||||
|
|
||||||
@@ -74,6 +79,22 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.value.on('profileData', (data) => {
|
||||||
|
profileUser.value = data;
|
||||||
|
profilePosts.value = data.posts;
|
||||||
|
isProfileLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('profileUpdated', (data) => {
|
||||||
|
if (profileUser.value && profileUser.value.wallet_address === walletAddress.value) {
|
||||||
|
profileUser.value = { ...profileUser.value, ...data };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('postCreated', (post) => {
|
||||||
|
profilePosts.value = [post, ...profilePosts.value];
|
||||||
|
});
|
||||||
|
|
||||||
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
||||||
username.value = newName;
|
username.value = newName;
|
||||||
const savedAuth = Cookies.get('plexus_auth');
|
const savedAuth = Cookies.get('plexus_auth');
|
||||||
@@ -171,6 +192,29 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProfile(address) {
|
||||||
|
if (!socket.value) return;
|
||||||
|
isProfileLoading.value = true;
|
||||||
|
socket.value.emit('getProfile', address);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProfile(bio, bannerColor) {
|
||||||
|
if (!socket.value) return;
|
||||||
|
socket.value.emit('updateProfile', {
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
bio,
|
||||||
|
bannerColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPost(content) {
|
||||||
|
if (!socket.value || !content.trim()) return;
|
||||||
|
socket.value.emit('createPost', {
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setChannel(channelId) {
|
function setChannel(channelId) {
|
||||||
currentChannel.value = channelId;
|
currentChannel.value = channelId;
|
||||||
if (!messages.value[channelId]) {
|
if (!messages.value[channelId]) {
|
||||||
@@ -209,10 +253,16 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
onlineUsers,
|
onlineUsers,
|
||||||
offlineUsers,
|
offlineUsers,
|
||||||
currentMessages,
|
currentMessages,
|
||||||
|
profileUser,
|
||||||
|
profilePosts,
|
||||||
|
isProfileLoading,
|
||||||
connect,
|
connect,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
toggleReaction,
|
toggleReaction,
|
||||||
updateUsername,
|
updateUsername,
|
||||||
|
getProfile,
|
||||||
|
updateProfile,
|
||||||
|
createPost,
|
||||||
setChannel
|
setChannel
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
26
server/db.js
26
server/db.js
@@ -11,6 +11,8 @@ con.exec(`
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
wallet_address VARCHAR PRIMARY KEY,
|
wallet_address VARCHAR PRIMARY KEY,
|
||||||
username VARCHAR UNIQUE,
|
username VARCHAR UNIQUE,
|
||||||
|
bio VARCHAR DEFAULT '',
|
||||||
|
banner_color VARCHAR DEFAULT '#6366f1',
|
||||||
last_seen TIMESTAMP
|
last_seen TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -31,8 +33,17 @@ con.exec(`
|
|||||||
PRIMARY KEY (message_id, wallet_address, emoji),
|
PRIMARY KEY (message_id, wallet_address, emoji),
|
||||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
wallet_address VARCHAR,
|
||||||
|
content VARCHAR,
|
||||||
|
timestamp TIMESTAMP,
|
||||||
|
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE SEQUENCE IF NOT EXISTS seq_msg_id START 1;
|
CREATE SEQUENCE IF NOT EXISTS seq_msg_id START 1;
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS seq_post_id START 1;
|
||||||
|
|
||||||
-- Migration: Add tx_id to messages if it doesn't exist (for existing DBs)
|
-- Migration: Add tx_id to messages if it doesn't exist (for existing DBs)
|
||||||
PRAGMA table_info('messages');
|
PRAGMA table_info('messages');
|
||||||
@@ -46,7 +57,20 @@ con.exec(`
|
|||||||
if (!hasTxId) {
|
if (!hasTxId) {
|
||||||
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
||||||
if (err) console.error("Error adding tx_id column:", err);
|
if (err) console.error("Error adding tx_id column:", err);
|
||||||
else console.log("Added tx_id column to messages table");
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migration: Add bio and banner_color to users
|
||||||
|
con.all("PRAGMA table_info('users')", (err, rows) => {
|
||||||
|
if (err) return;
|
||||||
|
const hasBio = rows.some(r => r.name === 'bio');
|
||||||
|
if (!hasBio) {
|
||||||
|
con.run("ALTER TABLE users ADD COLUMN bio VARCHAR DEFAULT ''", (err) => {
|
||||||
|
if (err) console.error("Error adding bio column:", err);
|
||||||
|
});
|
||||||
|
con.run("ALTER TABLE users ADD COLUMN banner_color VARCHAR DEFAULT '#6366f1'", (err) => {
|
||||||
|
if (err) console.error("Error adding banner_color column:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,11 +51,13 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
// User exists, update last seen
|
// User exists, update last seen
|
||||||
|
const existingUsername = rows[0].username;
|
||||||
con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
||||||
if (err) return console.error("Prepare error:", err);
|
if (err) return console.error("Prepare error:", err);
|
||||||
uStmt.run(now, walletAddress, (err) => {
|
uStmt.run(now, walletAddress, (err) => {
|
||||||
uStmt.finalize();
|
uStmt.finalize();
|
||||||
if (err) console.error("Update error:", err);
|
if (err) console.error("Update error:", err);
|
||||||
|
socket.emit('usernameUpdated', { username: existingUsername });
|
||||||
broadcastUserList();
|
broadcastUserList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -77,6 +79,7 @@ io.on('connection', (socket) => {
|
|||||||
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
||||||
iStmt.finalize();
|
iStmt.finalize();
|
||||||
if (err) console.error("Insert error:", err);
|
if (err) console.error("Insert error:", err);
|
||||||
|
socket.emit('usernameUpdated', { username: finalUsername });
|
||||||
broadcastUserList();
|
broadcastUserList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -165,6 +168,61 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('getProfile', (walletAddress) => {
|
||||||
|
console.log(`Fetching profile for ${walletAddress}`);
|
||||||
|
con.prepare(`SELECT wallet_address, username, bio, banner_color, last_seen FROM users WHERE wallet_address = ?`, (err, stmt) => {
|
||||||
|
if (err) return socket.emit('error', { message: 'Database error' });
|
||||||
|
stmt.all(walletAddress, (err, rows) => {
|
||||||
|
stmt.finalize();
|
||||||
|
if (err || rows.length === 0) return socket.emit('error', { message: 'User not found' });
|
||||||
|
|
||||||
|
const user = rows[0];
|
||||||
|
// Fetch posts
|
||||||
|
con.prepare(`SELECT * FROM posts WHERE wallet_address = ? ORDER BY timestamp DESC LIMIT 50`, (err, pStmt) => {
|
||||||
|
if (err) return socket.emit('profileData', { ...user, posts: [] });
|
||||||
|
pStmt.all(walletAddress, (err, posts) => {
|
||||||
|
pStmt.finalize();
|
||||||
|
socket.emit('profileData', { ...user, posts: posts || [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('updateProfile', ({ walletAddress, bio, bannerColor }) => {
|
||||||
|
console.log(`Updating profile for ${walletAddress}`);
|
||||||
|
con.prepare(`UPDATE users SET bio = ?, banner_color = ? WHERE wallet_address = ?`, (err, stmt) => {
|
||||||
|
if (err) return socket.emit('error', { message: 'Database error' });
|
||||||
|
stmt.run(bio, bannerColor, walletAddress, (err) => {
|
||||||
|
stmt.finalize();
|
||||||
|
if (err) return socket.emit('error', { message: 'Failed to update profile' });
|
||||||
|
socket.emit('profileUpdated', { bio, bannerColor });
|
||||||
|
broadcastUserList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('createPost', ({ walletAddress, content }) => {
|
||||||
|
if (!content || content.trim() === '') return;
|
||||||
|
console.log(`New post from ${walletAddress}: ${content}`);
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
con.prepare(`INSERT INTO posts (id, wallet_address, content, timestamp) VALUES (nextval('seq_post_id'), ?, ?, ?)`, (err, stmt) => {
|
||||||
|
if (err) return socket.emit('error', { message: 'Database error' });
|
||||||
|
stmt.run(walletAddress, content, timestamp, (err) => {
|
||||||
|
stmt.finalize();
|
||||||
|
if (err) return socket.emit('error', { message: 'Failed to create post' });
|
||||||
|
|
||||||
|
// Fetch all posts to broadcast update or just emit the new one
|
||||||
|
// For simplicity, we'll just tell the user it was created
|
||||||
|
socket.emit('postCreated', { content, timestamp });
|
||||||
|
|
||||||
|
// If we want a live feed, we could broadcast to a "profile room"
|
||||||
|
// For now, the user can just refresh or we emit to them
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('toggleReaction', ({ messageId, walletAddress, emoji }) => {
|
socket.on('toggleReaction', ({ messageId, walletAddress, emoji }) => {
|
||||||
console.log(`Toggling reaction: ${emoji} on message ${messageId} by ${walletAddress}`);
|
console.log(`Toggling reaction: ${emoji} on message ${messageId} by ${walletAddress}`);
|
||||||
con.prepare(`SELECT * FROM reactions WHERE message_id = ? AND wallet_address = ? AND emoji = ?`, (err, stmt) => {
|
con.prepare(`SELECT * FROM reactions WHERE message_id = ? AND wallet_address = ? AND emoji = ?`, (err, stmt) => {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user