Compare commits
10 Commits
2faf2dd8dc
...
712f62f7ae
| Author | SHA1 | Date | |
|---|---|---|---|
| 712f62f7ae | |||
| 2553d087a0 | |||
| ed62ac0641 | |||
| 477f447b67 | |||
| bd36b4fda8 | |||
| 7955d88018 | |||
| 588a3500f0 | |||
| 1a59c3435d | |||
| 40dbe40b17 | |||
| 41046ad922 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,3 +36,4 @@ server/data/
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.pyc
|
||||
|
||||
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
|
||||
18
CHANGELOG.md
Normal file
18
CHANGELOG.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Professional documentation structure in `docs/`.
|
||||
- Internal task tracker using Python and DuckDB.
|
||||
- Centralized `Makefile` for project automation.
|
||||
- Dockerized development shell.
|
||||
- Unique username enforcement on the server.
|
||||
- Transaction simulation for messages, reactions, and username changes.
|
||||
|
||||
### Fixed
|
||||
- DuckDB parameter handling issue in the backend.
|
||||
- Reaction animations and persistence.
|
||||
- `.gitignore` consolidation.
|
||||
44
CONTRIBUTING.md
Normal file
44
CONTRIBUTING.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 🤝 Contributing to Plexus
|
||||
|
||||
We maintain high standards for code quality and documentation. Please follow this workflow for all contributions.
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
### 1. Pick a Task
|
||||
Before coding, ensure you are working on an assigned task.
|
||||
- List tasks: `make task-list`
|
||||
- Add a task: `make task-add title="Your Task"`
|
||||
- Mark as in-progress: (Update manually in `tasks/tasks.duckdb` or via CLI if implemented).
|
||||
|
||||
### 2. Implementation
|
||||
- Write clean, modular code.
|
||||
- Follow the existing design patterns (Vue 3 Composition API, Pinia, Socket.io).
|
||||
- Ensure UI changes are mobile-friendly and follow the Discord-inspired aesthetic.
|
||||
|
||||
### 3. Verification
|
||||
Before committing, you **must** verify your changes:
|
||||
- Run linting: `make lint`
|
||||
- Run tests: `make test`
|
||||
- Manual check: Verify the feature in the browser.
|
||||
|
||||
### 4. Commit
|
||||
We use **Husky** and **lint-staged** to enforce quality.
|
||||
- Your commit will fail if linting or tests do not pass.
|
||||
- Use descriptive commit messages (e.g., `feat: add user profiles`, `fix: message alignment`).
|
||||
|
||||
### 5. Documentation
|
||||
If your change affects the architecture, data model, or API:
|
||||
- Update the relevant file in `docs/`.
|
||||
- Ensure the root `README.md` is still accurate.
|
||||
|
||||
### 6. Finalize Task
|
||||
Mark the task as done:
|
||||
- `make task-done id=X`
|
||||
|
||||
## 🛠 Tooling
|
||||
- **Linting**: ESLint for JS/Vue, Ruff for Python.
|
||||
- **Testing**: Mocha for backend integration tests.
|
||||
- **Automation**: Use the `Makefile` for all common operations.
|
||||
|
||||
---
|
||||
Thank you for helping make Plexus better!
|
||||
25
Dockerfile.dev
Normal file
25
Dockerfile.dev
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:20-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
make \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install DuckDB
|
||||
RUN curl -L https://github.com/duckdb/duckdb/releases/download/v0.10.0/duckdb_cli-linux-amd64.zip -o duckdb.zip \
|
||||
&& apt-get update && apt-get install -y unzip \
|
||||
&& unzip duckdb.zip -d /usr/local/bin \
|
||||
&& rm duckdb.zip
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip3 install duckdb ruff --break-system-packages
|
||||
|
||||
# Default command
|
||||
CMD ["/bin/bash"]
|
||||
45
Makefile
Normal file
45
Makefile
Normal file
@@ -0,0 +1,45 @@
|
||||
.PHONY: install dev lint shell task-list task-add task-done task-update
|
||||
|
||||
# Installation
|
||||
install:
|
||||
cd client && npm install
|
||||
cd server && npm install
|
||||
pip3 install duckdb ruff --break-system-packages
|
||||
|
||||
# Development
|
||||
dev:
|
||||
docker compose up --build
|
||||
|
||||
# Linting & Testing
|
||||
lint:
|
||||
cd client && npm run lint
|
||||
ruff check tasks/
|
||||
|
||||
test:
|
||||
cd server && npm test
|
||||
|
||||
# Docker Shell
|
||||
shell:
|
||||
docker compose run --rm dev-shell
|
||||
|
||||
# Task Tracker Integration
|
||||
PYTHON=python3
|
||||
TASK_CLI=tasks/cli.py
|
||||
|
||||
task-list:
|
||||
$(PYTHON) $(TASK_CLI) list
|
||||
|
||||
task-add:
|
||||
$(PYTHON) $(TASK_CLI) add "$(title)"
|
||||
|
||||
task-done:
|
||||
$(PYTHON) $(TASK_CLI) done $(id)
|
||||
|
||||
task-update:
|
||||
$(PYTHON) $(TASK_CLI) update $(id) $(status)
|
||||
|
||||
task-delete:
|
||||
$(PYTHON) $(TASK_CLI) delete $(id)
|
||||
|
||||
task-filter:
|
||||
$(PYTHON) $(TASK_CLI) list --status $(status)
|
||||
66
README.md
Normal file
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 🌌 Plexus
|
||||
|
||||
Plexus is a premium, decentralized-inspired chat application built with **Vue 3**, **Node.js**, **Socket.io**, and **DuckDB**. It features a sleek Discord-style interface, real-time messaging, and social profiles with customizable "walls".
|
||||
|
||||

