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 MusicPlayer from './MusicPlayer.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';
|
||||
|
||||
const showTokenCreator = ref(false);
|
||||
const showMobileMenu = ref(false);
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
||||
@@ -34,10 +35,17 @@ const saveSettings = () => {
|
||||
</script>
|
||||
|
||||
<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 -->
|
||||
<div v-if="showSettings" class="absolute 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 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-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">
|
||||
<h2 class="text-xl font-bold text-white">Profile Settings</h2>
|
||||
<button @click="showSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
@@ -50,13 +58,13 @@ const saveSettings = () => {
|
||||
<input
|
||||
v-model="newUsername"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,7 +87,12 @@ const saveSettings = () => {
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<h1 class="font-bold text-white truncate">Plexus Server</h1>
|
||||
<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">
|
||||
<!-- Token Creator Link -->
|
||||
<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',
|
||||
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 v-for="channel in channels" :key="channel.id">
|
||||
<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',
|
||||
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>
|
||||
|
||||
<!-- Music Player & Profile -->
|
||||
<div class="bg-[#232428] p-2 space-y-2">
|
||||
<div class="bg-discord-black p-2 space-y-2">
|
||||
<MusicPlayer />
|
||||
|
||||
<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">
|
||||
{{ username?.substring(0, 2).toUpperCase() }}
|
||||
</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 class="flex-1 min-w-0">
|
||||
<div class="text-xs font-bold text-white truncate">{{ username }}</div>
|
||||
@@ -133,9 +146,15 @@ const saveSettings = () => {
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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" />
|
||||
<span class="font-bold text-white mr-4">{{ showTokenCreator ? 'Token Creator' : currentChannel }}</span>
|
||||
</div>
|
||||
@@ -147,7 +166,7 @@ const saveSettings = () => {
|
||||
</div>
|
||||
|
||||
<!-- 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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,33 +53,36 @@ const formatTime = (isoString) => {
|
||||
</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 class="flex-1 flex flex-col h-full bg-discord-dark relative z-10">
|
||||
<!-- Header (Desktop only, mobile header is in ChatLayout) -->
|
||||
<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-sm font-bold text-white flex items-center gap-2">
|
||||
<Hash size="18" class="text-gray-400" />
|
||||
{{ currentChannel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">Welcome to #{{ currentChannel }}!</h2>
|
||||
<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 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"
|
||||
>
|
||||
<!-- 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'"
|
||||
: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 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) }}
|
||||
@@ -88,17 +91,26 @@ const formatTime = (isoString) => {
|
||||
|
||||
<!-- 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">
|
||||
<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']">
|
||||
{{ 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>
|
||||
|
||||
<!-- 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 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 }}
|
||||
</div>
|
||||
|
||||
@@ -120,7 +132,7 @@ const formatTime = (isoString) => {
|
||||
</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">
|
||||
<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
|
||||
@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"
|
||||
@@ -130,7 +142,7 @@ const formatTime = (isoString) => {
|
||||
</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">
|
||||
<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
|
||||
v-for="emoji in EMOJIS"
|
||||
:key="emoji"
|
||||
@@ -145,22 +157,33 @@ const formatTime = (isoString) => {
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="p-4 bg-crypto-panel/80 border-t border-white/5">
|
||||
<div class="relative">
|
||||
<div class="p-4 bg-discord-dark">
|
||||
<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
|
||||
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"
|
||||
class="w-full bg-transparent text-white placeholder-gray-500 py-3 pl-4 pr-12 focus:outline-none"
|
||||
/>
|
||||
<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"
|
||||
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" />
|
||||
</button>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -45,8 +45,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!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) => {
|
||||
@@ -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();
|
||||
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) {
|
||||
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...');
|
||||
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
||||
|
||||
socket.value.emit('sendMessage', {
|
||||
channelId: currentChannel.value,
|
||||
walletAddress: walletAddress.value,
|
||||
content,
|
||||
txId: mockTxId
|
||||
});
|
||||
setTimeout(() => {
|
||||
// Randomly fail 5% of the time for demonstration
|
||||
const failed = Math.random() < 0.05;
|
||||
|
||||
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) {
|
||||
@@ -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) {
|
||||
currentChannel.value = channelId;
|
||||
if (!messages.value[channelId]) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-crypto-dark text-crypto-text overflow-hidden;
|
||||
@apply bg-discord-dark text-crypto-text overflow-hidden antialiased;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
@@ -12,21 +12,48 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-crypto-dark;
|
||||
@apply bg-discord-black;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-crypto-panel rounded;
|
||||
@apply bg-discord-sidebar rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-crypto-muted;
|
||||
@apply bg-white/10;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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-text': '#e2e8f0',
|
||||
'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: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
sans: ['Inter', 'sans-serif', 'system-ui'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
||||
'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: {
|
||||
fadeInUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||
'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 os
|
||||
|
||||
DB_PATH = "data/tasks.duckdb"
|
||||
DB_PATH = "tasks/tasks.duckdb"
|
||||
|
||||
def init_db():
|
||||
if not os.path.exists("data"):
|
||||
os.makedirs("data")
|
||||
|
||||
con = duckdb.connect(DB_PATH)
|
||||
con.execute("""
|
||||
|
||||
Reference in New Issue
Block a user