feat: UX improvements - credits, draggable sasquatch, mobile fixes
1. AboutPage: add Credits section (Zapstore, ContextVM, Angor, Geyser, MKStack, Shakespeare, Nostr), add Zapstore download button, fix powered-by text overflow on mobile (wrap-friendly flex layout) 2. SasquatchGuide: make minimized bubble draggable on all devices - pointer events for mouse, touch, and stylus - saves position to localStorage, persists across sessions - tap still opens, drag moves (5px threshold to distinguish) 3. Layout (burger menu): add overflow-y-auto + pb-8 to SheetContent so mobile menu is scrollable when content exceeds screen height 4. CashuWalletCard (wallet): fix input+button rows overflowing on mobile - flex-col on small screens, flex-row on xs+ (stacked on phones) - whitespace-nowrap on buttons to prevent text wrapping 5. LabPage: compact action buttons on mobile (h-8, text-[10px], px-2) so all 4 buttons fit in 2x2 grid without overflow/truncation 6. LoginDialog (Amber): improve visibility-change handler - use fresh SimplePool subscription on each foreground return instead of querySync (which doesn't exist in nostr-tools v2.13) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
84d80ac3a7
commit
5dffa5cac3
6 changed files with 231 additions and 108 deletions
|
|
@ -571,8 +571,8 @@ export function Layout({ children }: LayoutProps) {
|
|||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-80">
|
||||
<div className="flex flex-col gap-6 mt-6">
|
||||
<SheetContent side="right" className="w-80 overflow-y-auto">
|
||||
<div className="flex flex-col gap-6 mt-6 pb-8">
|
||||
{/* Wallet Card - Mobile Menu */}
|
||||
{user && (
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Falls back to static FAQ if AI is unavailable.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
|
@ -322,6 +322,53 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
|||
const [streamingText, setStreamingText] = useState('');
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Draggable position state for minimized bubble
|
||||
const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
|
||||
const dragMoved = useRef(false);
|
||||
|
||||
// Dragging handlers
|
||||
const onPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
dragMoved.current = false;
|
||||
const rect = dragRef.current.getBoundingClientRect();
|
||||
dragStart.current = { mx: e.clientX, my: e.clientY, bx: rect.left, by: rect.top };
|
||||
dragRef.current.setPointerCapture(e.pointerId);
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragStart.current) return;
|
||||
const dx = e.clientX - dragStart.current.mx;
|
||||
const dy = e.clientY - dragStart.current.my;
|
||||
if (!dragMoved.current && Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
|
||||
dragMoved.current = true;
|
||||
setIsDragging(true);
|
||||
const newX = Math.max(0, Math.min(window.innerWidth - 56, dragStart.current.bx + dx));
|
||||
const newY = Math.max(0, Math.min(window.innerHeight - 56, dragStart.current.by + dy));
|
||||
setDragPos({ x: newX, y: newY });
|
||||
}, [setDragPos]);
|
||||
|
||||
const onPointerUp = useCallback((e: React.PointerEvent) => {
|
||||
if (dragRef.current) dragRef.current.releasePointerCapture(e.pointerId);
|
||||
const wasDrag = dragMoved.current;
|
||||
dragStart.current = null;
|
||||
dragMoved.current = false;
|
||||
setTimeout(() => setIsDragging(false), 50);
|
||||
if (!wasDrag) {
|
||||
setIsMinimized(false);
|
||||
}
|
||||
}, [setIsMinimized]);
|
||||
|
||||
const bubbleStyle = useMemo(() => {
|
||||
if (dragPos) {
|
||||
return { position: 'fixed' as const, left: dragPos.x, top: dragPos.y, bottom: 'auto', right: 'auto', zIndex: 50 };
|
||||
}
|
||||
return undefined;
|
||||
}, [dragPos]);
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
const timer = setTimeout(() => setIsVisible(true), 500);
|
||||
|
|
@ -501,12 +548,25 @@ Instructions:
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* ── Minimized bubble — always visible, bottom-left ── */}
|
||||
{/* ── Minimized bubble — draggable ── */}
|
||||
{isMinimized && (
|
||||
<div className="fixed bottom-20 md:bottom-6 left-3 md:left-4 z-50">
|
||||
<button
|
||||
onClick={() => setIsMinimized(false)}
|
||||
className="relative group flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full shadow-2xl border-2 border-[#ccff00]/60 hover:border-[#ccff00] transition-all duration-200 hover:scale-110 active:scale-95"
|
||||
<div
|
||||
ref={dragRef}
|
||||
className={cn(
|
||||
'z-50 touch-none select-none',
|
||||
dragPos ? '' : 'fixed bottom-20 md:bottom-6 left-3 md:left-4',
|
||||
)}
|
||||
style={bubbleStyle}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
title="Press and drag to move · Tap to open"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative group flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full shadow-2xl border-2 border-[#ccff00]/60 transition-all duration-200',
|
||||
isDragging ? 'scale-110 border-[#ccff00] cursor-grabbing' : 'hover:border-[#ccff00] hover:scale-110 active:scale-95 cursor-grab',
|
||||
)}
|
||||
style={{
|
||||
background: 'radial-gradient(circle at 40% 35%, #2d4a1e 0%, #1a2e0f 50%, #0d1a07 100%)',
|
||||
boxShadow: '0 0 20px rgba(204,255,0,0.2), 0 4px 20px rgba(0,0,0,0.8)',
|
||||
|
|
@ -516,13 +576,11 @@ Instructions:
|
|||
<img
|
||||
src="/images/sasquatch-guide.webp"
|
||||
alt="The Sasquatch"
|
||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover"
|
||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover pointer-events-none"
|
||||
/>
|
||||
{/* Neon pulse ring */}
|
||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00]" />
|
||||
{/* Live dot */}
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,49 +157,54 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
|||
const pending = pendingRef.current || loadPendingNC();
|
||||
if (!pending || amberStep !== 'waiting') return;
|
||||
|
||||
setAmberStatus('Checking relay for Amber response...');
|
||||
|
||||
// Quick one-shot relay check when app comes back to foreground
|
||||
const { nsecDecode } = await import('nostr-tools/nip19').catch(() => ({ nsecDecode: null }));
|
||||
if (!nsecDecode) return;
|
||||
setAmberStatus('Checking for Amber response...');
|
||||
|
||||
try {
|
||||
const decoded = nip19.decode(pending.clientNsec);
|
||||
if (decoded.type !== 'nsec') return;
|
||||
const clientSk = decoded.data;
|
||||
|
||||
const pool = new SimplePool();
|
||||
const since = Math.floor(pending.timestamp / 1000) - 30;
|
||||
|
||||
// Fetch recent events — one-shot query
|
||||
const events = await pool.querySync(
|
||||
NOSTRCONNECT_RELAYS,
|
||||
{
|
||||
kinds: [24133],
|
||||
'#p': [pending.clientPubkey],
|
||||
since,
|
||||
},
|
||||
{ maxWait: 5000 }
|
||||
);
|
||||
// Re-subscribe with a fresh pool each time app comes to foreground
|
||||
const freshPool = new SimplePool();
|
||||
let found = false;
|
||||
|
||||
pool.close(NOSTRCONNECT_RELAYS);
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => { resolve(); }, 6000);
|
||||
|
||||
for (const event of events) {
|
||||
try {
|
||||
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
|
||||
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
|
||||
if (parsed.error && parsed.error !== '') continue;
|
||||
if (parsed.result !== pending.secret && parsed.result !== 'ack') continue;
|
||||
const sub = freshPool.subscribeMany(
|
||||
NOSTRCONNECT_RELAYS,
|
||||
[{ kinds: [24133], '#p': [pending.clientPubkey], since }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
if (found) return;
|
||||
try {
|
||||
const decrypted = await nip04.decrypt(clientSk, event.pubkey, event.content);
|
||||
const parsed = JSON.parse(decrypted) as { id: string; result: string; error?: string };
|
||||
if (parsed.error && parsed.error !== '') return;
|
||||
if (parsed.result !== pending.secret && parsed.result !== 'ack') return;
|
||||
found = true;
|
||||
clearTimeout(timeout);
|
||||
sub.close();
|
||||
freshPool.close(NOSTRCONNECT_RELAYS);
|
||||
pendingRef.current = null;
|
||||
abortRef.current?.abort();
|
||||
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
||||
resolve();
|
||||
} catch { /* wrong encryption */ }
|
||||
},
|
||||
oneose: () => {
|
||||
// All caught up — wait a bit more then give up
|
||||
setTimeout(() => { sub.close(); freshPool.close(NOSTRCONNECT_RELAYS); resolve(); }, 2000);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Found it!
|
||||
pendingRef.current = null;
|
||||
abortRef.current?.abort();
|
||||
await completeLogin(event.pubkey, clientSk, pending.clientNsec, pending.relays);
|
||||
return;
|
||||
} catch { /* wrong encryption or format */ }
|
||||
if (!found) {
|
||||
setAmberStatus('Not received yet. Did you approve in Amber? Tap retry or come back after approving.');
|
||||
}
|
||||
|
||||
setAmberStatus('No response yet — did you approve in Amber?');
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -294,24 +294,25 @@ export function CashuWalletCard() {
|
|||
{/* Receive Tab */}
|
||||
<TabsContent value="receive" className="space-y-4 mt-4">
|
||||
{/* Lightning Invoice */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Receive Lightning Payment</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in sats"
|
||||
value={quoteAmount}
|
||||
onChange={(e) => setQuoteAmount(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleLightningReceive}
|
||||
disabled={!activeMintUrl || !quoteAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
Generate Invoice
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Receive Lightning Payment</Label>
|
||||
<div className="flex flex-col xs:flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in sats"
|
||||
value={quoteAmount}
|
||||
onChange={(e) => setQuoteAmount(e.target.value)}
|
||||
className="min-w-0"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleLightningReceive}
|
||||
disabled={!activeMintUrl || !quoteAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||
>
|
||||
Generate Invoice
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
|
|
@ -357,24 +358,25 @@ export function CashuWalletCard() {
|
|||
{/* Send Tab */}
|
||||
<TabsContent value="send" className="space-y-4 mt-4">
|
||||
{/* Send Token */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Create Cashu Token</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in sats"
|
||||
value={sendAmount}
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTokenSend}
|
||||
disabled={!activeMintUrl || !sendAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
Create Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Create Cashu Token</Label>
|
||||
<div className="flex flex-col xs:flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Amount in sats"
|
||||
value={sendAmount}
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
className="min-w-0"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTokenSend}
|
||||
disabled={!activeMintUrl || !sendAmount}
|
||||
className="bg-orange-500 hover:bg-orange-600 whitespace-nowrap xs:flex-shrink-0"
|
||||
>
|
||||
Create Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import {
|
|||
ExternalLink,
|
||||
BarChart3,
|
||||
AlertCircle,
|
||||
Download,
|
||||
Smartphone,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function AboutPage() {
|
||||
|
|
@ -235,26 +237,82 @@ Version: v0.1.0`;
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Download on Zapstore */}
|
||||
<Card className="bg-black/90 border-2 border-[#ccff00]/50 mt-8">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div className="flex-1 text-center sm:text-left">
|
||||
<div className="flex items-center gap-2 justify-center sm:justify-start mb-2">
|
||||
<Smartphone className="h-5 w-5 text-[#ccff00]" />
|
||||
<h3 className="font-black uppercase text-[#ccff00]">Download the App</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">
|
||||
Install SkateSpots on Android via Zapstore — the open-source, Bitcoin-native app store.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://zapstore.dev/apps/org.skatespots.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Button className="gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase px-6">
|
||||
<Download className="h-4 w-4" />
|
||||
Get on Zapstore
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Credits */}
|
||||
<Card className="bg-black/90 border-2 border-gray-700 mt-8">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-black uppercase text-white mb-4 flex items-center gap-2">
|
||||
<Heart className="h-5 w-5 text-[#ff0040]" />
|
||||
Credits
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 gap-3 text-sm">
|
||||
{[
|
||||
{ label: 'Built with', name: 'MKStack', url: 'https://soapbox.pub/mkstack', desc: 'Nostr client framework' },
|
||||
{ label: 'Vibed with', name: 'Shakespeare', url: 'https://shakespeare.diy', desc: 'AI-powered development' },
|
||||
{ label: 'Powered by', name: 'Nostr', url: 'https://nostr.com', desc: 'Decentralized protocol' },
|
||||
{ label: 'Crowdfunding', name: 'Geyser', url: 'https://geyser.fund/project/skatespots', desc: 'Bitcoin Lightning crowdfunding' },
|
||||
{ label: 'Crowdfunding', name: 'Angor', url: 'https://angor.io/', desc: 'Bitcoin crowdfunding platform' },
|
||||
{ label: 'Distributed on', name: 'Zapstore', url: 'https://zapstore.dev/apps/org.skatespots.app', desc: 'Open-source app store' },
|
||||
{ label: 'AI powered by', name: 'ContextVM', url: 'https://contextvm.org/s/e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530', desc: 'Nostr AI infrastructure' },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-black/50 border border-gray-800 hover:border-[#ccff00]/40 hover:bg-[#ccff00]/5 transition-all group"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 text-gray-600 group-hover:text-[#ccff00] flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-gray-500 text-xs">{item.label}: </span>
|
||||
<span className="font-bold text-white group-hover:text-[#ccff00] transition-colors">{item.name}</span>
|
||||
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Powered By */}
|
||||
<div className="text-center mt-12 p-6">
|
||||
<p className="text-gray-400 mb-2 uppercase tracking-wide">Powered by</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<a
|
||||
href="https://nostr.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ccff00] hover:text-[#ccff00]/80 font-black text-xl uppercase"
|
||||
>
|
||||
<div className="text-center mt-8 pb-6 px-4">
|
||||
<p className="text-gray-400 mb-3 uppercase tracking-wide text-sm">Powered by</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 md:gap-3">
|
||||
<a href="https://nostr.com" target="_blank" rel="noopener noreferrer"
|
||||
className="text-[#ccff00] hover:text-[#ccff00]/80 font-black text-lg md:text-xl uppercase">
|
||||
Nostr
|
||||
</a>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="text-[#ff0040] font-black text-xl uppercase">
|
||||
Decentralized
|
||||
</span>
|
||||
<span className="text-[#ff0040] font-black text-lg md:text-xl uppercase">Decentralized</span>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="text-orange-400 font-black text-xl uppercase">
|
||||
Community-Owned
|
||||
</span>
|
||||
<span className="text-orange-400 font-black text-lg md:text-xl uppercase">Community-Owned</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -111,29 +111,29 @@ export default function LabPage() {
|
|||
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
||||
</div>
|
||||
{user ? (
|
||||
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2 w-full md:w-auto">
|
||||
<Link to="/become-coach" className="col-span-1">
|
||||
<Button className="w-full bg-red-500 hover:bg-red-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<div className="grid grid-cols-2 gap-1.5 w-full md:w-auto md:flex md:flex-wrap md:gap-2">
|
||||
<Link to="/become-coach">
|
||||
<Button size="sm" className="w-full bg-red-500 hover:bg-red-600 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.offerCoaching')}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/become-filmer" className="col-span-1">
|
||||
<Button className="w-full bg-orange-500 hover:bg-orange-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<Link to="/become-filmer">
|
||||
<Button size="sm" className="w-full bg-orange-500 hover:bg-orange-600 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.offerFilming')}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/become-editor" className="col-span-1">
|
||||
<Button className="w-full bg-purple-600 hover:bg-purple-700 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<Link to="/become-editor">
|
||||
<Button size="sm" className="w-full bg-purple-600 hover:bg-purple-700 gap-1 uppercase font-black text-[10px] sm:text-xs h-8 px-2">
|
||||
<Plus className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">Offer Editing</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/card" className="col-span-1">
|
||||
<Button variant="outline" className="w-full gap-1.5 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs sm:text-sm">
|
||||
<CreditCard className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">Get Your Card</span>
|
||||
<Link to="/card">
|
||||
<Button size="sm" variant="outline" className="w-full gap-1 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-[10px] sm:text-xs h-8 px-2">
|
||||
<CreditCard className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate">Get Card</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue