feat: implement premium UI, mobile responsiveness, and message status LEDs

This commit is contained in:
2026-01-13 23:21:55 +01:00
parent 7955d88018
commit bd36b4fda8
6 changed files with 199 additions and 96 deletions

View File

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

View File

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

View File

@@ -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]) {

View File

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

View File

@@ -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)' },
} }
} }
}, },

View File

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