feat: implement user profiles and social walls (Tasks #171, #173, #174)

This commit is contained in:
2026-01-13 23:24:19 +01:00
parent 477f447b67
commit ed62ac0641
8 changed files with 372 additions and 20 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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() }}

View 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>

View File

@@ -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
}; };
}); });

View File

@@ -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
); );
@@ -32,7 +34,16 @@ con.exec(`
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);
}); });
} }
}); });

View File

@@ -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.