chore: set up pre-commit hooks and fix linting (Task #181)

This commit is contained in:
2026-01-13 23:27:33 +01:00
parent ed62ac0641
commit 2553d087a0
17 changed files with 1971 additions and 164 deletions

4
.husky/pre-commit Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -4,17 +4,20 @@
install:
cd client && npm install
cd server && npm install
pip install duckdb ruff
pip3 install duckdb ruff --break-system-packages
# Development
dev:
docker compose up --build
# Linting
# Linting & Testing
lint:
cd client && npm run lint || true
cd client && npm run lint
ruff check tasks/
test:
cd server && npm test
# Docker Shell
shell:
docker compose run --rm dev-shell

1162
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .js, .vue --fix"
"lint": "eslint . --ext .js,.vue --fix"
},
"dependencies": {
"@solana/web3.js": "^1.98.4",

View File

@@ -7,7 +7,7 @@ import ChatLayout from './components/ChatLayout.vue';
const chatStore = useChatStore();
const videoRef = ref(null);
const handleMuteToggle = (isMuted) => {
const handleMuteToggle = () => {
if (videoRef.value) {
// Note: YouTube iframe API would be needed for true control,
// but for a simple background video loop, we can't easily unmute a background iframe without user interaction policies.
@@ -27,15 +27,18 @@ const handleMuteToggle = (isMuted) => {
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
></iframe>
/>
<!-- Overlay gradient -->
<div class="absolute inset-0 bg-crypto-dark/60 backdrop-blur-[2px]"></div>
<div class="absolute inset-0 bg-crypto-dark/60 backdrop-blur-[2px]" />
</div>
<!-- Content -->
<div class="relative z-10 h-full">
<WalletConnect v-if="!chatStore.isConnected" />
<ChatLayout v-else @toggleMute="handleMuteToggle" />
<ChatLayout
v-else
@toggle-mute="handleMuteToggle"
/>
</div>
</div>
</template>

View File

@@ -3,9 +3,8 @@ import { useChatStore } from '../stores/chat';
import { storeToRefs } from 'pinia';
import MessageList from './MessageList.vue';
import UserList from './UserList.vue';
import UserList from './UserList.vue';
import MusicPlayer from './MusicPlayer.vue';
import { Hash, Volume2, VolumeX, Settings, X, Coins, Menu, User } from 'lucide-vue-next';
import { Hash, Volume2, VolumeX, Settings, X, Menu, User } from 'lucide-vue-next';
import { ref } from 'vue';
const showProfile = ref(false);
@@ -40,16 +39,24 @@ const saveSettings = () => {
<!-- 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>
@click="showMobileMenu = false"
/>
<!-- Settings Modal -->
<div v-if="showSettings" class="fixed 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-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">
<h2 class="text-xl font-bold text-white">
Profile Settings
</h2>
<button
class="text-gray-400 hover:text-white transition-colors"
@click="showSettings = false"
>
<X size="24" />
</button>
</div>
@@ -61,7 +68,7 @@ const saveSettings = () => {
type="text"
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>
@@ -72,14 +79,14 @@ const saveSettings = () => {
</div>
<div class="p-6 border-t border-white/5 flex gap-3">
<button
@click="showSettings = 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"
@click="showSettings = false"
>
Cancel
</button>
<button
@click="saveSettings"
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"
@click="saveSettings"
>
Save Changes
</button>
@@ -95,32 +102,54 @@ const saveSettings = () => {
]"
>
<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">
<VolumeX v-if="isMuted" size="18" />
<Volume2 v-else size="18" />
<h1 class="font-bold text-white truncate">
Plexus Server
</h1>
<button
class="text-gray-400 hover:text-gray-200 transition-colors"
@click="toggleMute"
>
<VolumeX
v-if="isMuted"
size="18"
/>
<Volume2
v-else
size="18"
/>
</button>
</div>
<div class="flex-1 overflow-y-auto py-3 space-y-0.5 px-2">
<!-- Profile Link -->
<button
@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',
showProfile && selectedProfileAddress === walletAddress ? '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']"
@click="selectedProfileAddress = walletAddress; showProfile = true; showMobileMenu = false"
>
<User size="18" class="text-violet-400" />
<User
size="18"
class="text-violet-400"
/>
<span class="text-sm font-medium">My Profile</span>
</button>
<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 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); showProfile = false; showMobileMenu = false"
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group',
currentChannel === channel.id && !showProfile ? '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']"
@click="chatStore.setChannel(channel.id); showProfile = false; showMobileMenu = false"
>
<Hash size="18" :class="currentChannel === channel.id && !showProfile ? '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>
</button>
</div>
@@ -130,18 +159,28 @@ const saveSettings = () => {
<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">
<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="relative">
<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-discord-black 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 class="flex-1 min-w-0">
<div class="text-xs font-bold text-white truncate">{{ username }}</div>
<div class="text-[10px] text-gray-400 truncate">#{{ walletAddress?.slice(-4) }}</div>
<div class="text-xs font-bold text-white truncate">
{{ username }}
</div>
<div class="text-[10px] text-gray-400 truncate">
#{{ walletAddress?.slice(-4) }}
</div>
</div>
<Settings size="14" class="text-gray-400 group-hover:text-gray-200" />
<Settings
size="14"
class="text-gray-400 group-hover:text-gray-200"
/>
</div>
</div>
</div>
@@ -151,19 +190,28 @@ const saveSettings = () => {
<!-- Header -->
<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"
class="md:hidden mr-3 text-gray-400 hover:text-white transition-colors"
@click="showMobileMenu = true"
>
<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">{{ showProfile ? (selectedProfileAddress === walletAddress ? 'My Profile' : 'User Profile') : currentChannel }}</span>
</div>
<div class="flex-1 flex overflow-hidden">
<div class="flex-1 flex flex-col relative overflow-hidden">
<UserProfile v-if="showProfile" :address="selectedProfileAddress" />
<MessageList v-else @view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }" />
<UserProfile
v-if="showProfile"
:address="selectedProfileAddress"
/>
<MessageList
v-else
@view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }"
/>
</div>
<!-- Member List (Discord Style) -->

View File

@@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -59,45 +59,64 @@ const emit = defineEmits(['view-profile']);
<!-- 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" />
<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 custom-scrollbar">
<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-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>
<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 || msg.tempId"
<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"
@click="emit('view-profile', 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 cursor-pointer hover:opacity-80 transition-opacity"
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-discord-sidebar'"
@click="emit('view-profile', msg.walletAddress)"
>
{{ 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">
<div
v-else
class="w-10 text-[10px] text-crypto-muted opacity-0 group-hover:opacity-100 text-right pr-2 pt-1.5 transition-opacity"
>
{{ formatTime(msg.timestamp) }}
</div>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress" class="flex items-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
@click="emit('view-profile', msg.walletAddress)"
:class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']"
@click="emit('view-profile', msg.walletAddress)"
>
{{ msg.username }}
</span>
@@ -113,7 +132,7 @@ const emit = defineEmits(['view-profile']);
'led-red': msg.status === 'failed'
}"
:title="msg.status"
></div>
/>
</div>
<div :class="['text-sm leading-relaxed break-words', msg.status === 'failed' ? 'text-status-failed line-through opacity-60' : 'text-gray-100']">
@@ -121,15 +140,18 @@ const emit = defineEmits(['view-profile']);
</div>
<!-- Reactions Display -->
<div v-if="msg.reactions && msg.reactions.length > 0" class="flex flex-wrap gap-1 mt-1.5">
<div
v-if="msg.reactions && msg.reactions.length > 0"
class="flex flex-wrap gap-1 mt-1.5"
>
<button
v-for="emoji in getUniqueEmojis(msg.reactions)"
:key="emoji"
@click="toggleReaction(msg.id, emoji)"
:class="['flex items-center gap-1.5 px-2 py-0.5 rounded-lg text-xs border transition-all animate-pop-in',
hasUserReacted(msg.reactions, emoji)
? 'bg-violet-500/20 border-violet-500/50 text-violet-300'
: 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10']"
hasUserReacted(msg.reactions, emoji)
? 'bg-violet-500/20 border-violet-500/50 text-violet-300'
: 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10']"
@click="toggleReaction(msg.id, emoji)"
>
<span>{{ emoji }}</span>
<span class="font-bold">{{ getReactionCount(msg.reactions, emoji) }}</span>
@@ -138,22 +160,28 @@ const emit = defineEmits(['view-profile']);
</div>
<!-- Hover Actions -->
<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">
<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"
title="Add Reaction"
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
>
<Smile size="16" />
</button>
<!-- Emoji Picker Popover -->
<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">
<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"
@click="toggleReaction(msg.id, emoji)"
class="hover:scale-125 transition-transform p-1 text-lg"
@click="toggleReaction(msg.id, emoji)"
>
{{ emoji }}
</button>
@@ -167,27 +195,27 @@ const emit = defineEmits(['view-profile']);
<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}`"
type="text"
:placeholder="`Message #${currentChannel}`"
class="w-full bg-transparent text-white placeholder-gray-500 py-3 pl-4 pr-12 focus:outline-none"
/>
@keyup.enter="send"
>
<button
@click="send"
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-violet-400 transition-colors"
@click="send"
>
<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 class="led led-orange w-1.5 h-1.5" /> 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 class="led led-green w-1.5 h-1.5" /> 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 class="led led-red w-1.5 h-1.5" /> Failed
</div>
</div>
</div>

View File

@@ -51,41 +51,66 @@ const updateVolume = () => {
class="hidden"
:src="`https://www.youtube.com/embed/${LOFI_VIDEOS[currentVideoIndex]}?enablejsapi=1&autoplay=0&controls=0&disablekb=1&fs=0&modestbranding=1&iv_load_policy=3`"
frameborder="0"
></iframe>
/>
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg animate-pulse-slow">
<Music size="24" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-white truncate">Lofi Radio</div>
<div class="text-[10px] text-crypto-muted uppercase tracking-wider">Chilling in the Nebula</div>
<div class="text-sm font-bold text-white truncate">
Lofi Radio
</div>
<div class="text-[10px] text-crypto-muted uppercase tracking-wider">
Chilling in the Nebula
</div>
</div>
<div class="flex items-center gap-2">
<button @click="togglePlay" class="p-2 bg-white/10 hover:bg-white/20 rounded-full text-white transition-all">
<Pause v-if="isPlaying" size="18" />
<Play v-else size="18" />
<button
class="p-2 bg-white/10 hover:bg-white/20 rounded-full text-white transition-all"
@click="togglePlay"
>
<Pause
v-if="isPlaying"
size="18"
/>
<Play
v-else
size="18"
/>
</button>
<button @click="nextTrack" class="p-2 hover:bg-white/10 rounded-full text-gray-400 hover:text-white transition-all">
<button
class="p-2 hover:bg-white/10 rounded-full text-gray-400 hover:text-white transition-all"
@click="nextTrack"
>
<SkipForward size="18" />
</button>
</div>
</div>
<div class="mt-4 flex items-center gap-3">
<button @click="toggleMute" class="text-gray-400 hover:text-white transition-colors">
<VolumeX v-if="isMuted || volume == 0" size="16" />
<Volume2 v-else size="16" />
<button
class="text-gray-400 hover:text-white transition-colors"
@click="toggleMute"
>
<VolumeX
v-if="isMuted || volume == 0"
size="16"
/>
<Volume2
v-else
size="16"
/>
</button>
<input
v-model="volume"
@input="updateVolume"
type="range"
type="range"
min="0"
max="100"
class="flex-1 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-indigo-500"
/>
class="flex-1 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-indigo-500"
@input="updateVolume"
>
</div>
</div>
</template>

View File

@@ -13,19 +13,21 @@ const emit = defineEmits(['view-profile']);
<div class="flex-1 overflow-y-auto p-3 space-y-6">
<!-- Online Users -->
<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
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"
@click="emit('view-profile', user.wallet_address)"
>
<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">
{{ user.username.substring(0, 2).toUpperCase() }}
</div>
<div class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-[#2b2d31] rounded-full"></div>
<div class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-[#2b2d31] rounded-full" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-300 truncate group-hover:text-white transition-colors">
@@ -38,19 +40,21 @@ const emit = defineEmits(['view-profile']);
<!-- Offline Users -->
<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
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"
@click="emit('view-profile', user.wallet_address)"
>
<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">
{{ user.username.substring(0, 2).toUpperCase() }}
</div>
<div class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-gray-600 border-2 border-[#2b2d31] rounded-full"></div>
<div class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-gray-600 border-2 border-[#2b2d31] rounded-full" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-500 truncate group-hover:text-gray-400">

View File

@@ -2,7 +2,7 @@
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';
import { MessageSquare, Calendar, Edit3, Send, X } from 'lucide-vue-next';
const props = defineProps({
address: {
@@ -12,7 +12,7 @@ const props = defineProps({
});
const chatStore = useChatStore();
const { profileUser, profilePosts, isProfileLoading, walletAddress, username } = storeToRefs(chatStore);
const { profileUser, profilePosts, isProfileLoading, walletAddress } = storeToRefs(chatStore);
const isEditing = ref(false);
const editBio = ref('');
@@ -51,11 +51,17 @@ const formatTime = (isoString) => {
<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
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 v-else-if="profileUser" class="flex flex-col">
<div
v-else-if="profileUser"
class="flex flex-col"
>
<!-- Banner -->
<div
class="h-48 w-full relative transition-colors duration-500"
@@ -72,13 +78,17 @@ const formatTime = (isoString) => {
<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>
<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"
@click="startEditing"
>
<Edit3 size="16" /> Edit Profile
</button>
@@ -98,20 +108,26 @@ const formatTime = (isoString) => {
<!-- 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
<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">
<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"
@click="submitPost"
>
Post <Send size="16" />
</button>
@@ -120,7 +136,10 @@ const formatTime = (isoString) => {
<!-- Posts List -->
<div class="space-y-6">
<div v-if="profilePosts.length === 0" class="text-center py-12 text-gray-500 italic">
<div
v-if="profilePosts.length === 0"
class="text-center py-12 text-gray-500 italic"
>
No posts yet.
</div>
<div
@@ -134,8 +153,12 @@ const formatTime = (isoString) => {
{{ 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 class="font-bold text-white">
{{ profileUser.username }}
</div>
<div class="text-xs text-gray-500">
{{ formatTime(post.timestamp) }}
</div>
</div>
</div>
</div>
@@ -148,11 +171,19 @@ const formatTime = (isoString) => {
</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
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">
<h2 class="text-xl font-bold text-white">
Edit Profile
</h2>
<button
class="text-gray-400 hover:text-white transition-colors"
@click="isEditing = false"
>
<X size="24" />
</button>
</div>
@@ -163,7 +194,7 @@ const formatTime = (isoString) => {
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>
@@ -172,25 +203,25 @@ const formatTime = (isoString) => {
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"
@click="isEditing = false"
>
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"
@click="saveProfile"
>
Save Changes
</button>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { ref } from 'vue';
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
@@ -42,19 +42,28 @@ const connectWallet = async () => {
<template>
<div class="flex flex-col items-center justify-center h-screen bg-black/50 backdrop-blur-sm">
<div class="p-8 bg-crypto-panel rounded-xl shadow-2xl border border-crypto-accent/20 text-center max-w-md w-full">
<h1 class="text-3xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-600 text-transparent bg-clip-text">Crypto Chat</h1>
<p class="text-crypto-muted mb-8">Connect your wallet to join the conversation.</p>
<h1 class="text-3xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-600 text-transparent bg-clip-text">
Crypto Chat
</h1>
<p class="text-crypto-muted mb-8">
Connect your wallet to join the conversation.
</p>
<button
@click="connectWallet"
:disabled="isConnecting"
:disabled="isConnecting"
class="w-full py-3 px-6 bg-crypto-accent hover:bg-violet-600 text-white rounded-lg font-semibold transition-all transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@click="connectWallet"
>
<span v-if="isConnecting">Connecting...</span>
<span v-else>Connect Phantom Wallet</span>
</button>
<p v-if="error" class="mt-4 text-red-400 text-sm">{{ error }}</p>
<p
v-if="error"
class="mt-4 text-red-400 text-sm"
>
{{ error }}
</p>
</div>
</div>
</template>

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { io } from 'socket.io-client';
import { ref, computed, onMounted } from 'vue';
import { ref, computed } from 'vue';
import Cookies from 'js-cookie';
export const useChatStore = defineStore('chat', () => {

520
package-lock.json generated Normal file
View File

@@ -0,0 +1,520 @@
{
"name": "plexus",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"husky": "^9.1.7",
"lint-staged": "^16.2.7"
}
},
"node_modules/ansi-escapes": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^7.1.0",
"string-width": "^8.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true,
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/lint-staged": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
"integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^14.0.2",
"listr2": "^9.0.5",
"micromatch": "^4.0.8",
"nano-spawn": "^2.0.0",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.8.1"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/listr2": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nano-spawn": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
"integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"license": "MIT",
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slice-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-width": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"devDependencies": {
"husky": "^9.1.7",
"lint-staged": "^16.2.7"
},
"scripts": {
"prepare": "husky"
},
"lint-staged": {
"*": "make lint test"
}
}

View File

@@ -1,6 +1,7 @@
import argparse
import sys
from db import init_db, add_task, list_tasks, update_task, delete_task
from db import add_task, delete_task, init_db, list_tasks, update_task
def main():
init_db()

View File

@@ -1,5 +1,5 @@
import duckdb
import os
DB_PATH = "tasks/tasks.duckdb"