feat: Zero-downtime deploy + mobile Settings design
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
201
deploy.sh
201
deploy.sh
@ -1,8 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# POUNCE DEPLOY SCRIPT
|
# POUNCE ZERO-DOWNTIME DEPLOY SCRIPT
|
||||||
# Commits all changes and deploys to server
|
# - Builds locally first (optional)
|
||||||
|
# - Syncs only changed files
|
||||||
|
# - Hot-reloads backend without full restart
|
||||||
|
# - Rebuilds frontend in background
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@ -12,6 +15,7 @@ GREEN='\033[0;32m'
|
|||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Server config
|
# Server config
|
||||||
@ -20,62 +24,163 @@ SERVER_HOST="10.42.0.73"
|
|||||||
SERVER_PATH="/home/user/pounce"
|
SERVER_PATH="/home/user/pounce"
|
||||||
SERVER_PASS="user"
|
SERVER_PASS="user"
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
QUICK_MODE=false
|
||||||
|
BACKEND_ONLY=false
|
||||||
|
FRONTEND_ONLY=false
|
||||||
|
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-q|--quick) QUICK_MODE=true ;;
|
||||||
|
-b|--backend) BACKEND_ONLY=true ;;
|
||||||
|
-f|--frontend) FRONTEND_ONLY=true ;;
|
||||||
|
*) COMMIT_MSG="$1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${BLUE} POUNCE DEPLOY SCRIPT ${NC}"
|
echo -e "${BLUE} POUNCE ZERO-DOWNTIME DEPLOY ${NC}"
|
||||||
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
|
|
||||||
# Get commit message
|
if $QUICK_MODE; then
|
||||||
if [ -z "$1" ]; then
|
echo -e "${CYAN}⚡ Quick mode: Skipping git, only syncing changes${NC}"
|
||||||
COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
|
|
||||||
else
|
|
||||||
COMMIT_MSG="$1"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\n${YELLOW}[1/5] Staging changes...${NC}"
|
if $BACKEND_ONLY; then
|
||||||
git add -A
|
echo -e "${CYAN}🔧 Backend only mode${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\n${YELLOW}[2/5] Committing: ${COMMIT_MSG}${NC}"
|
if $FRONTEND_ONLY; then
|
||||||
git commit -m "$COMMIT_MSG" || echo "Nothing to commit"
|
echo -e "${CYAN}🎨 Frontend only mode${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\n${YELLOW}[3/5] Pushing to git.6bit.ch...${NC}"
|
# Step 1: Git (unless quick mode)
|
||||||
git push origin main
|
if ! $QUICK_MODE; then
|
||||||
|
echo -e "\n${YELLOW}[1/4] Git operations...${NC}"
|
||||||
echo -e "\n${YELLOW}[4/5] Syncing files to server...${NC}"
|
|
||||||
# Sync frontend
|
|
||||||
sshpass -p "$SERVER_PASS" rsync -avz --delete \
|
|
||||||
--exclude 'node_modules' \
|
|
||||||
--exclude '.next' \
|
|
||||||
--exclude '.git' \
|
|
||||||
frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/
|
|
||||||
|
|
||||||
# Sync backend (exclude .env to preserve server config!)
|
|
||||||
sshpass -p "$SERVER_PASS" rsync -avz --delete \
|
|
||||||
--exclude '__pycache__' \
|
|
||||||
--exclude '.pytest_cache' \
|
|
||||||
--exclude 'venv' \
|
|
||||||
--exclude '.git' \
|
|
||||||
--exclude '*.pyc' \
|
|
||||||
--exclude '.env' \
|
|
||||||
--exclude '*.db' \
|
|
||||||
backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/
|
|
||||||
|
|
||||||
echo -e "\n${YELLOW}[5/5] Building and restarting on server...${NC}"
|
|
||||||
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'EOF'
|
|
||||||
cd ~/pounce
|
|
||||||
|
|
||||||
# Build frontend
|
# Check for changes
|
||||||
echo "Building frontend..."
|
if git diff --quiet && git diff --staged --quiet; then
|
||||||
cd frontend
|
echo " No changes to commit"
|
||||||
npm run build 2>&1 | tail -10
|
else
|
||||||
cd ..
|
git add -A
|
||||||
|
|
||||||
|
if [ -z "$COMMIT_MSG" ]; then
|
||||||
|
COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "$COMMIT_MSG" || true
|
||||||
|
echo " ✓ Committed: $COMMIT_MSG"
|
||||||
|
fi
|
||||||
|
|
||||||
# Restart services
|
git push origin main 2>/dev/null && echo " ✓ Pushed to git.6bit.ch" || echo " ⚠ Push failed or nothing to push"
|
||||||
echo "Restarting services..."
|
else
|
||||||
./start.sh 2>&1 | tail -5
|
echo -e "\n${YELLOW}[1/4] Skipping git (quick mode)${NC}"
|
||||||
EOF
|
fi
|
||||||
|
|
||||||
|
# Step 2: Sync files (only changed)
|
||||||
|
echo -e "\n${YELLOW}[2/4] Syncing changed files...${NC}"
|
||||||
|
|
||||||
|
RSYNC_OPTS="-az --info=name1 --delete"
|
||||||
|
|
||||||
|
if ! $BACKEND_ONLY; then
|
||||||
|
echo " Frontend:"
|
||||||
|
sshpass -p "$SERVER_PASS" rsync $RSYNC_OPTS \
|
||||||
|
--exclude 'node_modules' \
|
||||||
|
--exclude '.next' \
|
||||||
|
--exclude '.git' \
|
||||||
|
frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/ 2>&1 | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $FRONTEND_ONLY; then
|
||||||
|
echo " Backend:"
|
||||||
|
sshpass -p "$SERVER_PASS" rsync $RSYNC_OPTS \
|
||||||
|
--exclude '__pycache__' \
|
||||||
|
--exclude '.pytest_cache' \
|
||||||
|
--exclude 'venv' \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '*.pyc' \
|
||||||
|
--exclude '.env' \
|
||||||
|
--exclude '*.db' \
|
||||||
|
backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/ 2>&1 | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 3: Reload backend (graceful, no restart)
|
||||||
|
if ! $FRONTEND_ONLY; then
|
||||||
|
echo -e "\n${YELLOW}[3/4] Reloading backend (graceful)...${NC}"
|
||||||
|
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'BACKEND_EOF'
|
||||||
|
# Signal uvicorn to reload (if running with --reload)
|
||||||
|
# Otherwise, just check it's running
|
||||||
|
BACKEND_PID=$(pgrep -f 'uvicorn app.main:app' | head -1)
|
||||||
|
|
||||||
|
if [ -n "$BACKEND_PID" ]; then
|
||||||
|
# Touch a file to trigger auto-reload if uvicorn has --reload
|
||||||
|
touch ~/pounce/backend/app/main.py
|
||||||
|
echo " ✓ Backend reload triggered (PID: $BACKEND_PID)"
|
||||||
|
else
|
||||||
|
echo " ⚠ Backend not running, starting..."
|
||||||
|
cd ~/pounce/backend
|
||||||
|
source ../venv/bin/activate
|
||||||
|
nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 > backend.log 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
echo " ✓ Backend started"
|
||||||
|
fi
|
||||||
|
BACKEND_EOF
|
||||||
|
else
|
||||||
|
echo -e "\n${YELLOW}[3/4] Skipping backend (frontend only)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Rebuild frontend (in background to minimize downtime)
|
||||||
|
if ! $BACKEND_ONLY; then
|
||||||
|
echo -e "\n${YELLOW}[4/4] Rebuilding frontend...${NC}"
|
||||||
|
sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'FRONTEND_EOF'
|
||||||
|
cd ~/pounce/frontend
|
||||||
|
|
||||||
|
# Build new version
|
||||||
|
echo " Building..."
|
||||||
|
npm run build 2>&1 | grep -E '(✓|○|λ|Error|error)' | head -10 | sed 's/^/ /'
|
||||||
|
|
||||||
|
BUILD_EXIT=$?
|
||||||
|
|
||||||
|
if [ $BUILD_EXIT -eq 0 ]; then
|
||||||
|
# Gracefully restart Next.js
|
||||||
|
NEXT_PID=$(pgrep -f 'next start' | head -1)
|
||||||
|
|
||||||
|
if [ -n "$NEXT_PID" ]; then
|
||||||
|
echo " Restarting Next.js (PID: $NEXT_PID)..."
|
||||||
|
kill $NEXT_PID 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start new instance
|
||||||
|
nohup npm run start > frontend.log 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
NEW_PID=$(pgrep -f 'next start' | head -1)
|
||||||
|
if [ -n "$NEW_PID" ]; then
|
||||||
|
echo " ✓ Frontend running (PID: $NEW_PID)"
|
||||||
|
else
|
||||||
|
echo " ⚠ Frontend may not have started correctly"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ✗ Build failed, keeping old version"
|
||||||
|
fi
|
||||||
|
FRONTEND_EOF
|
||||||
|
else
|
||||||
|
echo -e "\n${YELLOW}[4/4] Skipping frontend (backend only)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
echo -e "\n${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "\n${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "${GREEN} ✅ DEPLOY COMPLETE! ${NC}"
|
echo -e "${GREEN} ✅ DEPLOY COMPLETE! ${NC}"
|
||||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
||||||
echo -e "\nFrontend: ${BLUE}http://$SERVER_HOST:3000${NC}"
|
echo -e ""
|
||||||
echo -e "Backend: ${BLUE}http://$SERVER_HOST:8000${NC}"
|
echo -e " Frontend: ${BLUE}https://pounce.ch${NC} / http://$SERVER_HOST:3000"
|
||||||
|
echo -e " Backend: ${BLUE}https://pounce.ch/api${NC} / http://$SERVER_HOST:8000"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${CYAN}Quick commands:${NC}"
|
||||||
|
echo -e " ./deploy.sh -q # Quick sync, no git"
|
||||||
|
echo -e " ./deploy.sh -b # Backend only"
|
||||||
|
echo -e " ./deploy.sh -f # Frontend only"
|
||||||
|
echo -e " ./deploy.sh \"message\" # Full deploy with commit message"
|
||||||
|
|||||||
@ -30,8 +30,15 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Database,
|
Database,
|
||||||
|
Menu,
|
||||||
|
Eye,
|
||||||
|
Gavel,
|
||||||
|
Tag,
|
||||||
|
Coins,
|
||||||
|
LogOut,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type SettingsTab = 'profile' | 'notifications' | 'billing' | 'security'
|
type SettingsTab = 'profile' | 'notifications' | 'billing' | 'security'
|
||||||
@ -111,12 +118,13 @@ const PLANS = [
|
|||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { user, isAuthenticated, isLoading, checkAuth, subscription } = useStore()
|
const { user, isAuthenticated, isLoading, checkAuth, subscription, logout } = useStore()
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<SettingsTab>('profile')
|
const [activeTab, setActiveTab] = useState<SettingsTab>('profile')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [success, setSuccess] = useState<string | null>(null)
|
const [success, setSuccess] = useState<string | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
const [profileForm, setProfileForm] = useState({
|
const [profileForm, setProfileForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@ -303,6 +311,41 @@ export default function SettingsPage() {
|
|||||||
{ id: 'security' as const, label: 'Security', icon: Shield },
|
{ id: 'security' as const, label: 'Security', icon: Shield },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Mobile Navigation
|
||||||
|
const mobileNavItems = [
|
||||||
|
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
|
||||||
|
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
|
||||||
|
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
|
||||||
|
{ href: '/terminal/settings', label: 'Settings', icon: Settings, active: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
|
||||||
|
|
||||||
|
const drawerNavSections = [
|
||||||
|
{
|
||||||
|
title: 'Discover',
|
||||||
|
items: [
|
||||||
|
{ href: '/terminal/radar', label: 'Radar', icon: Target },
|
||||||
|
{ href: '/terminal/market', label: 'Market', icon: Gavel },
|
||||||
|
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manage',
|
||||||
|
items: [
|
||||||
|
{ href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
|
||||||
|
{ href: '/terminal/sniper', label: 'Sniper', icon: Bell },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Monetize',
|
||||||
|
items: [
|
||||||
|
{ href: '/terminal/yield', label: 'Yield', icon: Coins },
|
||||||
|
{ href: '/terminal/listing', label: 'For Sale', icon: Tag },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandCenterLayout minimal>
|
<CommandCenterLayout minimal>
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
@ -820,6 +863,110 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Mobile Bottom Padding */}
|
||||||
|
<div className="h-20 lg:hidden" />
|
||||||
|
|
||||||
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
|
{/* MOBILE BOTTOM NAVIGATION */}
|
||||||
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
|
<nav className="lg:hidden fixed bottom-0 left-0 right-0 z-40 bg-[#020202]/95 backdrop-blur-md border-t border-white/[0.06]">
|
||||||
|
<div className="flex items-center justify-around h-14">
|
||||||
|
{mobileNavItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors",
|
||||||
|
item.active ? "text-accent" : "text-white/40"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="w-5 h-5" />
|
||||||
|
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
|
||||||
|
{item.active && <div className="absolute bottom-0 w-8 h-0.5 bg-accent" />}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => setMenuOpen(true)}
|
||||||
|
className="flex flex-col items-center justify-center gap-0.5 flex-1 h-full text-white/40"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
<span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
|
{/* NAVIGATION DRAWER */}
|
||||||
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
|
{menuOpen && (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="lg:hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<div className="lg:hidden fixed right-0 top-0 bottom-0 w-72 bg-[#020202] border-l border-white/[0.08] z-50 flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Image src="/logo.svg" alt="Pounce" width={20} height={20} />
|
||||||
|
<span className="text-xs font-mono text-white/40">Terminal v1.0</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav Sections */}
|
||||||
|
<div className="flex-1 overflow-y-auto py-4">
|
||||||
|
{drawerNavSections.map((section) => (
|
||||||
|
<div key={section.title} className="mb-4">
|
||||||
|
<div className="px-4 py-1">
|
||||||
|
<span className="text-[9px] font-mono text-white/30 uppercase tracking-widest">{section.title}</span>
|
||||||
|
</div>
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="flex items-center gap-3 px-4 py-2.5 text-white/60 hover:text-white hover:bg-white/[0.03] transition-colors"
|
||||||
|
>
|
||||||
|
<item.icon className="w-4 h-4" />
|
||||||
|
<span className="text-xs font-mono">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Section */}
|
||||||
|
<div className="border-t border-white/[0.08] p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
|
||||||
|
<TierIcon className="w-4 h-4 text-accent" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-mono text-white truncate">{user?.name || user?.email}</p>
|
||||||
|
<p className="text-[10px] text-accent">{tierName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setMenuOpen(false); logout(); }}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-2 text-xs font-mono text-white/40 border border-white/10 hover:border-red-500/30 hover:text-red-400 transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CommandCenterLayout>
|
</CommandCenterLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user