The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
404 lines
No EOL
14 KiB
TypeScript
404 lines
No EOL
14 KiB
TypeScript
import { useState, forwardRef } from 'react';
|
|
import { Wallet, Plus, Trash2, Zap, Globe, WalletMinimal, CheckCircle, X } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Drawer,
|
|
DrawerContent,
|
|
DrawerDescription,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
DrawerTrigger,
|
|
DrawerClose,
|
|
} from '@/components/ui/drawer';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { useNWC } from '@/hooks/useNWCContext';
|
|
import { useWallet } from '@/hooks/useWallet';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
import type { NWCConnection, NWCInfo } from '@/hooks/useNWC';
|
|
import type { WebLNProvider } from "@webbtc/webln-types";
|
|
|
|
interface WalletModalProps {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
// Extracted AddWalletContent to prevent re-renders
|
|
const AddWalletContent = forwardRef<HTMLDivElement, {
|
|
alias: string;
|
|
setAlias: (value: string) => void;
|
|
connectionUri: string;
|
|
setConnectionUri: (value: string) => void;
|
|
}>(({ alias, setAlias, connectionUri, setConnectionUri }, ref) => (
|
|
<div className="space-y-4 px-4" ref={ref}>
|
|
<div>
|
|
<Label htmlFor="alias">Wallet Name (optional)</Label>
|
|
<Input
|
|
id="alias"
|
|
placeholder="My Lightning Wallet"
|
|
value={alias}
|
|
onChange={(e) => setAlias(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="connection-uri">Connection URI</Label>
|
|
<Textarea
|
|
id="connection-uri"
|
|
placeholder="nostr+walletconnect://..."
|
|
value={connectionUri}
|
|
onChange={(e) => setConnectionUri(e.target.value)}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
));
|
|
AddWalletContent.displayName = 'AddWalletContent';
|
|
|
|
// Recommended wallets for NWC
|
|
const RECOMMENDED_WALLETS = [
|
|
{ name: 'Alby', url: 'https://getalby.com', type: 'extension' },
|
|
{ name: 'Minibits', url: 'https://www.minibits.cash', type: 'mobile' },
|
|
{ name: 'Zeus', url: 'https://zeusln.com', type: 'mobile' },
|
|
{ name: 'Coinos', url: 'https://coinos.io', type: 'web' },
|
|
];
|
|
|
|
// Extracted WalletContent to prevent re-renders
|
|
const WalletContent = forwardRef<HTMLDivElement, {
|
|
webln: WebLNProvider | null;
|
|
hasNWC: boolean;
|
|
connections: NWCConnection[];
|
|
connectionInfo: Record<string, NWCInfo>;
|
|
activeConnection: string | null;
|
|
handleSetActive: (cs: string) => void;
|
|
handleRemoveConnection: (cs: string) => void;
|
|
setAddDialogOpen: (open: boolean) => void;
|
|
}>(({
|
|
webln,
|
|
hasNWC,
|
|
connections,
|
|
connectionInfo,
|
|
activeConnection,
|
|
handleSetActive,
|
|
handleRemoveConnection,
|
|
setAddDialogOpen
|
|
}, ref) => (
|
|
<div className="space-y-4 sm:space-y-6 px-0 sm:px-4 pb-4" ref={ref}>
|
|
{/* NWC First - Primary Option */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-medium text-sm sm:text-base">Nostr Wallet Connect</h3>
|
|
<Button size="sm" variant="outline" onClick={() => setAddDialogOpen(true)}>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
Add Wallet
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Connected Wallets List */}
|
|
{connections.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{connections.map((connection) => {
|
|
const info = connectionInfo[connection.connectionString];
|
|
const isActive = activeConnection === connection.connectionString;
|
|
return (
|
|
<div key={connection.connectionString} className={`flex items-center justify-between p-3 border rounded-lg ${isActive ? 'ring-2 ring-primary bg-primary/5' : ''}`}>
|
|
<div className="flex items-center gap-3">
|
|
<WalletMinimal className="h-4 w-4 text-muted-foreground" />
|
|
<div>
|
|
<p className="text-sm font-medium">
|
|
{connection.alias || info?.alias || 'Lightning Wallet'}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
NWC Connection
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isActive && <CheckCircle className="h-4 w-4 text-green-600" />}
|
|
{!isActive && (
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => handleSetActive(connection.connectionString)}
|
|
>
|
|
<Zap className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => handleRemoveConnection(connection.connectionString)}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4 px-3 border border-dashed rounded-lg">
|
|
<WalletMinimal className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground mb-3">
|
|
Paste your NWC connection string to send zaps
|
|
</p>
|
|
<Button size="sm" onClick={() => setAddDialogOpen(true)}>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
Connect Wallet
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* WebLN - Secondary Option */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between p-2 sm:p-3 border rounded-lg">
|
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
<Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
<div className="min-w-0">
|
|
<p className="text-xs sm:text-sm font-medium">Browser Extension (WebLN)</p>
|
|
<p className="text-[10px] sm:text-xs text-muted-foreground">Auto-detected if installed</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
|
{webln && <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-600" />}
|
|
<Badge variant={webln ? "default" : "secondary"} className="text-[10px] sm:text-xs">
|
|
{webln ? "Ready" : "Not Found"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recommended Wallets - Show when no wallet connected */}
|
|
{!webln && connections.length === 0 && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium text-muted-foreground">Get NWC from:</h4>
|
|
<div className="grid gap-2">
|
|
{RECOMMENDED_WALLETS.map((wallet) => (
|
|
<a
|
|
key={wallet.name}
|
|
href={wallet.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-between p-2 border rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="h-4 w-4 text-yellow-500" />
|
|
<span className="text-sm font-medium">{wallet.name}</span>
|
|
</div>
|
|
<Badge variant="outline" className="text-[10px]">
|
|
{wallet.type}
|
|
</Badge>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
));
|
|
WalletContent.displayName = 'WalletContent';
|
|
|
|
export function WalletModal({ children, className }: WalletModalProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
const [connectionUri, setConnectionUri] = useState('');
|
|
const [alias, setAlias] = useState('');
|
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
const isMobile = useIsMobile();
|
|
|
|
const {
|
|
connections,
|
|
activeConnection,
|
|
connectionInfo,
|
|
addConnection,
|
|
removeConnection,
|
|
setActiveConnection
|
|
} = useNWC();
|
|
|
|
const { webln } = useWallet();
|
|
|
|
const hasNWC = connections.length > 0 && connections.some(c => c.isConnected);
|
|
const { toast } = useToast();
|
|
|
|
const handleAddConnection = async () => {
|
|
if (!connectionUri.trim()) {
|
|
toast({
|
|
title: 'Connection URI required',
|
|
description: 'Please enter a valid NWC connection URI.',
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setIsConnecting(true);
|
|
try {
|
|
const success = await addConnection(connectionUri.trim(), alias.trim() || undefined);
|
|
if (success) {
|
|
setConnectionUri('');
|
|
setAlias('');
|
|
setAddDialogOpen(false);
|
|
}
|
|
} finally {
|
|
setIsConnecting(false);
|
|
}
|
|
};
|
|
|
|
const handleRemoveConnection = (connectionString: string) => {
|
|
removeConnection(connectionString);
|
|
};
|
|
|
|
const handleSetActive = (connectionString: string) => {
|
|
setActiveConnection(connectionString);
|
|
toast({
|
|
title: 'Active wallet changed',
|
|
description: 'The selected wallet is now active for zaps.',
|
|
});
|
|
};
|
|
|
|
const walletContentProps = {
|
|
webln,
|
|
hasNWC,
|
|
connections,
|
|
connectionInfo,
|
|
activeConnection,
|
|
handleSetActive,
|
|
handleRemoveConnection,
|
|
setAddDialogOpen,
|
|
};
|
|
|
|
const addWalletDialog = (
|
|
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-[425px] p-4 sm:p-6">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">Connect NWC Wallet</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
Enter your connection string from a compatible wallet.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<AddWalletContent
|
|
alias={alias}
|
|
setAlias={setAlias}
|
|
connectionUri={connectionUri}
|
|
setConnectionUri={setConnectionUri}
|
|
/>
|
|
<DialogFooter className="px-0 sm:px-4">
|
|
<Button
|
|
onClick={handleAddConnection}
|
|
disabled={isConnecting || !connectionUri.trim()}
|
|
className="w-full"
|
|
>
|
|
{isConnecting ? 'Connecting...' : 'Connect'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<>
|
|
<Drawer open={open} onOpenChange={setOpen}>
|
|
<DrawerTrigger asChild>
|
|
{children || (
|
|
<Button variant="outline" size="sm" className={className}>
|
|
<Wallet className="h-4 w-4 mr-2" />
|
|
Wallet Settings
|
|
</Button>
|
|
)}
|
|
</DrawerTrigger>
|
|
<DrawerContent className="h-full">
|
|
<DrawerHeader className="text-center relative">
|
|
<DrawerClose asChild>
|
|
<Button variant="ghost" size="sm" className="absolute right-4 top-4">
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Close</span>
|
|
</Button>
|
|
</DrawerClose>
|
|
<DrawerTitle className="flex items-center justify-center gap-2 pt-2">
|
|
<Wallet className="h-5 w-5" />
|
|
Lightning Wallet
|
|
</DrawerTitle>
|
|
<DrawerDescription>
|
|
Connect your lightning wallet to send zaps instantly.
|
|
</DrawerDescription>
|
|
</DrawerHeader>
|
|
<div className="overflow-y-auto">
|
|
<WalletContent {...walletContentProps} />
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
{/* Render Add Wallet as a separate Drawer for mobile */}
|
|
<Drawer open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
|
<DrawerContent>
|
|
<DrawerHeader>
|
|
<DrawerTitle>Connect NWC Wallet</DrawerTitle>
|
|
<DrawerDescription>
|
|
Enter your connection string from a compatible wallet.
|
|
</DrawerDescription>
|
|
</DrawerHeader>
|
|
<AddWalletContent
|
|
alias={alias}
|
|
setAlias={setAlias}
|
|
connectionUri={connectionUri}
|
|
setConnectionUri={setConnectionUri}
|
|
/>
|
|
<div className="p-4">
|
|
<Button
|
|
onClick={handleAddConnection}
|
|
disabled={isConnecting || !connectionUri.trim()}
|
|
className="w-full"
|
|
>
|
|
{isConnecting ? 'Connecting...' : 'Connect'}
|
|
</Button>
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
{children || (
|
|
<Button variant="outline" size="sm" className={className}>
|
|
<Wallet className="h-4 w-4 mr-2" />
|
|
Wallet Settings
|
|
</Button>
|
|
)}
|
|
</DialogTrigger>
|
|
<DialogContent className="w-[calc(100%-1rem)] sm:max-w-[500px] max-h-[85vh] max-h-[85dvh] overflow-y-auto p-4 sm:p-6">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2 text-base sm:text-lg">
|
|
<Wallet className="h-5 w-5" />
|
|
Lightning Wallet
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
Connect your lightning wallet to send zaps instantly.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<WalletContent {...walletContentProps} />
|
|
</DialogContent>
|
|
</Dialog>
|
|
{addWalletDialog}
|
|
</>
|
|
);
|
|
} |