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
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
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": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .js,.vue --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
@@ -21,6 +22,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-vue": "^9.21.1",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"vite": "^7.2.4"
|
"vite": "^7.2.4"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ChatLayout from './components/ChatLayout.vue';
|
|||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const videoRef = ref(null);
|
const videoRef = ref(null);
|
||||||
|
|
||||||
const handleMuteToggle = (isMuted) => {
|
const handleMuteToggle = () => {
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
// Note: YouTube iframe API would be needed for true control,
|
// 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.
|
// 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"
|
frameborder="0"
|
||||||
allow="autoplay; encrypted-media"
|
allow="autoplay; encrypted-media"
|
||||||
allowfullscreen
|
allowfullscreen
|
||||||
></iframe>
|
/>
|
||||||
<!-- Overlay gradient -->
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="relative z-10 h-full">
|
<div class="relative z-10 h-full">
|
||||||
<WalletConnect v-if="!chatStore.isConnected" />
|
<WalletConnect v-if="!chatStore.isConnected" />
|
||||||
<ChatLayout v-else @toggleMute="handleMuteToggle" />
|
<ChatLayout
|
||||||
|
v-else
|
||||||
|
@toggle-mute="handleMuteToggle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { storeToRefs } from 'pinia';
|
|||||||
import MessageList from './MessageList.vue';
|
import MessageList from './MessageList.vue';
|
||||||
import UserList from './UserList.vue';
|
import UserList from './UserList.vue';
|
||||||
import MusicPlayer from './MusicPlayer.vue';
|
import MusicPlayer from './MusicPlayer.vue';
|
||||||
import TokenCreator from './TokenCreator.vue';
|
import { Hash, Volume2, VolumeX, Settings, X, Menu, User } from 'lucide-vue-next';
|
||||||
import { Hash, Volume2, VolumeX, Settings, X, Coins } from 'lucide-vue-next';
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const showTokenCreator = ref(false);
|
const showProfile = ref(false);
|
||||||
|
const selectedProfileAddress = ref(null);
|
||||||
|
const showMobileMenu = ref(false);
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
const { channels, currentChannel, username, walletAddress } = storeToRefs(chatStore);
|
||||||
@@ -34,13 +35,28 @@ const saveSettings = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen w-full overflow-hidden relative">
|
<div class="flex h-screen w-full overflow-hidden relative bg-discord-dark">
|
||||||
|
<!-- Mobile Menu Overlay -->
|
||||||
|
<div
|
||||||
|
v-if="showMobileMenu"
|
||||||
|
class="fixed inset-0 bg-black/60 z-40 md:hidden backdrop-blur-sm transition-opacity"
|
||||||
|
@click="showMobileMenu = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div v-if="showSettings" class="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
<div
|
||||||
<div class="bg-crypto-panel border border-white/10 rounded-2xl w-full max-w-md shadow-2xl animate-fade-in-up">
|
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">
|
<div class="p-6 border-b border-white/5 flex items-center justify-between">
|
||||||
<h2 class="text-xl font-bold text-white">Profile Settings</h2>
|
<h2 class="text-xl font-bold text-white">
|
||||||
<button @click="showSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
Profile Settings
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
class="text-gray-400 hover:text-white transition-colors"
|
||||||
|
@click="showSettings = false"
|
||||||
|
>
|
||||||
<X size="24" />
|
<X size="24" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,27 +66,27 @@ const saveSettings = () => {
|
|||||||
<input
|
<input
|
||||||
v-model="newUsername"
|
v-model="newUsername"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full bg-crypto-dark border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
class="w-full bg-discord-black border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-violet-500/50 transition-all"
|
||||||
placeholder="Enter new username"
|
placeholder="Enter new username"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-crypto-muted uppercase tracking-wider mb-2">Wallet Address</label>
|
<label class="block text-xs font-bold text-crypto-muted uppercase tracking-wider mb-2">Wallet Address</label>
|
||||||
<div class="w-full bg-crypto-dark/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
<div class="w-full bg-discord-black/50 border border-white/5 rounded-xl px-4 py-3 text-gray-500 text-sm truncate">
|
||||||
{{ walletAddress }}
|
{{ walletAddress }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 border-t border-white/5 flex gap-3">
|
<div class="p-6 border-t border-white/5 flex gap-3">
|
||||||
<button
|
<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"
|
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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
@@ -79,76 +95,128 @@ const saveSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Channels Sidebar -->
|
<!-- Channels Sidebar -->
|
||||||
<div class="w-64 bg-[#2b2d31] flex flex-col border-r border-black/20">
|
<div
|
||||||
|
:class="[
|
||||||
|
'fixed inset-y-0 left-0 w-64 bg-discord-sidebar flex flex-col border-r border-black/20 z-50 transition-transform duration-300 md:relative md:translate-x-0',
|
||||||
|
showMobileMenu ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="h-12 px-4 flex items-center justify-between border-b border-black/20 shadow-sm">
|
<div class="h-12 px-4 flex items-center justify-between border-b border-black/20 shadow-sm">
|
||||||
<h1 class="font-bold text-white truncate">Plexus Server</h1>
|
<h1 class="font-bold text-white truncate">
|
||||||
<button @click="toggleMute" class="text-gray-400 hover:text-gray-200 transition-colors">
|
Plexus Server
|
||||||
<VolumeX v-if="isMuted" size="18" />
|
</h1>
|
||||||
<Volume2 v-else size="18" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto py-3 space-y-0.5 px-2">
|
<div class="flex-1 overflow-y-auto py-3 space-y-0.5 px-2">
|
||||||
<!-- Token Creator Link -->
|
<!-- Profile Link -->
|
||||||
<button
|
<button
|
||||||
@click="showTokenCreator = true"
|
|
||||||
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group mb-4',
|
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group mb-4',
|
||||||
showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
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" />
|
<User
|
||||||
<span class="text-sm font-medium">Token Creator</span>
|
size="18"
|
||||||
|
class="text-violet-400"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium">My Profile</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">Text Channels</div>
|
<div class="px-2 mb-2 text-[11px] font-bold text-gray-500 uppercase tracking-wider">
|
||||||
<div v-for="channel in channels" :key="channel.id">
|
Text Channels
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="channel in channels"
|
||||||
|
:key="channel.id"
|
||||||
|
>
|
||||||
<button
|
<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',
|
:class="['w-full flex items-center gap-2 px-2 py-1.5 rounded-md transition-all group',
|
||||||
currentChannel === channel.id && !showTokenCreator ? 'bg-[#3f4147] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-gray-200']"
|
currentChannel === channel.id && !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 && !showTokenCreator ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'" />
|
<Hash
|
||||||
|
size="18"
|
||||||
|
:class="currentChannel === channel.id && !showProfile ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'"
|
||||||
|
/>
|
||||||
<span class="text-sm font-medium">{{ channel.name }}</span>
|
<span class="text-sm font-medium">{{ channel.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Music Player & Profile -->
|
<!-- Music Player & Profile -->
|
||||||
<div class="bg-[#232428] p-2 space-y-2">
|
<div class="bg-discord-black p-2 space-y-2">
|
||||||
<MusicPlayer />
|
<MusicPlayer />
|
||||||
|
|
||||||
<div class="flex items-center gap-2 p-1.5 rounded-md hover:bg-[#35373c] transition-all group cursor-pointer" @click="showSettings = true">
|
<div
|
||||||
|
class="flex items-center gap-2 p-1.5 rounded-md hover:bg-[#35373c] transition-all group cursor-pointer"
|
||||||
|
@click="showSettings = true"
|
||||||
|
>
|
||||||
<div class="relative">
|
<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">
|
<div class="w-8 h-8 rounded-full bg-violet-600 flex items-center justify-center text-white text-xs font-bold">
|
||||||
{{ username?.substring(0, 2).toUpperCase() }}
|
{{ username?.substring(0, 2).toUpperCase() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-[#232428] rounded-full"></div>
|
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-discord-black rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-xs font-bold text-white truncate">{{ username }}</div>
|
<div class="text-xs font-bold text-white truncate">
|
||||||
<div class="text-[10px] text-gray-400 truncate">#{{ walletAddress?.slice(-4) }}</div>
|
{{ username }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-gray-400 truncate">
|
||||||
|
#{{ walletAddress?.slice(-4) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Settings size="14" class="text-gray-400 group-hover:text-gray-200" />
|
<Settings
|
||||||
|
size="14"
|
||||||
|
class="text-gray-400 group-hover:text-gray-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 flex flex-col bg-[#313338] relative overflow-hidden">
|
<div class="flex-1 flex flex-col bg-discord-dark relative overflow-hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="h-12 px-4 flex items-center border-b border-black/20 shadow-sm bg-[#313338]/95 backdrop-blur-sm z-10">
|
<div class="h-12 px-4 flex items-center border-b border-black/20 shadow-sm bg-discord-dark/95 backdrop-blur-sm z-10">
|
||||||
<Hash size="20" class="text-gray-400 mr-2" />
|
<button
|
||||||
<span class="font-bold text-white mr-4">{{ showTokenCreator ? 'Token Creator' : currentChannel }}</span>
|
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>
|
||||||
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<div class="flex-1 flex flex-col relative overflow-hidden">
|
<div class="flex-1 flex flex-col relative overflow-hidden">
|
||||||
<TokenCreator v-if="showTokenCreator" @back="showTokenCreator = false" />
|
<UserProfile
|
||||||
<MessageList v-else />
|
v-if="showProfile"
|
||||||
|
:address="selectedProfileAddress"
|
||||||
|
/>
|
||||||
|
<MessageList
|
||||||
|
v-else
|
||||||
|
@view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Member List (Discord Style) -->
|
<!-- Member List (Discord Style) -->
|
||||||
<div class="w-60 bg-[#2b2d31] border-l border-black/20 hidden lg:flex flex-col">
|
<div class="w-60 bg-discord-sidebar border-l border-black/20 hidden xl:flex flex-col">
|
||||||
<UserList />
|
<UserList @view-profile="(addr) => { selectedProfileAddress = addr; showProfile = true; }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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) => {
|
const formatTime = (isoString) => {
|
||||||
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['view-profile']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 flex flex-col h-full bg-black/40 backdrop-blur-sm relative z-10">
|
<div class="flex-1 flex flex-col h-full bg-discord-dark relative z-10">
|
||||||
<!-- Header -->
|
<!-- Header (Desktop only, mobile header is in ChatLayout) -->
|
||||||
<div class="h-12 border-b border-white/5 flex items-center px-4 shadow-sm bg-crypto-panel/50">
|
<div class="hidden md:flex h-12 border-b border-black/20 items-center px-4 shadow-sm bg-discord-dark/95 backdrop-blur-sm">
|
||||||
<div class="text-lg font-bold text-white"># {{ currentChannel }}</div>
|
<div class="text-sm font-bold text-white flex items-center gap-2">
|
||||||
|
<Hash
|
||||||
|
size="18"
|
||||||
|
class="text-gray-400"
|
||||||
|
/>
|
||||||
|
{{ currentChannel }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth">
|
<div
|
||||||
|
ref="messagesContainer"
|
||||||
|
class="flex-1 overflow-y-auto p-4 space-y-1 scroll-smooth custom-scrollbar"
|
||||||
|
>
|
||||||
<!-- Beginning of conversation marker -->
|
<!-- Beginning of conversation marker -->
|
||||||
<div class="py-12 px-4 border-b border-white/5 mb-8">
|
<div class="py-12 px-4 border-b border-white/5 mb-8">
|
||||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center text-white mb-4 shadow-xl shadow-violet-600/20">
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center text-white mb-4 shadow-xl shadow-violet-600/20">
|
||||||
<Hash size="32" />
|
<Hash size="32" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-white mb-1">Welcome to #{{ currentChannel }}!</h2>
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
<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>
|
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>
|
||||||
|
|
||||||
<div v-for="(msg, index) in currentMessages" :key="msg.id"
|
<div
|
||||||
|
v-for="(msg, index) in currentMessages"
|
||||||
|
:key="msg.id || msg.tempId"
|
||||||
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
|
class="group flex gap-4 px-4 py-1 hover:bg-white/[0.02] transition-colors relative"
|
||||||
>
|
>
|
||||||
<!-- Avatar (only if first message in group) -->
|
<!-- Avatar (only if first message in group) -->
|
||||||
<div class="w-10 flex-shrink-0">
|
<div class="w-10 flex-shrink-0">
|
||||||
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
<div
|
||||||
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"
|
v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress"
|
||||||
:class="msg.walletAddress === walletAddress ? 'bg-gradient-to-br from-violet-500 to-fuchsia-600' : 'bg-crypto-panel'"
|
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>
|
||||||
<div v-else class="w-10 text-[10px] text-crypto-muted opacity-0 group-hover:opacity-100 text-right pr-2 pt-1.5 transition-opacity">
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-10 text-[10px] text-crypto-muted opacity-0 group-hover:opacity-100 text-right pr-2 pt-1.5 transition-opacity"
|
||||||
|
>
|
||||||
{{ formatTime(msg.timestamp) }}
|
{{ formatTime(msg.timestamp) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div v-if="index === 0 || currentMessages[index-1].walletAddress !== msg.walletAddress" class="flex items-baseline gap-2 mb-0.5">
|
<div
|
||||||
<span :class="['text-sm font-bold hover:underline cursor-pointer', msg.walletAddress === walletAddress ? 'text-violet-400' : 'text-white']">
|
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 }}
|
{{ msg.username }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="msg.txId" class="text-[9px] text-crypto-muted font-mono bg-white/5 px-1.5 py-0.5 rounded border border-white/5">
|
|
||||||
{{ msg.txId.slice(0, 8) }}
|
|
||||||
</span>
|
|
||||||
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
<span class="text-[10px] text-crypto-muted">{{ formatTime(msg.timestamp) }}</span>
|
||||||
|
|
||||||
|
<!-- Status LED -->
|
||||||
|
<div
|
||||||
|
v-if="msg.status"
|
||||||
|
class="led ml-1"
|
||||||
|
:class="{
|
||||||
|
'led-orange': msg.status === 'pending',
|
||||||
|
'led-green': msg.status === 'validated',
|
||||||
|
'led-red': msg.status === 'failed'
|
||||||
|
}"
|
||||||
|
:title="msg.status"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-100 text-sm leading-relaxed break-words">
|
<div :class="['text-sm leading-relaxed break-words', msg.status === 'failed' ? 'text-status-failed line-through opacity-60' : 'text-gray-100']">
|
||||||
{{ msg.content }}
|
{{ msg.content }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reactions Display -->
|
<!-- 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
|
<button
|
||||||
v-for="emoji in getUniqueEmojis(msg.reactions)"
|
v-for="emoji in getUniqueEmojis(msg.reactions)"
|
||||||
:key="emoji"
|
: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',
|
: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)
|
hasUserReacted(msg.reactions, emoji)
|
||||||
? 'bg-violet-500/20 border-violet-500/50 text-violet-300'
|
? 'bg-violet-500/20 border-violet-500/50 text-violet-300'
|
||||||
: 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10']"
|
: 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10']"
|
||||||
|
@click="toggleReaction(msg.id, emoji)"
|
||||||
>
|
>
|
||||||
<span>{{ emoji }}</span>
|
<span>{{ emoji }}</span>
|
||||||
<span class="font-bold">{{ getReactionCount(msg.reactions, emoji) }}</span>
|
<span class="font-bold">{{ getReactionCount(msg.reactions, emoji) }}</span>
|
||||||
@@ -120,22 +160,28 @@ const formatTime = (isoString) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover Actions -->
|
<!-- Hover Actions -->
|
||||||
<div class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-crypto-panel border border-white/10 rounded-lg p-1 shadow-xl">
|
<div
|
||||||
|
v-if="msg.status !== 'failed'"
|
||||||
|
class="absolute right-4 -top-4 opacity-0 group-hover:opacity-100 transition-opacity z-20 flex gap-1 bg-discord-sidebar border border-white/10 rounded-lg p-1 shadow-xl"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
|
|
||||||
class="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-all"
|
class="p-1.5 hover:bg-white/10 rounded text-gray-400 hover:text-white transition-all"
|
||||||
title="Add Reaction"
|
title="Add Reaction"
|
||||||
|
@click="showEmojiPicker = showEmojiPicker === msg.id ? null : msg.id"
|
||||||
>
|
>
|
||||||
<Smile size="16" />
|
<Smile size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Emoji Picker Popover -->
|
<!-- Emoji Picker Popover -->
|
||||||
<div v-if="showEmojiPicker === msg.id" class="absolute right-0 bottom-full mb-2 bg-crypto-panel border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-fade-in-up">
|
<div
|
||||||
|
v-if="showEmojiPicker === msg.id"
|
||||||
|
class="absolute right-0 bottom-full mb-2 bg-discord-sidebar border border-white/10 rounded-xl p-2 shadow-2xl flex gap-1 z-30 animate-pop-in"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="emoji in EMOJIS"
|
v-for="emoji in EMOJIS"
|
||||||
:key="emoji"
|
:key="emoji"
|
||||||
@click="toggleReaction(msg.id, emoji)"
|
|
||||||
class="hover:scale-125 transition-transform p-1 text-lg"
|
class="hover:scale-125 transition-transform p-1 text-lg"
|
||||||
|
@click="toggleReaction(msg.id, emoji)"
|
||||||
>
|
>
|
||||||
{{ emoji }}
|
{{ emoji }}
|
||||||
</button>
|
</button>
|
||||||
@@ -145,22 +191,33 @@ const formatTime = (isoString) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input -->
|
<!-- Input -->
|
||||||
<div class="p-4 bg-crypto-panel/80 border-t border-white/5">
|
<div class="p-4 bg-discord-dark">
|
||||||
<div class="relative">
|
<div class="relative bg-discord-sidebar/50 rounded-xl border border-white/5 p-1 transition-all focus-within:border-violet-500/30 focus-within:bg-discord-sidebar/80">
|
||||||
<input
|
<input
|
||||||
v-model="newMessage"
|
v-model="newMessage"
|
||||||
@keyup.enter="send"
|
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="`Message #${currentChannel}`"
|
:placeholder="`Message #${currentChannel}`"
|
||||||
class="w-full bg-crypto-dark/50 text-white placeholder-gray-500 rounded-lg py-3 pl-4 pr-12 focus:outline-none focus:ring-2 focus:ring-crypto-accent/50 border border-white/5 transition-all"
|
class="w-full bg-transparent text-white placeholder-gray-500 py-3 pl-4 pr-12 focus:outline-none"
|
||||||
/>
|
@keyup.enter="send"
|
||||||
|
>
|
||||||
<button
|
<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"
|
@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" />
|
<Send size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-2 flex items-center gap-4 px-1">
|
||||||
|
<div class="flex items-center gap-1.5 text-[10px] text-gray-500 uppercase tracking-widest font-bold">
|
||||||
|
<div class="led led-orange w-1.5 h-1.5" /> 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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -51,41 +51,66 @@ const updateVolume = () => {
|
|||||||
class="hidden"
|
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`"
|
: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"
|
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">
|
<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" />
|
<Music size="24" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-sm font-bold text-white truncate">Lofi Radio</div>
|
<div class="text-sm font-bold text-white truncate">
|
||||||
<div class="text-[10px] text-crypto-muted uppercase tracking-wider">Chilling in the Nebula</div>
|
Lofi Radio
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-crypto-muted uppercase tracking-wider">
|
||||||
|
Chilling in the Nebula
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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">
|
<button
|
||||||
<Pause v-if="isPlaying" size="18" />
|
class="p-2 bg-white/10 hover:bg-white/20 rounded-full text-white transition-all"
|
||||||
<Play v-else size="18" />
|
@click="togglePlay"
|
||||||
|
>
|
||||||
|
<Pause
|
||||||
|
v-if="isPlaying"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
|
<Play
|
||||||
|
v-else
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
</button>
|
</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" />
|
<SkipForward size="18" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-3">
|
<div class="mt-4 flex items-center gap-3">
|
||||||
<button @click="toggleMute" class="text-gray-400 hover:text-white transition-colors">
|
<button
|
||||||
<VolumeX v-if="isMuted || volume == 0" size="16" />
|
class="text-gray-400 hover:text-white transition-colors"
|
||||||
<Volume2 v-else size="16" />
|
@click="toggleMute"
|
||||||
|
>
|
||||||
|
<VolumeX
|
||||||
|
v-if="isMuted || volume == 0"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<Volume2
|
||||||
|
v-else
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
v-model="volume"
|
v-model="volume"
|
||||||
@input="updateVolume"
|
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
class="flex-1 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-indigo-500"
|
class="flex-1 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-indigo-500"
|
||||||
/>
|
@input="updateVolume"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 chatStore = useChatStore();
|
||||||
const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
||||||
|
|
||||||
|
const emit = defineEmits(['view-profile']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,14 +13,21 @@ const { onlineUsers, offlineUsers } = storeToRefs(chatStore);
|
|||||||
<div class="flex-1 overflow-y-auto p-3 space-y-6">
|
<div class="flex-1 overflow-y-auto p-3 space-y-6">
|
||||||
<!-- Online Users -->
|
<!-- Online Users -->
|
||||||
<div v-if="onlineUsers.length > 0">
|
<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 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="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">
|
<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() }}
|
{{ user.username.substring(0, 2).toUpperCase() }}
|
||||||
</div>
|
</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>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-sm font-medium text-gray-300 truncate group-hover:text-white transition-colors">
|
<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 -->
|
<!-- Offline Users -->
|
||||||
<div v-if="offlineUsers.length > 0">
|
<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 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="relative">
|
||||||
<div class="w-8 h-8 rounded-full bg-[#3f4147] flex items-center justify-center text-xs font-bold text-gray-500">
|
<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() }}
|
{{ user.username.substring(0, 2).toUpperCase() }}
|
||||||
</div>
|
</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>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-sm font-medium text-gray-500 truncate group-hover:text-gray-400">
|
<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>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useChatStore } from '../stores/chat';
|
import { useChatStore } from '../stores/chat';
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@@ -42,19 +42,28 @@ const connectWallet = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center h-screen bg-black/50 backdrop-blur-sm">
|
<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">
|
<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>
|
<h1 class="text-3xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-pink-600 text-transparent bg-clip-text">
|
||||||
<p class="text-crypto-muted mb-8">Connect your wallet to join the conversation.</p>
|
Crypto Chat
|
||||||
|
</h1>
|
||||||
|
<p class="text-crypto-muted mb-8">
|
||||||
|
Connect your wallet to join the conversation.
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="connectWallet"
|
|
||||||
:disabled="isConnecting"
|
:disabled="isConnecting"
|
||||||
class="w-full py-3 px-6 bg-crypto-accent hover:bg-violet-600 text-white rounded-lg font-semibold transition-all transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
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-if="isConnecting">Connecting...</span>
|
||||||
<span v-else>Connect Phantom Wallet</span>
|
<span v-else>Connect Phantom Wallet</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
export const useChatStore = defineStore('chat', () => {
|
export const useChatStore = defineStore('chat', () => {
|
||||||
@@ -15,6 +15,11 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const users = ref([]);
|
const users = ref([]);
|
||||||
const channels = 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 onlineUsers = computed(() => users.value.filter(u => u.online));
|
||||||
const offlineUsers = 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]) {
|
if (!messages.value[message.channelId]) {
|
||||||
messages.value[message.channelId] = [];
|
messages.value[message.channelId] = [];
|
||||||
}
|
}
|
||||||
// Use spread to trigger reactivity if needed, though .push should work in Vue 3
|
|
||||||
messages.value[message.channelId] = [...messages.value[message.channelId], message];
|
// Check if this message matches a local pending message (by content and wallet)
|
||||||
|
// In a real app, we'd use the txId to match
|
||||||
|
const pendingIdx = messages.value[message.channelId].findIndex(
|
||||||
|
m => m.status === 'pending' && m.content === message.content && m.walletAddress === message.walletAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingIdx !== -1) {
|
||||||
|
// Update the pending message with server data and mark as validated
|
||||||
|
messages.value[message.channelId][pendingIdx] = { ...message, status: 'validated' };
|
||||||
|
} else {
|
||||||
|
messages.value[message.channelId] = [...messages.value[message.channelId], { ...message, status: 'validated' }];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('userList', (userList) => {
|
socket.value.on('userList', (userList) => {
|
||||||
@@ -63,48 +79,90 @@ 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();
|
fetchChannels();
|
||||||
fetchMessages(currentChannel.value);
|
fetchMessages(currentChannel.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReaction(messageId, emoji) {
|
|
||||||
if (!socket.value) return;
|
|
||||||
|
|
||||||
// Simulate a blockchain transaction for reaction
|
|
||||||
console.log('Simulating 1 $PLEXUS transaction for reaction...');
|
|
||||||
|
|
||||||
socket.value.emit('toggleReaction', {
|
|
||||||
messageId,
|
|
||||||
walletAddress: walletAddress.value,
|
|
||||||
emoji
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const savedAuth = Cookies.get('plexus_auth');
|
|
||||||
if (savedAuth) {
|
|
||||||
try {
|
|
||||||
const { wallet, name, sig } = JSON.parse(savedAuth);
|
|
||||||
connect(wallet, name, sig);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse saved auth', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendMessage(content) {
|
function sendMessage(content) {
|
||||||
if (!socket.value || !content.trim()) return;
|
if (!socket.value || !content.trim()) return;
|
||||||
|
|
||||||
// Simulate a blockchain transaction
|
const tempId = 'temp-' + Date.now();
|
||||||
|
const pendingMsg = {
|
||||||
|
tempId,
|
||||||
|
channelId: currentChannel.value,
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
username: username.value,
|
||||||
|
content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'pending',
|
||||||
|
reactions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to local state immediately
|
||||||
|
if (!messages.value[currentChannel.value]) {
|
||||||
|
messages.value[currentChannel.value] = [];
|
||||||
|
}
|
||||||
|
messages.value[currentChannel.value].push(pendingMsg);
|
||||||
|
|
||||||
|
// Simulate a blockchain transaction delay
|
||||||
console.log('Simulating 1 $PLEXUS transaction for message...');
|
console.log('Simulating 1 $PLEXUS transaction for message...');
|
||||||
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
const mockTxId = 'TX' + Math.random().toString(36).substring(2, 15).toUpperCase();
|
||||||
|
|
||||||
socket.value.emit('sendMessage', {
|
setTimeout(() => {
|
||||||
channelId: currentChannel.value,
|
// Randomly fail 5% of the time for demonstration
|
||||||
walletAddress: walletAddress.value,
|
const failed = Math.random() < 0.05;
|
||||||
content,
|
|
||||||
txId: mockTxId
|
if (failed) {
|
||||||
});
|
const msg = messages.value[currentChannel.value].find(m => m.tempId === tempId);
|
||||||
|
if (msg) msg.status = 'failed';
|
||||||
|
console.error('Transaction failed!');
|
||||||
|
|
||||||
|
// Remove failed message after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
messages.value[currentChannel.value] = messages.value[currentChannel.value].filter(m => m.tempId !== tempId);
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
socket.value.emit('sendMessage', {
|
||||||
|
channelId: currentChannel.value,
|
||||||
|
walletAddress: walletAddress.value,
|
||||||
|
content,
|
||||||
|
txId: mockTxId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReaction(messageId, emoji) {
|
function toggleReaction(messageId, emoji) {
|
||||||
@@ -134,22 +192,28 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for username updates
|
function getProfile(address) {
|
||||||
onMounted(() => {
|
if (!socket.value) return;
|
||||||
if (socket.value) {
|
isProfileLoading.value = true;
|
||||||
socket.value.on('usernameUpdated', ({ username: newName }) => {
|
socket.value.emit('getProfile', address);
|
||||||
username.value = newName;
|
}
|
||||||
// Update cookie
|
|
||||||
const authData = JSON.parse(Cookies.get('plexus_auth') || '{}');
|
|
||||||
authData.name = newName;
|
|
||||||
Cookies.set('plexus_auth', JSON.stringify(authData), { expires: 7 });
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.value.on('error', (err) => {
|
function updateProfile(bio, bannerColor) {
|
||||||
alert(err.message || 'An error occurred');
|
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) {
|
function setChannel(channelId) {
|
||||||
currentChannel.value = channelId;
|
currentChannel.value = channelId;
|
||||||
@@ -189,10 +253,16 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
onlineUsers,
|
onlineUsers,
|
||||||
offlineUsers,
|
offlineUsers,
|
||||||
currentMessages,
|
currentMessages,
|
||||||
|
profileUser,
|
||||||
|
profilePosts,
|
||||||
|
isProfileLoading,
|
||||||
connect,
|
connect,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
toggleReaction,
|
toggleReaction,
|
||||||
updateUsername,
|
updateUsername,
|
||||||
|
getProfile,
|
||||||
|
updateProfile,
|
||||||
|
createPost,
|
||||||
setChannel
|
setChannel
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-crypto-dark text-crypto-text overflow-hidden;
|
@apply bg-discord-dark text-crypto-text overflow-hidden antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
@@ -12,21 +12,48 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-crypto-dark;
|
@apply bg-discord-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-crypto-panel rounded;
|
@apply bg-discord-sidebar rounded-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
@apply bg-crypto-muted;
|
@apply bg-white/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass {
|
.glass {
|
||||||
@apply backdrop-blur-md bg-white/5 border border-white/10;
|
@apply backdrop-blur-md bg-white/[0.03] border border-white/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
@apply backdrop-blur-xl bg-black/40 border-r border-white/5;
|
@apply backdrop-blur-xl bg-discord-sidebar/80 border-r border-white/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led {
|
||||||
|
@apply w-2 h-2 rounded-full shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-orange {
|
||||||
|
@apply bg-status-pending shadow-status-pending/50 animate-led-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-green {
|
||||||
|
@apply bg-status-validated shadow-status-validated/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.led-red {
|
||||||
|
@apply bg-status-failed shadow-status-failed/50 animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness helpers */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-hidden {
|
||||||
|
@apply -translate-x-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-visible {
|
||||||
|
@apply translate-x-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,18 +12,34 @@ export default {
|
|||||||
'crypto-accent': '#8b5cf6', // Violet
|
'crypto-accent': '#8b5cf6', // Violet
|
||||||
'crypto-text': '#e2e8f0',
|
'crypto-text': '#e2e8f0',
|
||||||
'crypto-muted': '#94a3b8',
|
'crypto-muted': '#94a3b8',
|
||||||
|
'discord-dark': '#313338',
|
||||||
|
'discord-sidebar': '#2b2d31',
|
||||||
|
'discord-black': '#1e1f22',
|
||||||
|
'status-pending': '#f59e0b', // Orange
|
||||||
|
'status-validated': '#10b981', // Green
|
||||||
|
'status-failed': '#ef4444', // Red
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ['Inter', 'sans-serif', 'system-ui'],
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
'fade-in-up': 'fadeInUp 0.3s ease-out',
|
||||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'pop-in': 'popIn 0.2s cubic-bezier(0.26, 0.53, 0.74, 1.48)',
|
||||||
|
'led-pulse': 'ledPulse 2s infinite',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
fadeInUp: {
|
fadeInUp: {
|
||||||
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
popIn: {
|
||||||
|
'0%': { opacity: '0', transform: 'scale(0.8)' },
|
||||||
|
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
},
|
||||||
|
ledPulse: {
|
||||||
|
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
'50%': { opacity: '0.6', transform: 'scale(0.95)' },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,3 +16,12 @@ services:
|
|||||||
- "8080:80"
|
- "8080:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- server
|
- 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 (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
wallet_address VARCHAR PRIMARY KEY,
|
wallet_address VARCHAR PRIMARY KEY,
|
||||||
username VARCHAR UNIQUE,
|
username VARCHAR UNIQUE,
|
||||||
|
bio VARCHAR DEFAULT '',
|
||||||
|
banner_color VARCHAR DEFAULT '#6366f1',
|
||||||
last_seen TIMESTAMP
|
last_seen TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,7 +34,16 @@ con.exec(`
|
|||||||
FOREIGN KEY (wallet_address) REFERENCES users(wallet_address)
|
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_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)
|
-- Migration: Add tx_id to messages if it doesn't exist (for existing DBs)
|
||||||
PRAGMA table_info('messages');
|
PRAGMA table_info('messages');
|
||||||
@@ -46,7 +57,20 @@ con.exec(`
|
|||||||
if (!hasTxId) {
|
if (!hasTxId) {
|
||||||
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
con.run("ALTER TABLE messages ADD COLUMN tx_id VARCHAR", (err) => {
|
||||||
if (err) console.error("Error adding tx_id column:", 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) {
|
if (rows.length > 0) {
|
||||||
// User exists, update last seen
|
// User exists, update last seen
|
||||||
|
const existingUsername = rows[0].username;
|
||||||
con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
con.prepare(`UPDATE users SET last_seen = ? WHERE wallet_address = ?`, (err, uStmt) => {
|
||||||
if (err) return console.error("Prepare error:", err);
|
if (err) return console.error("Prepare error:", err);
|
||||||
uStmt.run(now, walletAddress, (err) => {
|
uStmt.run(now, walletAddress, (err) => {
|
||||||
uStmt.finalize();
|
uStmt.finalize();
|
||||||
if (err) console.error("Update error:", err);
|
if (err) console.error("Update error:", err);
|
||||||
|
socket.emit('usernameUpdated', { username: existingUsername });
|
||||||
broadcastUserList();
|
broadcastUserList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -77,6 +79,7 @@ io.on('connection', (socket) => {
|
|||||||
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
iStmt.run(walletAddress, finalUsername, now, (err) => {
|
||||||
iStmt.finalize();
|
iStmt.finalize();
|
||||||
if (err) console.error("Insert error:", err);
|
if (err) console.error("Insert error:", err);
|
||||||
|
socket.emit('usernameUpdated', { username: finalUsername });
|
||||||
broadcastUserList();
|
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 }) => {
|
socket.on('toggleReaction', ({ messageId, walletAddress, emoji }) => {
|
||||||
console.log(`Toggling reaction: ${emoji} on message ${messageId} by ${walletAddress}`);
|
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) => {
|
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