chore: set up pre-commit hooks and fix linting (Task #181)
This commit is contained in:
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
9
Makefile
9
Makefile
@@ -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
1162
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
520
package-lock.json
generated
Normal 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
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.7"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "make lint test"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import duckdb
|
||||
import os
|
||||
|
||||
DB_PATH = "tasks/tasks.duckdb"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user