|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
- **💎 Premium UI**: Discord-inspired dark theme with glassmorphism and smooth animations.
|
||||
- **📱 Mobile Ready**: Fully responsive layout with adaptive components.
|
||||
- **⚡ Real-time Social**: Instant messaging, reactions, and user profiles with social walls.
|
||||
- **🚦 Transaction Lifecycle**: Simulated blockchain transaction states (Pending, Validated, Failed) with LED indicators.
|
||||
- **🛠 Robust Tooling**: Automated linting, testing, and a custom internal task tracker.
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3, Pinia, Tailwind CSS, Lucide Icons.
|
||||
- **Backend**: Node.js, Express, Socket.io.
|
||||
- **Database**: DuckDB (Fast, analytical, and serverless-friendly).
|
||||
- **Automation**: Makefile, Husky, Lint-staged, Ruff (Python), ESLint (JS).
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Explore our detailed documentation in the `docs/` directory:
|
||||
|
||||
- [🏗 Architecture](file:///home/sinan/Documents/repositories/local/plexus/docs/architecture.md) - High-level system design.
|
||||
- [📂 Structure](file:///home/sinan/Documents/repositories/local/plexus/docs/structure.md) - Directory and file organization.
|
||||
- [⚙️ Functions & API](file:///home/sinan/Documents/repositories/local/plexus/docs/functions.md) - Socket events and backend logic.
|
||||
- [📊 Data Model](file:///home/sinan/Documents/repositories/local/plexus/docs/data-model.md) - Database schema and migrations.
|
||||
- [📈 Scalability](file:///home/sinan/Documents/repositories/local/plexus/docs/scalability.md) - Future roadmap and scaling strategies.
|
||||
- [📝 Task Tracker](file:///home/sinan/Documents/repositories/local/plexus/docs/tasks.md) - How to use the internal task management tool.
|
||||
|
||||
## 🚦 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (v18+)
|
||||
- Python 3.10+
|
||||
- Docker (optional, for dev shell)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start the dev environment (Docker)
|
||||
make dev
|
||||
|
||||
# Or run locally
|
||||
cd server && npm run dev
|
||||
cd client && npm run dev
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We follow a strict development workflow. Please read [CONTRIBUTING.md](file:///home/sinan/Documents/repositories/local/plexus/CONTRIBUTING.md) before starting.
|
||||
|
||||
1. **Pick a Task**: Use `make task-list` to find something to work on.
|
||||
2. **Code**: Implement your changes.
|
||||
3. **Verify**: Run `make lint test` to ensure quality.
|
||||
4. **Commit**: Pre-commit hooks will automatically run linting and tests.
|
||||
5. **Document**: Update relevant docs if you add new features.
|
||||
|
||||
---
|
||||
Built with ❤️ by the Plexus Team.
|
||||
21
client/.eslintrc.cjs
Normal file
21
client/.eslintrc.cjs
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: [
|
||||
'vue',
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
};
|
||||
1162
client/package-lock.json
generated
1162
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .js,.vue --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
@@ -21,6 +22,8 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"postcss": "^8.4.33",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^7.2.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>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { storeToRefs } from 'pinia';
|
||||
import MessageList from './MessageList.vue';
|
||||
import UserList from './UserList.vue';
|
||||
import MusicPlayer from './MusicPlayer.vue';
|
||||
import TokenCreator from './TokenCreator.vue';
|
||||
import { Hash, Volume2, VolumeX, Settings, X, Coins } from 'lucide-vue-next';
|
||||
import { Hash, Volume2, VolumeX, Settings, X, Menu, User } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showTokenCreator = ref(false);
|
||||
const showProfile = ref(false);
|
||||
const selectedProfileAddress = ref(null);
|
||||
const showMobileMenu = ref(false);
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
||||
@@ -34,13 +35,28 @@ const saveSettings = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen w-full overflow-hidden relative">
|
||||
<div class="flex h-screen w-full overflow-hidden relative bg-discord-dark">
|
||||
<!-- Mobile Menu Overlay -->
|
||||
<div
|
||||
v-if="showMobileMenu"
|
||||
class="fixed inset-0 bg-black/60 z-40 md:hidden backdrop-blur-sm transition-opacity"
|
||||
@click="showMobileMenu = false"
|
||||
/>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div v-if="showSettings" class="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div class="bg-crypto-panel border border-white/10 rounded-2xl w-full max-w-md shadow-2xl animate-fade-in-up">
|
||||
<div
|
||||
v-if="showSettings"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
>
|
||||
<div class="bg-discord-sidebar border border-white/10 rounded-2xl w-full max-w-md shadow-2xl animate-pop-in">
|
||||
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-white">Profile Settings</h2>
|
||||
<button @click="showSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
<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>
|
||||
@@ -50,27 +66,27 @@ const saveSettings = () => {
|
||||
<input
|
||||
v-model="newUsername"
|
||||
type="text"
|
||||
class="w-full bg-crypto-dark border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
||||
class="w-full bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
||||
placeholder="Enter new username"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-crypto-muted uppercase tracking-wider mb-2">Wallet Address</label>
|
||||
<div class="w-full bg-crypto-dark/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
||||
<div class="w-full bg-discord-black/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
||||
{{ walletAddress }}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -79,76 +95,128 @@ const saveSettings = () => {
|
||||
</div>
|
||||
|
||||
<!-- Channels Sidebar -->
|
||||
<div class="w-64 bg-[#2b2d31] flex flex-col border-r border-black/20">
|
||||
<div
|
||||
:class="[
|
||||
'fixed inset-y-0 left-0 w-64 bg-discord-sidebar flex flex-col border-r border-black/20 z-50 transition-transform duration-300 md:relative md:translate-x-0',
|
||||
showMobileMenu ? 'translate-x-0' : '-translate-x-full'
|
||||
]"
|
||||
>
|
||||
<div class="h-12 px-4 flex items-center justify-between border-b border-black/20 shadow-sm">
|
||||
<h1 class="font-bold text-white truncate">Plexus Server</h1>
|
||||
<button @click="toggleMute" class="text-gray-400 hover:text-gray-200 transition-colors">
|
||||
<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">
|
||||
<!-- Token Creator Link -->
|
||||
<!-- Profile Link -->
|
||||
<button
|
||||
@click="showTokenCreator = true"
|
||||
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group mb-4',
|
||||
showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||
showProfile && selectedProfileAddress === walletAddress ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||
@click="selectedProfileAddress = walletAddress; showProfile = true; showMobileMenu = false"
|
||||
>
|
||||
<Coins size="18" class="text-violet-400" />
|
||||
<span class="text-sm font-medium">Token Creator</span>
|
||||
<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">
|
||||
<button
|
||||
@click="chatStore.setChannel(channel.id); showTokenCreator = false"
|
||||
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group',
|
||||
currentChannel === channel.id && !showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
||||
<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"
|
||||
>
|
||||
<Hash size="18" :class="currentChannel === channel.id && !showTokenCreator ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'" />
|
||||
<button
|
||||
: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']"
|
||||
@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'"
|
||||
/>
|
||||
<span class="text-sm font-medium">{{ channel.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Music Player & Profile -->
|
||||
<div class="bg-[#232428] p-2 space-y-2">
|
||||
<div class="bg-discord-black p-2 space-y-2">
|
||||
<MusicPlayer />
|
||||
|
||||
<div class="flex items-center gap-2 p-1.5 rounded-md hover:bg-[#35373c] transition-all group cursor-pointer" @click="showSettings = true">
|
||||
<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-[#232428] rounded-full"></div>
|
||||
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-discord-black rounded-full" />
|
||||
</div>
|
||||
<div 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>
|
||||
<Settings size="14" class="text-gray-400 group-hover:text-gray-200" />
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col bg-[#313338] relative overflow-hidden">
|
||||
<div class="flex-1 flex flex-col bg-discord-dark relative overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="h-12 px-4 flex items-center border-b border-black/20 shadow-sm bg-[#313338]/95 backdrop-blur-sm z-10">
|
||||
<Hash size="20" class="text-gray-400 mr-2" />
|
||||
<span class="font-bold text-white mr-4">{{ showTokenCreator ? 'Token Creator' : currentChannel }}</span>
|
||||
<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
|
||||
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"
|
||||
/>
|
||||
<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">
|
||||
<TokenCreator v-if="showTokenCreator" @back="showTokenCreator = false" />
|
||||
<MessageList v-else />
|
||||
<UserProfile
|
||||
v-if="showProfile"
|
||||
:address="selectedProfileAddress"
|
||||
/>
|
||||
<MessageList
|
||||
v-else
|
||||
@view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Member List (Discord Style) -->
|
||||
<div class="w-60 bg-[#2b2d31] border-l border-black/20 hidden lg:flex flex-col">
|
||||
<UserList />
|
||||
<div class="w-60 bg-discord-sidebar border-l border-black/20 hidden xl:flex flex-col">
|
||||
<UserList @view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -50,68 +50,108 @@ const send = () => {
|
||||
const formatTime = (isoString) => {
|
||||
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const emit = defineEmits(['view-profile']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col h-full bg-black/40 backdrop-blur-sm relative z-10">
|
||||
<!-- Header -->
|
||||
<div class="h-12 border-b border-white/5 flex items-center px-4 shadow-sm bg-crypto-panel/50">
|
||||
<div class="text-lg font-bold text-white"># {{ currentChannel }}</div>
|
||||
<div class="flex-1 flex flex-col h-full bg-discord-dark relative z-10">
|
||||
<!-- Header (Desktop only, mobile header is in ChatLayout) -->
|
||||
<div class="hidden md:flex h-12 border-b border-black/20 items-center px-4 shadow-sm bg-discord-dark/95 backdrop-blur-sm">
|
||||
<div class="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Hash
|
||||
size="18"
|
||||
class="text-gray-400"
|
||||
/>
|
||||
{{ currentChannel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth">
|
||||
<div
|
||||
ref="messagesContainer"
|
||||
class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth custom-scrollbar"
|
||||
>
|
||||
<!-- Beginning of conversation marker -->
|
||||
<div class="py-12 px-4 border-b border-white/5 mb-8">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center text-white mb-4 shadow-xl shadow-violet-600/20">
|
||||
<Hash size="32" />
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Welcome to #{{ currentChannel }}!</h2>
|
||||
<p class="text-gray-400 text-sm max-w-md">This is the very beginning of the <span class="text-violet-400 font-semibold">#{{ currentChannel }}</span> channel. Use this space to connect, share, and grow with the community.</p>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">
|
||||
Welcome to #{{ currentChannel }}!
|
||||
</h2>
|
||||
<p class="text-gray-400 text-base max-w-md leading-relaxed">
|
||||
This is the very beginning of the <span class="text-violet-400 font-semibold">#{{ currentChannel }}</span> channel. Use this space to connect, share, and grow with the community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(msg, index) in currentMessages" :key="msg.id"
|
||||
<div
|
||||
v-for="(msg, index) in currentMessages"
|
||||
:key="msg.id || msg.tempId"
|
||||
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
|
||||
>
|
||||
<!-- Avatar (only if first message in group) -->
|
||||
<div class="w-10 flex-shrink-0">
|
||||
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm shadow-lg border border-white/10 mt-1"
|
||||
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-crypto-panel'"
|
||||
<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() }}
|
||||
{{ 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-baseline gap-2 mb-0.5">
|
||||
<span :class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']">
|
||||
<div
|
||||
v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
||||
class="flex items-center gap-2 mb-0.5"
|
||||
>
|
||||
<span
|
||||
:class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']"
|
||||
@click="emit('view-profile', msg.walletAddress)"
|
||||
>
|
||||
{{ msg.username }}
|
||||
</span>
|
||||
<span v-if="msg.txId" class="text-[9px] text-crypto-muted font-mono bg-white/5 px-1.5 py-0.5 rounded border border-white/5">
|
||||
{{ msg.txId.slice(0, 8) }}
|
||||
</span>
|
||||
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
||||
|
||||
<!-- Status LED -->
|
||||
<div
|
||||
v-if="msg.status"
|
||||
class="led ml-1"
|
||||
:class="{
|
||||
'led-orange': msg.status === 'pending',
|
||||
'led-green': msg.status === 'validated',
|
||||
'led-red': msg.status === 'failed'
|
||||
}"
|
||||
:title="msg.status"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-100 text-sm leading-relaxed break-words">
|
||||
<div :class="['text-sm leading-relaxed break-words', msg.status === 'failed' ? 'text-status-failed line-through opacity-60' : 'text-gray-100']">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
|
||||
<!-- 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']"
|
||||
@click="toggleReaction(msg.id, emoji)"
|
||||
>
|
||||
<span>{{ emoji }}</span>
|
||||
<span class="font-bold">{{ getReactionCount(msg.reactions, emoji) }}</span>
|
||||
@@ -120,22 +160,28 @@ const formatTime = (isoString) => {
|
||||
</div>
|
||||
|
||||
<!-- Hover Actions -->
|
||||
<div class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-crypto-panel border border-white/10 rounded-lg p-1 shadow-xl">
|
||||
<div
|
||||
v-if="msg.status !== 'failed'"
|
||||
class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-discord-sidebar border border-white/10 rounded-lg p-1 shadow-xl"
|
||||
>
|
||||
<button
|
||||
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
|
||||
class="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-all"
|
||||
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-crypto-panel border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-fade-in-up">
|
||||
<div
|
||||
v-if="showEmojiPicker === msg.id"
|
||||
class="absolute right-0 bottom-full mb-2 bg-discord-sidebar border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-pop-in"
|
||||
>
|
||||
<button
|
||||
v-for="emoji in EMOJIS"
|
||||
:key="emoji"
|
||||
@click="toggleReaction(msg.id, emoji)"
|
||||
class="hover:scale-125 transition-transform p-1 text-lg"
|
||||
@click="toggleReaction(msg.id, emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
@@ -145,22 +191,33 @@ const formatTime = (isoString) => {
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="p-4 bg-crypto-panel/80 border-t border-white/5">
|
||||
<div class="relative">
|
||||
<div class="p-4 bg-discord-dark">
|
||||
<div class="relative bg-discord-sidebar/50 rounded-xl border border-white/5 p-1 transition-all focus-within:border-violet-500/30 focus-within:bg-discord-sidebar/80">
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="send"
|
||||
type="text"
|
||||
:placeholder="`Message #${currentChannel}`"
|
||||
class="w-full bg-crypto-dark/50 text-white placeholder-gray-500 rounded-lg py-3 pl-4 pr-12 focus:outline-none focus:ring-2 focus:ring-crypto-accent/50 border border-white/5 transition-all"
|
||||
/>
|
||||
class="w-full bg-transparent text-white placeholder-gray-500 py-3 pl-4 pr-12 focus:outline-none"
|
||||
@keyup.enter="send"
|
||||
>
|
||||
<button
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-violet-400 transition-colors"
|
||||
@click="send"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-white hover:bg-white/10 rounded-md transition-colors"
|
||||
>
|
||||
<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" /> 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" /> 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" /> Failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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"
|
||||
min="0"
|
||||
max="100"
|
||||
class="flex-1 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-indigo-500"
|
||||
/>
|
||||
@input="updateVolume"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Coins, ArrowLeft, Rocket, Shield, Zap } from 'lucide-vue-next';
|
||||
|
||||
const emit = defineEmits(['back']);
|
||||
|
||||
const tokenName = ref('');
|
||||
const tokenSymbol = ref('');
|
||||
const decimals = ref(9);
|
||||
const totalSupply = ref(1000000000);
|
||||
const isCreating = ref(false);
|
||||
const success = ref(false);
|
||||
|
||||
const createToken = async () => {
|
||||
if (!tokenName.value || !tokenSymbol.value) return;
|
||||
|
||||
isCreating.value = true;
|
||||
// Simulate blockchain interaction
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
isCreating.value = false;
|
||||
success.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col h-full bg-black/40 backdrop-blur-sm relative z-10 overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="h-12 border-b border-white/5 flex items-center px-4 shadow-sm bg-crypto-panel/50 sticky top-0 z-20">
|
||||
<button @click="emit('back')" class="mr-4 p-1.5 hover:bg-white/10 rounded-lg text-gray-400 hover:text-white transition-all">
|
||||
<ArrowLeft size="18" />
|
||||
</button>
|
||||
<div class="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Coins size="20" class="text-violet-400" />
|
||||
Token Creator
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-8 max-w-2xl mx-auto w-full">
|
||||
<div v-if="!success" class="space-y-8 animate-fade-in">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-white mb-2">Create your PLEXUS Token</h2>
|
||||
<p class="text-crypto-muted">Launch your own SPL token on Solana in seconds.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-crypto-muted uppercase tracking-wider ml-1">Token Name</label>
|
||||
<input
|
||||
v-model="tokenName"
|
||||
type="text"
|
||||
placeholder="e.g. Plexus Gold"
|
||||
class="w-full bg-crypto-dark/50 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"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-crypto-muted uppercase tracking-wider ml-1">Symbol</label>
|
||||
<input
|
||||
v-model="tokenSymbol"
|
||||
type="text"
|
||||
placeholder="e.g. PXG"
|
||||
class="w-full bg-crypto-dark/50 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"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-crypto-muted uppercase tracking-wider ml-1">Decimals</label>
|
||||
<input
|
||||
v-model="decimals"
|
||||
type="number"
|
||||
class="w-full bg-crypto-dark/50 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"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-bold text-crypto-muted uppercase tracking-wider ml-1">Total Supply</label>
|
||||
<input
|
||||
v-model="totalSupply"
|
||||
type="number"
|
||||
class="w-full bg-crypto-dark/50 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-violet-500/10 border border-violet-500/20 rounded-2xl p-6 space-y-4">
|
||||
<h3 class="font-bold text-white flex items-center gap-2">
|
||||
<Shield size="18" class="text-violet-400" />
|
||||
Security Features
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div class="flex items-center gap-3 text-gray-300">
|
||||
<Zap size="14" class="text-yellow-400" />
|
||||
Revoke Mint Authority
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-gray-300">
|
||||
<Zap size="14" class="text-yellow-400" />
|
||||
Revoke Freeze Authority
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="createToken"
|
||||
:disabled="isCreating || !tokenName || !tokenSymbol"
|
||||
class="w-full py-4 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white rounded-2xl font-bold shadow-xl shadow-violet-600/20 transition-all transform hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-3"
|
||||
>
|
||||
<Rocket v-if="!isCreating" size="20" />
|
||||
<span v-if="isCreating" class="animate-spin rounded-full h-5 w-5 border-2 border-white/30 border-t-white"></span>
|
||||
{{ isCreating ? 'Launching on Solana...' : 'Create Token' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 animate-bounce-in">
|
||||
<div class="w-24 h-24 bg-green-500/20 border border-green-500/50 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Rocket size="48" class="text-green-400" />
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">Token Launched!</h2>
|
||||
<p class="text-crypto-muted mb-8">Your token <span class="text-white font-bold">${{ tokenSymbol }}</span> has been successfully created on the Solana blockchain.</p>
|
||||
<button @click="success = false; tokenName = ''; tokenSymbol = ''" class="px-8 py-3 bg-white/10 hover:bg-white/20 text-white rounded-xl font-medium transition-all">
|
||||
Create Another
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
.animate-bounce-in {
|
||||
animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes bounceIn {
|
||||
0% { opacity: 0; transform: scale(0.3); }
|
||||
50% { opacity: 1; transform: scale(1.05); }
|
||||
70% { transform: scale(0.9); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,8 @@ import { storeToRefs } from 'pinia';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
||||
|
||||
const emit = defineEmits(['view-profile']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -11,14 +13,21 @@ const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
||||
<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" class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all">
|
||||
<div
|
||||
v-for="user in onlineUsers"
|
||||
:key="user.wallet_address"
|
||||
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">
|
||||
@@ -31,14 +40,21 @@ const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
||||
|
||||
<!-- 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" class="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-[#35373c] cursor-pointer group transition-all opacity-60 hover:opacity-100">
|
||||
<div
|
||||
v-for="user in offlineUsers"
|
||||
:key="user.wallet_address"
|
||||
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">
|
||||
|
||||
232
client/src/components/UserProfile.vue
Normal file
232
client/src/components/UserProfile.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useChatStore } from '../stores/chat';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { MessageSquare, Calendar, Edit3, Send, X } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps({
|
||||
address: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { profileUser, profilePosts, isProfileLoading, walletAddress } = storeToRefs(chatStore);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const editBio = ref('');
|
||||
const editBannerColor = ref('');
|
||||
const newPostContent = ref('');
|
||||
|
||||
const loadProfile = () => {
|
||||
chatStore.getProfile(props.address);
|
||||
};
|
||||
|
||||
onMounted(loadProfile);
|
||||
watch(() => props.address, loadProfile);
|
||||
|
||||
const startEditing = () => {
|
||||
editBio.value = profileUser.value.bio || '';
|
||||
editBannerColor.value = profileUser.value.banner_color || '#6366f1';
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
const saveProfile = () => {
|
||||
chatStore.updateProfile(editBio.value, editBannerColor.value);
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
const submitPost = () => {
|
||||
if (!newPostContent.value.trim()) return;
|
||||
chatStore.createPost(newPostContent.value);
|
||||
newPostContent.value = '';
|
||||
};
|
||||
|
||||
const formatTime = (isoString) => {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col h-full bg-discord-dark overflow-y-auto custom-scrollbar">
|
||||
<div
|
||||
v-if="isProfileLoading"
|
||||
class="flex-1 flex items-center justify-center"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-violet-500" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="profileUser"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<!-- Banner -->
|
||||
<div
|
||||
class="h-48 w-full relative transition-colors duration-500"
|
||||
:style="{ backgroundColor: profileUser.banner_color }"
|
||||
>
|
||||
<div class="absolute -bottom-16 left-8">
|
||||
<div class="w-32 h-32 rounded-full border-8 border-discord-dark bg-violet-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl">
|
||||
{{ profileUser.username?.substring(0, 2).toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Info -->
|
||||
<div class="mt-20 px-8 pb-8 border-b border-white/5">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">
|
||||
{{ profileUser.username }}
|
||||
</h1>
|
||||
<p class="text-gray-400 font-mono text-sm mt-1">
|
||||
{{ profileUser.wallet_address }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="profileUser.wallet_address === walletAddress"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-gray-200 text-lg leading-relaxed max-w-2xl">
|
||||
{{ profileUser.bio || 'No bio yet...' }}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-4 text-gray-400 text-sm">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Calendar size="16" /> Joined {{ new Date(profileUser.last_seen).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wall / Posts -->
|
||||
<div class="p-8 max-w-3xl">
|
||||
<h2 class="text-xl font-bold text-white mb-6 flex items-center gap-2">
|
||||
<MessageSquare
|
||||
size="20"
|
||||
class="text-violet-400"
|
||||
/> Wall
|
||||
</h2>
|
||||
|
||||
<!-- New Post Input (only for owner) -->
|
||||
<div
|
||||
v-if="profileUser.wallet_address === walletAddress"
|
||||
class="mb-8 bg-discord-sidebar/30 rounded-2xl p-4 border border-white/5"
|
||||
>
|
||||
<textarea
|
||||
v-model="newPostContent"
|
||||
placeholder="What's on your mind?"
|
||||
class="w-full bg-transparent text-white placeholder-gray-500 resize-none focus:outline-none min-h-[100px]"
|
||||
/>
|
||||
<div class="flex justify-end mt-2 pt-2 border-t border-white/5">
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts List -->
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-if="profilePosts.length === 0"
|
||||
class="text-center py-12 text-gray-500 italic"
|
||||
>
|
||||
No posts yet.
|
||||
</div>
|
||||
<div
|
||||
v-for="post in profilePosts"
|
||||
:key="post.id"
|
||||
class="bg-discord-sidebar/20 rounded-2xl p-6 border border-white/5 hover:border-white/10 transition-all group"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-violet-600/20 flex items-center justify-center text-violet-400 font-bold">
|
||||
{{ profileUser.username?.substring(0, 2).toUpperCase() }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-white">
|
||||
{{ profileUser.username }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ formatTime(post.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-200 leading-relaxed">
|
||||
{{ post.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
>
|
||||
<div class="bg-discord-sidebar border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl animate-pop-in">
|
||||
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-white">
|
||||
Edit Profile
|
||||
</h2>
|
||||
<button
|
||||
class="text-gray-400 hover:text-white transition-colors"
|
||||
@click="isEditing = false"
|
||||
>
|
||||
<X size="24" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Bio</label>
|
||||
<textarea
|
||||
v-model="editBio"
|
||||
class="w-full bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all min-h-[120px]"
|
||||
placeholder="Tell us about yourself..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Banner Color</label>
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
v-model="editBannerColor"
|
||||
type="color"
|
||||
class="w-12 h-12 rounded-lg bg-transparent border-none cursor-pointer"
|
||||
>
|
||||
<input
|
||||
v-model="editBannerColor"
|
||||
type="text"
|
||||
class="flex-1 bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white font-mono"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-white/5 flex gap-3">
|
||||
<button
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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"
|
||||
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', () => {
|
||||
@@ -15,6 +15,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const users = ref([]);
|
||||
const channels = ref([]);
|
||||
|
||||
// Profile state
|
||||
const profileUser = ref(null);
|
||||
const profilePosts = ref([]);
|
||||
const isProfileLoading = ref(false);
|
||||
|
||||
const onlineUsers = computed(() => users.value.filter(u => u.online));
|
||||
const offlineUsers = computed(() => users.value.filter(u => !u.online));
|
||||
|
||||
@@ -45,8 +50,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!messages.value[message.channelId]) {
|
||||
messages.value[message.channelId] = [];
|
||||
}
|
||||
// Use spread to trigger reactivity if needed, though .push should work in Vue 3
|
||||
messages.value[message.channelId] = [...messages.value[message.channelId], message];
|
||||
|
||||
// Check if this message matches a local pending message (by content and wallet)
|
||||
// In a real app, we'd use the txId to match
|
||||
const pendingIdx = messages.value[message.channelId].findIndex(
|
||||
m => m.status === 'pending' && m.content === message.content && m.walletAddress === message.walletAddress
|
||||
);
|
||||
|
||||
if (pendingIdx !== -1) {
|
||||
// Update the pending message with server data and mark as validated
|
||||
messages.value[message.channelId][pendingIdx] = { ...message, status: 'validated' };
|
||||
} else {
|
||||
messages.value[message.channelId] = [...messages.value[message.channelId], { ...message, status: 'validated' }];
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('userList', (userList) => {
|
||||
@@ -63,42 +79,82 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('profileData', (data) => {
|
||||
profileUser.value = data;
|
||||
profilePosts.value = data.posts;
|
||||
isProfileLoading.value = false;
|
||||
});
|
||||
|
||||
socket.value.on('profileUpdated', (data) => {
|
||||
if (profileUser.value && profileUser.value.wallet_address === walletAddress.value) {
|
||||
profileUser.value = { ...profileUser.value, ...data };
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('postCreated', (post) => {
|
||||
profilePosts.value = [post, ...profilePosts.value];
|
||||
});
|
||||
|
||||
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
||||
username.value = newName;
|
||||
const savedAuth = Cookies.get('plexus_auth');
|
||||
if (savedAuth) {
|
||||
const authData = JSON.parse(savedAuth);
|
||||
authData.name = newName;
|
||||
Cookies.set('plexus_auth', JSON.stringify(authData), { expires: 7 });
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
// Handle failed messages if we can identify them
|
||||
// For now, just alert
|
||||
alert(err.message || 'An error occurred');
|
||||
});
|
||||
|
||||
fetchChannels();
|
||||
fetchMessages(currentChannel.value);
|
||||
}
|
||||
|
||||
function toggleReaction(messageId, emoji) {
|
||||
if (!socket.value) return;
|
||||
|
||||
// Simulate a blockchain transaction for reaction
|
||||
console.log('Simulating 1 $PLEXUS transaction for reaction...');
|
||||
|
||||
socket.value.emit('toggleReaction', {
|
||||
messageId,
|
||||
walletAddress: walletAddress.value,
|
||||
emoji
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const savedAuth = Cookies.get('plexus_auth');
|
||||
if (savedAuth) {
|
||||
try {
|
||||
const { wallet, name, sig } = JSON.parse(savedAuth);
|
||||
connect(wallet, name, sig);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved auth', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage(content) {
|
||||
if (!socket.value || !content.trim()) return;
|
||||
|
||||
// Simulate a blockchain transaction
|
||||
const tempId = 'temp-' + Date.now();
|
||||
const pendingMsg = {
|
||||
tempId,
|
||||
channelId: currentChannel.value,
|
||||
walletAddress: walletAddress.value,
|
||||
username: username.value,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'pending',
|
||||
reactions: []
|
||||
};
|
||||
|
||||
// Add to local state immediately
|
||||
if (!messages.value[currentChannel.value]) {
|
||||
messages.value[currentChannel.value] = [];
|
||||
}
|
||||
messages.value[currentChannel.value].push(pendingMsg);
|
||||
|
||||
// Simulate a blockchain transaction delay
|
||||
console.log('Simulating 1 $PLEXUS transaction for message...');
|
||||
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
||||
|
||||
setTimeout(() => {
|
||||
// Randomly fail 5% of the time for demonstration
|
||||
const failed = Math.random() < 0.05;
|
||||
|
||||
if (failed) {
|
||||
const msg = messages.value[currentChannel.value].find(m => m.tempId === tempId);
|
||||
if (msg) msg.status = 'failed';
|
||||
console.error('Transaction failed!');
|
||||
|
||||
// Remove failed message after 5 seconds
|
||||
setTimeout(() => {
|
||||
messages.value[currentChannel.value] = messages.value[currentChannel.value].filter(m => m.tempId !== tempId);
|
||||
}, 5000);
|
||||
} else {
|
||||
socket.value.emit('sendMessage', {
|
||||
channelId: currentChannel.value,
|
||||
walletAddress: walletAddress.value,
|
||||
@@ -106,6 +162,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
txId: mockTxId
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function toggleReaction(messageId, emoji) {
|
||||
if (!socket.value) return;
|
||||
@@ -134,22 +192,28 @@ export const useChatStore = defineStore('chat', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for username updates
|
||||
onMounted(() => {
|
||||
if (socket.value) {
|
||||
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
||||
username.value = newName;
|
||||
// Update cookie
|
||||
const authData = JSON.parse(Cookies.get('plexus_auth') || '{}');
|
||||
authData.name = newName;
|
||||
Cookies.set('plexus_auth', JSON.stringify(authData), { expires: 7 });
|
||||
});
|
||||
function getProfile(address) {
|
||||
if (!socket.value) return;
|
||||
isProfileLoading.value = true;
|
||||
socket.value.emit('getProfile', address);
|
||||
}
|
||||
|
||||
socket.value.on('error', (err) => {
|
||||
alert(err.message || 'An error occurred');
|
||||
function updateProfile(bio, bannerColor) {
|
||||
if (!socket.value) return;
|
||||
socket.value.emit('updateProfile', {
|
||||
walletAddress: walletAddress.value,
|
||||
bio,
|
||||
bannerColor
|
||||
});
|
||||
}
|
||||
|
||||
function createPost(content) {
|
||||
if (!socket.value || !content.trim()) return;
|
||||
socket.value.emit('createPost', {
|
||||
walletAddress: walletAddress.value,
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
function setChannel(channelId) {
|
||||
currentChannel.value = channelId;
|
||||
@@ -189,10 +253,16 @@ export const useChatStore = defineStore('chat', () => {
|
||||
onlineUsers,
|
||||
offlineUsers,
|
||||
currentMessages,
|
||||
profileUser,
|
||||
profilePosts,
|
||||
isProfileLoading,
|
||||
connect,
|
||||
sendMessage,
|
||||
toggleReaction,
|
||||
updateUsername,
|
||||
getProfile,
|
||||
updateProfile,
|
||||
createPost,
|
||||
setChannel
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-crypto-dark text-crypto-text overflow-hidden;
|
||||
@apply bg-discord-dark text-crypto-text overflow-hidden antialiased;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
@@ -12,21 +12,48 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-crypto-dark;
|
||||
@apply bg-discord-black;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-crypto-panel rounded;
|
||||
@apply bg-discord-sidebar rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-crypto-muted;
|
||||
@apply bg-white/10;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@apply backdrop-blur-md bg-white/5 border border-white/10;
|
||||
@apply backdrop-blur-md bg-white/[0.03] border border-white/10;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
@apply backdrop-blur-xl bg-black/40 border-r border-white/5;
|
||||
@apply backdrop-blur-xl bg-discord-sidebar/80 border-r border-white/5;
|
||||
}
|
||||
|
||||
.led {
|
||||
@apply w-2 h-2 rounded-full shadow-lg;
|
||||
}
|
||||
|
||||
.led-orange {
|
||||
@apply bg-status-pending shadow-status-pending/50 animate-led-pulse;
|
||||
}
|
||||
|
||||
.led-green {
|
||||
@apply bg-status-validated shadow-status-validated/50;
|
||||
}
|
||||
|
||||
.led-red {
|
||||
@apply bg-status-failed shadow-status-failed/50 animate-pulse;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness helpers */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-hidden {
|
||||
@apply -translate-x-full;
|
||||
}
|
||||
|
||||
.sidebar-visible {
|
||||
@apply translate-x-0;
|
||||
}
|
||||
}
|
||||
@@ -12,18 +12,34 @@ export default {
|
||||
'crypto-accent': '#8b5cf6', // Violet
|
||||
'crypto-text': '#e2e8f0',
|
||||
'crypto-muted': '#94a3b8',
|
||||
'discord-dark': '#313338',
|
||||
'discord-sidebar': '#2b2d31',
|
||||
'discord-black': '#1e1f22',
|
||||
'status-pending': '#f59e0b', // Orange
|
||||
'status-validated': '#10b981', // Green
|
||||
'status-failed': '#ef4444', // Red
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
sans: ['Inter', 'sans-serif', 'system-ui'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'pop-in': 'popIn 0.2s cubic-bezier(0.26, 0.53, 0.74, 1.48)',
|
||||
'led-pulse': 'ledPulse 2s infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeInUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
popIn: {
|
||||
'0%': { opacity: '0', transform: 'scale(0.8)' },
|
||||
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||
},
|
||||
ledPulse: {
|
||||
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
|
||||
'50%': { opacity: '0.6', transform: 'scale(0.95)' },
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,3 +16,12 @@ services:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
dev-shell:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- .:/app
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
|
||||
21
docs/README.md
Normal file
21
docs/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Plexus Documentation
|
||||
|
||||
Welcome to the official documentation for **Plexus**, a decentralized crypto chat application.
|
||||
|
||||
## 📖 Table of Contents
|
||||
|
||||
- [Architecture Overview](file:///home/sinan/Documents/repositories/local/plexus/docs/architecture.md) - How the system works.
|
||||
- [Development Guide](file:///home/sinan/Documents/repositories/local/plexus/docs/development.md) - Guidelines for contributors.
|
||||
- [Task Tracker](file:///home/sinan/Documents/repositories/local/plexus/docs/tasks.md) - How to use the internal task management system.
|
||||
- [Scalability & Roadmap](file:///home/sinan/Documents/repositories/local/plexus/docs/scalability.md) - Thinking bigger.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
To get started with development, please refer to the [Development Guide](file:///home/sinan/Documents/repositories/local/plexus/docs/development.md).
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3, Vite, TailwindCSS, Pinia.
|
||||
- **Backend**: Node.js, Express, Socket.IO.
|
||||
- **Database**: DuckDB (for chat history and task tracking).
|
||||
- **Blockchain**: Solana (simulated transactions).
|
||||
34
docs/architecture.md
Normal file
34
docs/architecture.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Architecture Overview
|
||||
|
||||
Plexus is designed as a real-time, decentralized-first chat application.
|
||||
|
||||
## 🏗 System Components
|
||||
|
||||
### 1. Frontend (Client)
|
||||
- Built with **Vue 3** and **Vite**.
|
||||
- Uses **Pinia** for state management (messages, users, channels).
|
||||
- Communicates with the backend via **Socket.IO** for real-time updates.
|
||||
- Simulates Solana transactions for messages, reactions, and username changes.
|
||||
|
||||
### 2. Backend (Server)
|
||||
- Built with **Node.js** and **Express**.
|
||||
- Uses **Socket.IO** to handle real-time communication.
|
||||
- **DuckDB** is used as the primary data store for:
|
||||
- User profiles (wallet addresses, unique usernames).
|
||||
- Message history.
|
||||
- Reactions.
|
||||
|
||||
### 3. Database Schema
|
||||
- `users`: `wallet_address` (PK), `username` (UNIQUE), `last_seen`.
|
||||
- `messages`: `id` (PK), `channel_id`, `wallet_address`, `content`, `timestamp`, `tx_id`.
|
||||
- `reactions`: `message_id`, `wallet_address`, `emoji`.
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
1. **User Joins**: Client emits `join` with wallet address and requested username. Server verifies/generates unique username and updates `users` table.
|
||||
2. **Sending Message**: Client simulates transaction, then emits `sendMessage` with `txId`. Server saves message to DuckDB and broadcasts `newMessage` to all clients.
|
||||
3. **Reactions**: Client emits `toggleReaction`. Server updates `reactions` table and broadcasts `updateReactions`.
|
||||
|
||||
## 🛡 Security
|
||||
- Currently uses a simple wallet-based identification.
|
||||
- Future versions will implement full message signing and verification.
|
||||
43
docs/data-model.md
Normal file
43
docs/data-model.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 📊 Data Model
|
||||
|
||||
Plexus uses **DuckDB** for fast, analytical, and serverless-friendly data storage.
|
||||
|
||||
## 🗄 Tables
|
||||
|
||||
### `users`
|
||||
Stores user identity and profile information.
|
||||
- `wallet_address` (VARCHAR, PK): Unique Solana wallet address.
|
||||
- `username` (VARCHAR, UNIQUE): Display name.
|
||||
- `bio` (VARCHAR): User biography.
|
||||
- `banner_color` (VARCHAR): Hex code for profile banner.
|
||||
- `last_seen` (TIMESTAMP): Last activity time.
|
||||
|
||||
### `messages`
|
||||
Stores chat history.
|
||||
- `id` (INTEGER, PK): Unique message ID (from `seq_msg_id`).
|
||||
- `channel_id` (VARCHAR): ID of the channel.
|
||||
- `wallet_address` (VARCHAR, FK): Sender's wallet.
|
||||
- `content` (VARCHAR): Message text.
|
||||
- `timestamp` (TIMESTAMP): Time sent.
|
||||
- `tx_id` (VARCHAR): Simulated transaction ID.
|
||||
|
||||
### `reactions`
|
||||
Stores message reactions.
|
||||
- `message_id` (INTEGER, FK): ID of the message.
|
||||
- `wallet_address` (VARCHAR, FK): User who reacted.
|
||||
- `emoji` (VARCHAR): The emoji character.
|
||||
- *Composite PK*: `(message_id, wallet_address, emoji)`.
|
||||
|
||||
### `posts`
|
||||
Stores social wall posts.
|
||||
- `id` (INTEGER, PK): Unique post ID (from `seq_post_id`).
|
||||
- `wallet_address` (VARCHAR, FK): Owner of the wall.
|
||||
- `content` (VARCHAR): Post text.
|
||||
- `timestamp` (TIMESTAMP): Time posted.
|
||||
|
||||
## 🔢 Sequences
|
||||
- `seq_msg_id`: Increments for each new message.
|
||||
- `seq_post_id`: Increments for each new wall post.
|
||||
|
||||
## 🔄 Migrations
|
||||
The `server/db.js` file handles automatic schema initialization and migrations (e.g., adding `tx_id` or `bio` columns if they are missing from an existing database).
|
||||
36
docs/development.md
Normal file
36
docs/development.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Development Guide
|
||||
|
||||
Welcome to the Plexus development team! Please follow these guidelines to ensure a consistent and high-quality codebase.
|
||||
|
||||
## 🛠 Setup
|
||||
|
||||
1. **Docker**: The easiest way to develop is using the dockerized shell.
|
||||
```bash
|
||||
make shell
|
||||
```
|
||||
2. **Local**: If you prefer local development:
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
## 📝 Coding Standards
|
||||
|
||||
- **Linting**: Always run `make lint` before pushing.
|
||||
- **Testing**: Add unit or integration tests for new features. Use `make test` to run them.
|
||||
- **Documentation**: Update the `docs/` directory if you change architecture or add major features.
|
||||
|
||||
## 🌿 Git Workflow
|
||||
|
||||
- **Commits**: Use descriptive commit messages.
|
||||
- **Task IDs**: Always include the task ID in your commit message if applicable.
|
||||
- Example: `feat: add emoji picker (Task #123)`
|
||||
- **Regular Commits**: Commit early and often. Small, atomic commits are preferred.
|
||||
|
||||
## 📋 Task Management
|
||||
|
||||
We use an internal task tracker powered by DuckDB.
|
||||
- List tasks: `make task-list`
|
||||
- Add task: `make task-add title="My new task"`
|
||||
- Complete task: `make task-done id=1`
|
||||
|
||||
See [Task Tracker Documentation](file:///home/sinan/Documents/repositories/local/plexus/docs/tasks.md) for more details.
|
||||
32
docs/functions.md
Normal file
32
docs/functions.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# ⚙️ Functions & API
|
||||
|
||||
Plexus communicates primarily through WebSockets (Socket.io) for real-time interaction.
|
||||
|
||||
## 🔌 Socket Events
|
||||
|
||||
### 📥 Client to Server
|
||||
- `join({ walletAddress, username })`: Register or login a user.
|
||||
- `sendMessage({ channelId, walletAddress, content, txId })`: Send a message to a channel.
|
||||
- `toggleReaction({ messageId, walletAddress, emoji })`: Toggle a reaction on a message.
|
||||
- `updateUsername({ walletAddress, newUsername, txId })`: Change username (simulated cost).
|
||||
- `getProfile(walletAddress)`: Fetch user profile and wall posts.
|
||||
- `updateProfile({ walletAddress, bio, bannerColor })`: Update profile details.
|
||||
- `createPost({ walletAddress, content })`: Post a message to the user's wall.
|
||||
|
||||
### 📤 Server to Client
|
||||
- `userList(users)`: Broadcast the updated list of online/offline users.
|
||||
- `newMessage(message)`: Broadcast a new message to all clients.
|
||||
- `updateReactions({ messageId, reactions })`: Broadcast updated reactions for a message.
|
||||
- `usernameUpdated({ username })`: Confirm a username change to the specific user.
|
||||
- `profileData(data)`: Send requested profile data to a user.
|
||||
- `profileUpdated(data)`: Confirm profile update.
|
||||
- `postCreated(post)`: Confirm post creation.
|
||||
- `error({ message })`: Send error messages to the client.
|
||||
|
||||
## 🌐 REST API
|
||||
- `GET /api/channels`: List all available chat channels.
|
||||
- `GET /api/messages/:channelId`: Fetch the last 100 messages and reactions for a channel.
|
||||
|
||||
## 🛠 Internal Logic
|
||||
- **Username Authority**: The server validates and ensures unique usernames, appending suffixes if necessary.
|
||||
- **Transaction Simulation**: The client simulates a 1.5s delay and a 5% failure rate for "blockchain" transactions.
|
||||
30
docs/scalability.md
Normal file
30
docs/scalability.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Thinking Bigger: Scalability & Future Roadmap
|
||||
|
||||
Plexus is currently a robust MVP, but to scale to millions of users, we need to consider several architectural improvements.
|
||||
|
||||
## 🚀 Scalability Improvements
|
||||
|
||||
### 1. Backend Scaling
|
||||
- **Load Balancing**: Use Nginx or HAProxy to distribute traffic across multiple Node.js instances.
|
||||
- **Redis for Pub/Sub**: Currently, Socket.IO broadcasts are limited to a single server. Integrating Redis will allow broadcasting across multiple server instances.
|
||||
- **Database Sharding**: As the message history grows, sharding DuckDB or migrating to a distributed database like CockroachDB or ClickHouse (for analytics) may be necessary.
|
||||
|
||||
### 2. Frontend Performance
|
||||
- **Code Splitting**: Use Vite's code-splitting features to reduce initial load times.
|
||||
- **Service Workers**: Implement PWA features for offline support and push notifications.
|
||||
- **Virtual Scrolling**: For channels with thousands of messages, implement virtual scrolling to maintain UI performance.
|
||||
|
||||
## 🛠 Advanced Tooling
|
||||
|
||||
### 1. CI/CD Pipeline
|
||||
- **GitHub Actions**: Automate linting, testing, and deployment.
|
||||
- **Automated Testing**: Expand test coverage with Playwright for E2E testing and Vitest for unit testing.
|
||||
|
||||
### 2. Monitoring & Logging
|
||||
- **Prometheus & Grafana**: Track server performance and user metrics.
|
||||
- **Sentry**: Real-time error tracking and reporting.
|
||||
|
||||
## 🌐 Decentralization Roadmap
|
||||
- **IPFS Integration**: Store media files and message history on IPFS for true decentralization.
|
||||
- **Smart Contracts**: Move the $PLEXUS transaction logic to on-chain Solana programs.
|
||||
- **DAO Governance**: Implement a DAO to allow users to vote on protocol changes.
|
||||
33
docs/structure.md
Normal file
33
docs/structure.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 📂 Project Structure
|
||||
|
||||
Plexus is organized into three main areas: the client, the server, and the internal tooling.
|
||||
|
||||
## 📁 Root Directory
|
||||
- `Makefile`: Central automation script for installation, development, and tasks.
|
||||
- `docker-compose.yml`: Defines the development environment services.
|
||||
- `Dockerfile.dev`: Unified development shell with all dependencies.
|
||||
- `package.json`: Root package for devtooling (Husky, lint-staged).
|
||||
- `pyproject.toml`: Configuration for Python linting (Ruff).
|
||||
|
||||
## 📁 `client/` (Frontend)
|
||||
Built with Vue 3 and Vite.
|
||||
- `src/components/`: Reusable UI components (Chat, Profile, UserList, etc.).
|
||||
- `src/stores/`: Pinia stores for state management (`chat.js`).
|
||||
- `src/style.css`: Global styles and Tailwind utilities.
|
||||
- `tailwind.config.js`: Custom theme and animations.
|
||||
|
||||
## 📁 `server/` (Backend)
|
||||
Built with Node.js and Socket.io.
|
||||
- `index.js`: Main entry point, socket event handlers.
|
||||
- `db.js`: Database initialization and schema management.
|
||||
- `data/`: Contains the DuckDB database file (`chat.duckdb`).
|
||||
- `tests/`: Integration tests for socket events.
|
||||
|
||||
## 📁 `tasks/` (Internal Tooling)
|
||||
Python-based task tracker.
|
||||
- `cli.py`: Command-line interface for managing tasks.
|
||||
- `db.py`: DuckDB backend for task storage.
|
||||
- `tasks.duckdb`: Database for internal tasks.
|
||||
|
||||
## 📁 `docs/` (Documentation)
|
||||
Linked Markdown files covering all aspects of the project.
|
||||
26
docs/tasks.md
Normal file
26
docs/tasks.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Task Tracker
|
||||
|
||||
Plexus uses a custom-built task tracking system to manage its development lifecycle. The data is stored in a local DuckDB database (`data/tasks.duckdb`).
|
||||
|
||||
## 🛠 Commands
|
||||
|
||||
You can interact with the task tracker using the `Makefile`:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make task-list` | List all active tasks. |
|
||||
| `make task-add title="..."` | Add a new task. |
|
||||
| `make task-update id=... status="..."` | Update a task's status. |
|
||||
| `make task-done id=...` | Mark a task as completed. |
|
||||
| `make task-filter status="..."` | Filter tasks by status. |
|
||||
|
||||
## 📊 Task Statuses
|
||||
|
||||
- `todo`: Task is waiting to be started.
|
||||
- `in-progress`: Task is currently being worked on.
|
||||
- `done`: Task is completed.
|
||||
- `blocked`: Task is waiting on external factors.
|
||||
|
||||
## 🐍 Implementation Details
|
||||
|
||||
The task tracker is implemented in Python and uses the `duckdb` library. The source code can be found in the `tasks/` directory.
|
||||
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"
|
||||
}
|
||||
}
|
||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = []
|
||||
26
server/db.js
26
server/db.js
@@ -11,6 +11,8 @@ con.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
wallet_address VARCHAR PRIMARY KEY,
|
||||
username VARCHAR UNIQUE,
|
||||
bio VARCHAR DEFAULT '',
|
||||
banner_color VARCHAR DEFAULT '#6366f1',
|
||||
last_seen TIMESTAMP
|
||||
);
|
||||
|
||||
@@ -32,7 +34,16 @@ con.exec(`
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
wallet_address VARCHAR,
|
||||
content VARCHAR,
|
||||
timestamp TIMESTAMP,
|
||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_msg_id START 1;
|
||||
CREATE SEQUENCE IF NOT EXISTS seq_post_id START 1;
|
||||
|
||||
-- Migration: Add tx_id to messages if it doesn't exist (for existing DBs)
|
||||
PRAGMA table_info('messages');
|
||||
@@ -46,7 +57,20 @@ con.exec(`
|
||||
if (!hasTxId) {
|
||||
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
||||
if (err) console.error("Error adding tx_id column:", err);
|
||||
else console.log("Added tx_id column to messages table");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Migration: Add bio and banner_color to users
|
||||
con.all("PRAGMA table_info('users')", (err, rows) => {
|
||||
if (err) return;
|
||||
const hasBio = rows.some(r => r.name === 'bio');
|
||||
if (!hasBio) {
|
||||
con.run("ALTER TABLE users ADD COLUMN bio VARCHAR DEFAULT ''", (err) => {
|
||||
if (err) console.error("Error adding bio column:", err);
|
||||
});
|
||||
con.run("ALTER TABLE users ADD COLUMN banner_color VARCHAR DEFAULT '#6366f1'", (err) => {
|
||||
if (err) console.error("Error adding banner_color column:", err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,11 +51,13 @@ io.on('connection', (socket) => {
|
||||
|
||||
if (rows.length > 0) {
|
||||
// User exists, update last seen
|
||||
const existingUsername = rows[0].username;
|
||||
con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
||||
if (err) return console.error("Prepare error:", err);
|
||||
uStmt.run(now, walletAddress, (err) => {
|
||||
uStmt.finalize();
|
||||
if (err) console.error("Update error:", err);
|
||||
socket.emit('usernameUpdated', { username: existingUsername });
|
||||
broadcastUserList();
|
||||
});
|
||||
});
|
||||
@@ -77,6 +79,7 @@ io.on('connection', (socket) => {
|
||||
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
||||
iStmt.finalize();
|
||||
if (err) console.error("Insert error:", err);
|
||||
socket.emit('usernameUpdated', { username: finalUsername });
|
||||
broadcastUserList();
|
||||
});
|
||||
});
|
||||
@@ -165,6 +168,61 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('getProfile', (walletAddress) => {
|
||||
console.log(`Fetching profile for ${walletAddress}`);
|
||||
con.prepare(`SELECT wallet_address, username, bio, banner_color, last_seen FROM users WHERE wallet_address = ?`, (err, stmt) => {
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
stmt.all(walletAddress, (err, rows) => {
|
||||
stmt.finalize();
|
||||
if (err || rows.length === 0) return socket.emit('error', { message: 'User not found' });
|
||||
|
||||
const user = rows[0];
|
||||
// Fetch posts
|
||||
con.prepare(`SELECT * FROM posts WHERE wallet_address = ? ORDER BY timestamp DESC LIMIT 50`, (err, pStmt) => {
|
||||
if (err) return socket.emit('profileData', { ...user, posts: [] });
|
||||
pStmt.all(walletAddress, (err, posts) => {
|
||||
pStmt.finalize();
|
||||
socket.emit('profileData', { ...user, posts: posts || [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('updateProfile', ({ walletAddress, bio, bannerColor }) => {
|
||||
console.log(`Updating profile for ${walletAddress}`);
|
||||
con.prepare(`UPDATE users SET bio = ?, banner_color = ? WHERE wallet_address = ?`, (err, stmt) => {
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
stmt.run(bio, bannerColor, walletAddress, (err) => {
|
||||
stmt.finalize();
|
||||
if (err) return socket.emit('error', { message: 'Failed to update profile' });
|
||||
socket.emit('profileUpdated', { bio, bannerColor });
|
||||
broadcastUserList();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('createPost', ({ walletAddress, content }) => {
|
||||
if (!content || content.trim() === '') return;
|
||||
console.log(`New post from ${walletAddress}: ${content}`);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
con.prepare(`INSERT INTO posts (id, wallet_address, content, timestamp) VALUES (nextval('seq_post_id'), ?, ?, ?)`, (err, stmt) => {
|
||||
if (err) return socket.emit('error', { message: 'Database error' });
|
||||
stmt.run(walletAddress, content, timestamp, (err) => {
|
||||
stmt.finalize();
|
||||
if (err) return socket.emit('error', { message: 'Failed to create post' });
|
||||
|
||||
// Fetch all posts to broadcast update or just emit the new one
|
||||
// For simplicity, we'll just tell the user it was created
|
||||
socket.emit('postCreated', { content, timestamp });
|
||||
|
||||
// If we want a live feed, we could broadcast to a "profile room"
|
||||
// For now, the user can just refresh or we emit to them
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('toggleReaction', ({ messageId, walletAddress, emoji }) => {
|
||||
console.log(`Toggling reaction: ${emoji} on message ${messageId} by ${walletAddress}`);
|
||||
con.prepare(`SELECT * FROM reactions WHERE message_id = ? AND wallet_address = ? AND emoji = ?`, (err, stmt) => {
|
||||
|
||||
62
tasks/cli.py
Normal file
62
tasks/cli.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import argparse
|
||||
|
||||
from db import add_task, delete_task, init_db, list_tasks, update_task
|
||||
|
||||
|
||||
def main():
|
||||
init_db()
|
||||
|
||||
parser = argparse.ArgumentParser(description="Plexus Task Tracker")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
# Add task
|
||||
add_parser = subparsers.add_parser("add", help="Add a new task")
|
||||
add_parser.add_argument("title", help="Task title")
|
||||
|
||||
# List tasks
|
||||
list_parser = subparsers.add_parser("list", help="List tasks")
|
||||
list_parser.add_argument("--status", help="Filter by status")
|
||||
|
||||
# Update task
|
||||
update_parser = subparsers.add_parser("update", help="Update task status")
|
||||
update_parser.add_argument("id", type=int, help="Task ID")
|
||||
update_parser.add_argument("status", help="New status")
|
||||
|
||||
# Done task
|
||||
done_parser = subparsers.add_parser("done", help="Mark task as done")
|
||||
done_parser.add_argument("id", type=int, help="Task ID")
|
||||
|
||||
# Delete task
|
||||
delete_parser = subparsers.add_parser("delete", help="Delete a task")
|
||||
delete_parser.add_argument("id", type=int, help="Task ID")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "add":
|
||||
add_task(args.title)
|
||||
print(f"Task added: {args.title}")
|
||||
|
||||
elif args.command == "list":
|
||||
tasks = list_tasks(args.status)
|
||||
print(f"{'ID':<5} {'Status':<15} {'Title'}")
|
||||
print("-" * 40)
|
||||
for t in tasks:
|
||||
print(f"{t[0]:<5} {t[2]:<15} {t[1]}")
|
||||
|
||||
elif args.command == "update":
|
||||
update_task(args.id, args.status)
|
||||
print(f"Task {args.id} updated to {args.status}")
|
||||
|
||||
elif args.command == "done":
|
||||
update_task(args.id, "done")
|
||||
print(f"Task {args.id} marked as done")
|
||||
|
||||
elif args.command == "delete":
|
||||
delete_task(args.id)
|
||||
print(f"Task {args.id} deleted")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
46
tasks/db.py
Normal file
46
tasks/db.py
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
import duckdb
|
||||
|
||||
DB_PATH = "tasks/tasks.duckdb"
|
||||
|
||||
def init_db():
|
||||
|
||||
con = duckdb.connect(DB_PATH)
|
||||
con.execute("""
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
status VARCHAR DEFAULT 'todo',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
con.execute("CREATE SEQUENCE IF NOT EXISTS seq_task_id START 1")
|
||||
con.close()
|
||||
|
||||
def get_connection():
|
||||
return duckdb.connect(DB_PATH)
|
||||
|
||||
def add_task(title):
|
||||
con = get_connection()
|
||||
con.execute("INSERT INTO tasks (id, title) VALUES (nextval('seq_task_id'), ?)", [title])
|
||||
con.close()
|
||||
|
||||
def list_tasks(status=None):
|
||||
con = get_connection()
|
||||
if status:
|
||||
res = con.execute("SELECT * FROM tasks WHERE status = ? ORDER BY id", [status]).fetchall()
|
||||
else:
|
||||
res = con.execute("SELECT * FROM tasks ORDER BY id").fetchall()
|
||||
con.close()
|
||||
return res
|
||||
|
||||
def update_task(task_id, status):
|
||||
con = get_connection()
|
||||
con.execute("UPDATE tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [status, task_id])
|
||||
con.close()
|
||||
|
||||
def delete_task(task_id):
|
||||
con = get_connection()
|
||||
con.execute("DELETE FROM tasks WHERE id = ?", [task_id])
|
||||
con.close()
|
||||
BIN
tasks/tasks.duckdb
Normal file
BIN
tasks/tasks.duckdb
Normal file
Binary file not shown.
Reference in New Issue
Block a user